Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions web-admin/src/features/navigation/nav-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,29 @@ export function getScreenNameFromPage(page: Page): MetricsEventScreenName {
}
return MetricsEventScreenName.Unknown;
}

/**
* Identifies the resource a page is showing, for telemetry.
* Returns empty strings on pages that aren't showing a named resource, in which case consumers fall
* back to parsing the resource out of the page URL.
*
* The type values are also produced by that URL fallback, which lives in the `rill_ui_telemetry_model`
* model of the rill-cloud-metrics project. Keep the two vocabularies in step: they land in the same
* column, so a value only used by one of them reads as two different resource types over time.
*/
export function getResourceFromPage(page: Page): {
type: string;
name: string;
} {
switch (true) {
case isMetricsExplorerPage(page):
return { type: "explore", name: page.params.dashboard ?? "" };
case isCanvasDashboardPage(page):
return { type: "canvas", name: page.params.dashboard ?? "" };
case isReportPage(page):
return { type: "report", name: page.params.report ?? "" };
case isAlertPage(page):
return { type: "alert", name: page.params.alert ?? "" };
}
return { type: "", name: "" };
}
30 changes: 29 additions & 1 deletion web-admin/src/routes/[organization]/[project]/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
getAdminServiceListDeploymentsQueryKey,
} from "@rilldata/web-admin/client";
import {
getResourceFromPage,
getScreenNameFromPage,
isEditPage,
isProjectInvitePage,
isProjectPage,
Expand All @@ -51,7 +53,10 @@
import { viewAsUserStore } from "@rilldata/web-admin/features/view-as-user/viewAsUserStore";
import ErrorPage from "@rilldata/web-common/components/ErrorPage.svelte";
import { themeControl } from "@rilldata/web-common/features/themes/theme-control";
import { metricsService } from "@rilldata/web-common/metrics/initMetrics";
import {
behaviourEvent,
metricsService,
} from "@rilldata/web-common/metrics/initMetrics";
import RuntimeProvider from "@rilldata/web-common/runtime-client/v2/RuntimeProvider.svelte";
import type { HTTPError } from "@rilldata/web-common/lib/errors";
import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient.ts";
Expand Down Expand Up @@ -246,6 +251,29 @@
});
}
});

// Fire a page view for every dashboard, canvas and project page the user opens.
// This runs off `page` rather than `afterNavigate` because the first page view has to wait for the
// current user query above to resolve, which happens after the initial navigation has completed.
//
// Views are deduped on the path plus the explore view mode. The view mode is a query param rather
// than a path segment, so keying on the path alone would never record a user switching to pivot or
// time dimension detail. The rest of the query string is deliberately excluded: filters, time range
// and sort change on nearly every interaction, and keying on them would emit an event per click.
let lastTrackedView: string | undefined;
$effect(() => {
const trackedView = `${page.url.pathname}?view=${page.url.searchParams.get("view") ?? ""}`;
// Events are dropped until loadCloudFields has run, so wait on the same inputs it needs.
if (!project || !$user.data?.user?.id || trackedView === lastTrackedView)
return;
lastTrackedView = trackedView;
const resource = getResourceFromPage(page);
void behaviourEvent?.firePageViewEvent(
getScreenNameFromPage(page),
resource.type,
resource.name,
);
});
</script>

{#if error}
Expand Down
13 changes: 13 additions & 0 deletions web-common/src/metrics/BehaviourEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export class BehaviourEventHandler {
this.commonUserMetrics = commonUserMetrics;
}

public firePageViewEvent(
screen_name: MetricsEventScreenName,
resource_type: string,
resource_name: string,
) {
return this.metricsService.dispatch("pageViewEvent", [
this.commonUserMetrics,
screen_name,
resource_type,
resource_name,
]);
}

public fireNavigationEvent(
entity_name: string,
medium: BehaviourEventMedium,
Expand Down
20 changes: 20 additions & 0 deletions web-common/src/metrics/service/BehaviourEventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ import {
import type { SourceConnectionType, SourceFileType } from "./SourceEventTypes";

export class BehaviourEventFactory extends MetricsEventFactory {
public pageViewEvent(
commonFields: CommonFields,
commonUserFields: CommonUserFields,
screen_name: MetricsEventScreenName,
resource_type: string,
resource_name: string,
): BehaviourEvent {
const event = this.getBaseMetricsEvent(
"behavioral",
BehaviourEventAction.PageView,
commonFields,
commonUserFields,
) as BehaviourEvent;
event.action = BehaviourEventAction.PageView;
event.screen_name = screen_name;
event.resource_type = resource_type;
event.resource_name = resource_name;
return event;
}

public navigationEvent(
commonFields: CommonFields,
commonUserFields: CommonUserFields,
Expand Down
9 changes: 9 additions & 0 deletions web-common/src/metrics/service/BehaviourEventTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import type { MetricsEventScreenName, MetricsEventSpace } from "./MetricsTypes";
import type { SourceEventFields } from "./SourceEventTypes";

export enum BehaviourEventAction {
// PageView fires on every page load and navigation.
// Navigate is narrower: it only fires when the user clicks through to a resource from
// somewhere in the app, and records where they came from.
PageView = "page-view",
Navigate = "navigate",

DeployIntent = "deploy-intent",
Expand Down Expand Up @@ -68,6 +72,11 @@ export interface BehaviourEvent
screen_name: MetricsEventScreenName;
source_screen: MetricsEventScreenName;
count: number;
// The resource the page is showing, e.g. "explore" and the name of the explore dashboard.
// Sent unhashed, unlike `entity_id`, because these identify a deployed cloud resource that the
// page URL already names in plain text.
resource_type: string;
resource_name: string;
}

export interface AddDataBehaviourEventFields {
Expand Down
1 change: 1 addition & 0 deletions web-common/src/metrics/service/MetricsEventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class MetricsEventFactory {
event_time: new Date().toISOString(),
event_type: eventType,
event_name: eventName,
page_url: window.location.href,
};
}
}
7 changes: 7 additions & 0 deletions web-common/src/metrics/service/MetricsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export interface MetricsEvent extends CommonFields, CommonUserFields {
event_time: string;
event_type: string;
event_name: string;
// The URL at the moment the event was fired.
// This is what attributes an event to the dashboard or canvas the user was on, since the resource
// name only exists in the URL.
// Note that the query string is a snapshot, not a history: it reflects the dashboard state at the
// instant of this event, so what it tells you depends on when the event fires. Page views are
// deduped per path and view mode, so their filters and time range are whatever the page loaded with.
page_url: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are guarding against path not search params in layout, time range, filters etc might not get logged.

If we want url params then we need to remove the guard from layout around url path. But this will lead to explosion of events since url changes quite often, we could optimize debouncing by say 1sec, but might still be too much.

If we only want explore/canvas names then how about emitting just that? org, project_id, resourceName, resourceType might give full picture.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not concerned about event volume, I think we can handle it. At some point it would be good to have info for at least most used time ranges, dimensions and measures but semantics is the issue here loading a dimension on dashboard may not mean it is being used, if its filtered on, expanded, sorted by etc. then it may counted as used and I think we may need it for canvas as well not just explore.

So may be we start simple for now with just resource name and type and then later add these things.

// Legacy:
event_datetime: number;
}
Expand Down
Loading