Skip to content

Commit ebd7ff5

Browse files
yannbfstorybook-bot
authored andcommitted
Merge pull request #32859 from storybookjs/shilman/first-load-new-user
Telemetry: Fix preview-first-load event (cherry picked from commit 239794e)
1 parent da2da6e commit ebd7ff5

File tree

5 files changed

+142
-16
lines changed

5 files changed

+142
-16
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { makePayload } from './preview-initialized-channel';
4+
5+
describe('makePayload', () => {
6+
beforeEach(() => {
7+
vi.useFakeTimers();
8+
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
9+
});
10+
afterEach(() => {
11+
vi.useRealTimers();
12+
});
13+
14+
it('new user init session', () => {
15+
const userAgent = 'Mozilla/5.0';
16+
const sessionId = 'session-123';
17+
const lastInit = {
18+
timestamp: Date.now() - 3000,
19+
body: {
20+
sessionId,
21+
payload: { newUser: true },
22+
},
23+
};
24+
25+
expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
26+
{
27+
"isNewUser": true,
28+
"timeSinceInit": 3000,
29+
"userAgent": "Mozilla/5.0",
30+
}
31+
`);
32+
});
33+
34+
it('existing user init session', () => {
35+
const userAgent = 'Mozilla/5.0';
36+
const sessionId = 'session-123';
37+
const lastInit = {
38+
timestamp: Date.now() - 3000,
39+
body: {
40+
sessionId,
41+
payload: {},
42+
},
43+
};
44+
45+
expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
46+
{
47+
"isNewUser": false,
48+
"timeSinceInit": 3000,
49+
"userAgent": "Mozilla/5.0",
50+
}
51+
`);
52+
});
53+
54+
it('no init session', () => {
55+
const userAgent = 'Mozilla/5.0';
56+
const sessionId = 'session-123';
57+
const lastInit = undefined;
58+
59+
expect(makePayload(userAgent, lastInit, sessionId)).toMatchInlineSnapshot(`
60+
{
61+
"isNewUser": false,
62+
"timeSinceInit": undefined,
63+
"userAgent": "Mozilla/5.0",
64+
}
65+
`);
66+
});
67+
68+
it('init session with different sessionId', () => {
69+
const userAgent = 'Mozilla/5.0';
70+
const sessionId = 'session-123';
71+
const lastInit = {
72+
timestamp: Date.now() - 3000,
73+
body: {
74+
sessionId: 'session-456',
75+
},
76+
};
77+
78+
expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
79+
{
80+
"isNewUser": false,
81+
"timeSinceInit": undefined,
82+
"userAgent": "Mozilla/5.0",
83+
}
84+
`);
85+
});
86+
});

code/core/src/core-server/server-channel/preview-initialized-channel.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import type { Channel } from 'storybook/internal/channels';
22
import { PREVIEW_INITIALIZED } from 'storybook/internal/core-events';
3-
import { telemetry } from 'storybook/internal/telemetry';
3+
import { type InitPayload, telemetry } from 'storybook/internal/telemetry';
44
import type { CoreConfig, Options } from 'storybook/internal/types';
55

6-
import { getLastEvents } from '../../telemetry/event-cache';
6+
import { type CacheEntry, getLastEvents } from '../../telemetry/event-cache';
77
import { getSessionId } from '../../telemetry/session-id';
88

9+
export const makePayload = (
10+
userAgent: string,
11+
lastInit: CacheEntry | undefined,
12+
sessionId: string
13+
) => {
14+
let timeSinceInit: number | undefined;
15+
const payload = {
16+
userAgent,
17+
isNewUser: false,
18+
timeSinceInit,
19+
};
20+
21+
if (sessionId && lastInit?.body?.sessionId === sessionId) {
22+
payload.timeSinceInit = Date.now() - lastInit.timestamp;
23+
payload.isNewUser = !!(lastInit.body.payload as InitPayload).newUser;
24+
}
25+
return payload;
26+
};
27+
928
export function initPreviewInitializedChannel(
1029
channel: Channel,
1130
options: Options,
@@ -19,9 +38,8 @@ export function initPreviewInitializedChannel(
1938
const lastInit = lastEvents.init;
2039
const lastPreviewFirstLoad = lastEvents['preview-first-load'];
2140
if (!lastPreviewFirstLoad) {
22-
const isInitSession = lastInit?.body.sessionId === sessionId;
23-
const timeSinceInit = lastInit ? Date.now() - lastInit.body.timestamp : undefined;
24-
telemetry('preview-first-load', { timeSinceInit, isInitSession, userAgent });
41+
const payload = makePayload(userAgent, lastInit, sessionId);
42+
telemetry('preview-first-load', payload);
2543
}
2644
} catch (e) {
2745
// do nothing

code/core/src/telemetry/event-cache.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cache } from 'storybook/internal/common';
22

3-
import type { EventType } from './types';
3+
import type { EventType, TelemetryEvent } from './types';
44

55
interface UpgradeSummary {
66
timestamp: number;
@@ -9,9 +9,14 @@ interface UpgradeSummary {
99
sessionId?: string;
1010
}
1111

12+
export interface CacheEntry {
13+
timestamp: number;
14+
body: TelemetryEvent;
15+
}
16+
1217
let operation: Promise<any> = Promise.resolve();
1318

14-
const setHelper = async (eventType: EventType, body: any) => {
19+
const setHelper = async (eventType: EventType, body: TelemetryEvent) => {
1520
const lastEvents = (await cache.get('lastEvents')) || {};
1621
lastEvents[eventType] = { body, timestamp: Date.now() };
1722
await cache.set('lastEvents', lastEvents);
@@ -23,16 +28,16 @@ export const set = async (eventType: EventType, body: any) => {
2328
return operation;
2429
};
2530

26-
export const get = async (eventType: EventType) => {
31+
export const get = async (eventType: EventType): Promise<CacheEntry | undefined> => {
2732
const lastEvents = await getLastEvents();
2833
return lastEvents[eventType];
2934
};
3035

31-
export const getLastEvents = async () => {
36+
export const getLastEvents = async (): Promise<Record<EventType, CacheEntry>> => {
3237
return (await cache.get('lastEvents')) || {};
3338
};
3439

35-
const upgradeFields = (event: any): UpgradeSummary => {
40+
const upgradeFields = (event: CacheEntry): UpgradeSummary => {
3641
const { body, timestamp } = event;
3742
return {
3843
timestamp,

code/core/src/telemetry/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@ export const telemetry = async (
4343
telemetryData.metadata = await getStorybookMetadata(options?.configDir);
4444
}
4545
} catch (error: any) {
46-
telemetryData.payload.metadataErrorMessage = sanitizeError(error).message;
46+
payload.metadataErrorMessage = sanitizeError(error).message;
4747

4848
if (options?.enableCrashReports) {
49-
telemetryData.payload.metadataError = sanitizeError(error);
49+
payload.metadataError = sanitizeError(error);
5050
}
5151
} finally {
52-
const { error } = telemetryData.payload;
52+
const { error } = payload;
5353
// make sure to anonymise possible paths from error messages
5454

5555
// make sure to anonymise possible paths from error messages
5656
if (error) {
57-
telemetryData.payload.error = sanitizeError(error);
57+
payload.error = sanitizeError(error);
5858
}
5959

60-
if (!telemetryData.payload.error || options?.enableCrashReports) {
60+
if (!payload.error || options?.enableCrashReports) {
6161
if (process.env?.STORYBOOK_TELEMETRY_DEBUG) {
6262
logger.info('\n[telemetry]');
6363
logger.info(JSON.stringify(telemetryData, null, 2));

code/core/src/telemetry/types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export type EventType =
3333
| 'onboarding-survey'
3434
| 'mocking'
3535
| 'preview-first-load';
36-
3736
export interface Dependency {
3837
version: string | undefined;
3938
versionSpecifier?: string;
@@ -89,6 +88,10 @@ export interface Payload {
8988
[key: string]: any;
9089
}
9190

91+
export interface Context {
92+
[key: string]: any;
93+
}
94+
9295
export interface Options {
9396
retryDelay: number;
9497
immediate: boolean;
@@ -103,3 +106,17 @@ export interface TelemetryData {
103106
payload: Payload;
104107
metadata?: StorybookMetadata;
105108
}
109+
110+
export interface TelemetryEvent extends TelemetryData {
111+
eventId: string;
112+
sessionId: string;
113+
context: Context;
114+
}
115+
116+
export interface InitPayload {
117+
projectType: string;
118+
features: { dev: boolean; docs: boolean; test: boolean; onboarding: boolean };
119+
newUser: boolean;
120+
versionSpecifier: string | undefined;
121+
cliIntegration: string | undefined;
122+
}

0 commit comments

Comments
 (0)