From 28ba84539b41fa9d9ff6b6a361b39bb65839e510 Mon Sep 17 00:00:00 2001 From: Ben Limmer Date: Wed, 27 May 2026 14:13:27 -0600 Subject: [PATCH] refactor: identify report analytics by the generating machine's anon id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report's PostHog distinct id was the per-run reportId, so a run's CLI events (keyed by the machine anon id) and its report views never resolved to the same person. That broke person-based funnels across the CLI→report boundary and let internal report activity slip past the anon-id cohort. Use the embedded generatedByAnonId as the report's distinct id so both surfaces share one person. pw_report_id stays on every event as a property for per-run slicing; the now-redundant pw_generated_by property is dropped. Not backward compatible: older reportId-keyed report persons won't merge, which is fine going forward. --- src/report/reportAnalyticsConfig.ts | 7 ++++--- src/report/web/analytics/postHogAnalytics.test.ts | 10 ++++++++-- src/report/web/analytics/postHogAnalytics.ts | 11 +++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/report/reportAnalyticsConfig.ts b/src/report/reportAnalyticsConfig.ts index a9edb5b..e9f5f10 100644 --- a/src/report/reportAnalyticsConfig.ts +++ b/src/report/reportAnalyticsConfig.ts @@ -4,10 +4,11 @@ export interface ReportAnalyticsConfig { // Mirrors the CLI's telemetry opt-out at generation time. When true the frontend never // initializes PostHog. The viewer's own browser opt-out is checked separately at runtime. readonly telemetryDisabled: boolean; - // Per-run id; the frontend uses it as the PostHog distinct id so every view of one report - // groups together and joins back to the CLI run that produced it. + // Per-run id, sent as the pw_report_id property (not the identity) so report views join back to + // the CLI run that produced them. readonly reportId: string; - // Anonymous id of the machine that generated the report (empty when telemetry was disabled). + // Anon id of the generating machine (empty when telemetry was disabled). Used as the PostHog + // distinct id so the report shares one person with its CLI run. readonly generatedByAnonId: string; readonly version: string; } diff --git a/src/report/web/analytics/postHogAnalytics.test.ts b/src/report/web/analytics/postHogAnalytics.test.ts index 3c90c41..ef5c1cd 100644 --- a/src/report/web/analytics/postHogAnalytics.test.ts +++ b/src/report/web/analytics/postHogAnalytics.test.ts @@ -50,11 +50,18 @@ describe('createPostHogReportAnalytics', () => { maskAllInputs: true, blockSelector: 'script[type="application/json"]', }, - bootstrap: { distinctID: 'report-123' }, + bootstrap: { distinctID: 'anon-456' }, }); expect(fake.init[0]?.config['before_send']).toBeDefined(); }); + test('falls back to the report id as the distinct id when no anon id was embedded', () => { + const fake = createFakeClient(); + createPostHogReportAnalytics(postHogReportAnalyticsOptions.build({ client: fake.client, generatedByAnonId: '' })); + + expect(fake.init[0]?.config['bootstrap']).toEqual({ distinctID: 'report-123' }); + }); + test('registers the pw_* super-properties', () => { const fake = createFakeClient(); createPostHogReportAnalytics(postHogReportAnalyticsOptions.build({ client: fake.client })); @@ -63,7 +70,6 @@ describe('createPostHogReportAnalytics', () => { pw_surface: 'report', pw_version: '1.2.3', pw_report_id: 'report-123', - pw_generated_by: 'anon-456', }); }); diff --git a/src/report/web/analytics/postHogAnalytics.ts b/src/report/web/analytics/postHogAnalytics.ts index 7176640..0ae11ae 100644 --- a/src/report/web/analytics/postHogAnalytics.ts +++ b/src/report/web/analytics/postHogAnalytics.ts @@ -23,19 +23,22 @@ export interface CreatePostHogReportAnalyticsOptions { export function createPostHogReportAnalytics(options: CreatePostHogReportAnalyticsOptions): Analytics { const { buildInfo, reportId, generatedByAnonId, version, client = posthog } = options; + // Identify by the generating machine's anon id (the CLI's distinct id too) so a run's CLI events + // and report views are one PostHog person. Fall back to the report id for reports with no anon id. + const distinctId = generatedByAnonId || reportId; + const superProperties: Record = { pw_surface: 'report', pw_version: version, pw_report_id: reportId, - pw_generated_by: generatedByAnonId, }; client.init(buildInfo.postHogKey, { api_host: buildInfo.postHogHost, - // The report runs from a file:// page with no reliable storage, so keep identity in memory - // and seed the distinct id from the report id rather than persisting one per viewer. + // The report runs from a file:// page with no reliable storage, so keep identity in memory and + // seed the distinct id via bootstrap rather than persisting one per viewer. persistence: 'memory', - bootstrap: { distinctID: reportId }, + bootstrap: { distinctID: distinctId }, // Report copy lives in clickable elements; autocapture would leak it via $elements_text. autocapture: false, capture_pageview: true,