Skip to content
Merged
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: "" };
}
45 changes: 44 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,11 +53,17 @@
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";
import { getRuntimeServiceListResourcesQueryKey } from "@rilldata/web-common/runtime-client";
import { Throttler } from "@rilldata/web-common/lib/throttler";

const PAGE_VIEW_THROTTLE_TIMEOUT = 250;

let { children }: { children: Snippet } = $props();

Expand Down Expand Up @@ -246,6 +254,41 @@
});
}
});

// 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.
//
// Events are throttled so that pages the user only passes through, like clicking into a dashboard
// and immediately hitting back, don't each record a view. The throttler keeps the most recent
// callback, so the page the user actually landed on is the one reported.
const pageViewThrottler = new Throttler(
PAGE_VIEW_THROTTLE_TIMEOUT,
PAGE_VIEW_THROTTLE_TIMEOUT,
);
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;
// Capture the page's fields now; `page` will have moved on by the time the throttler fires.
const screenName = getScreenNameFromPage(page);
const resource = getResourceFromPage(page);
pageViewThrottler.throttle(() =>
behaviourEvent?.firePageViewEvent(
screenName,
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;
Comment thread
AdityaHegde marked this conversation as resolved.
// Legacy:
event_datetime: number;
}
Expand Down
Loading