Skip to content

Commit eac8746

Browse files
authored
Merge pull request #32861 from storybookjs/version-non-patch-from-10.0.0-rc.3
Release: Prerelease 10.0.0-rc.4
2 parents 7286625 + ab713a0 commit eac8746

25 files changed

+665
-129
lines changed

CHANGELOG.prerelease.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 10.0.0-rc.4
2+
3+
- Core: Add `experimental_devServer` preset - [#32862](https://github.com/storybookjs/storybook/pull/32862), thanks @yannbf!
4+
- Core: Fix stepping back through story interactions panel - [#32793](https://github.com/storybookjs/storybook/pull/32793), thanks @ia319!
5+
- Core: Join framework preset path with slash - [#32838](https://github.com/storybookjs/storybook/pull/32838), thanks @brandonroberts!
6+
- Telemetry: Fix preview-first-load event - [#32859](https://github.com/storybookjs/storybook/pull/32859), thanks @shilman!
7+
18
## 10.0.0-rc.3
29

310
- A11y: Persist tab/highlight across docs navigation - [#32762](https://github.com/storybookjs/storybook/pull/32762), thanks @404Dealer!

code/core/src/common/utils/validate-config.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { afterEach, describe, expect, it, vi } from 'vitest';
22

3+
import { resolveModulePath } from 'exsolve';
4+
35
import { validateFrameworkName } from './validate-config';
46

7+
// mock exsolve to spy
8+
vi.mock('exsolve', { spy: true });
9+
510
describe('validateFrameworkName', () => {
611
afterEach(() => {
712
vi.resetAllMocks();
@@ -20,15 +25,20 @@ describe('validateFrameworkName', () => {
2025
});
2126

2227
it('should not throw if framework is unknown (community) but can be resolved', () => {
23-
// mock require.resolve to return a value
24-
vi.spyOn(require, 'resolve').mockReturnValue('some-community-framework');
25-
expect(() => validateFrameworkName('some-community-framework')).toThrow();
28+
vi.mocked(resolveModulePath).mockImplementation(() => {});
29+
30+
expect(() => validateFrameworkName('some-community-framework')).not.toThrow();
31+
});
32+
33+
it('should not throw if scoped framework is unknown (community) but can be resolved', () => {
34+
vi.mocked(resolveModulePath).mockImplementation(() => {});
35+
36+
expect(() => validateFrameworkName('@some-community/framework')).not.toThrow();
2637
});
2738

2839
it('should throw if framework is unknown and cannot be resolved', () => {
29-
// mock require.resolve to fail
30-
vi.spyOn(require, 'resolve').mockImplementation(() => {
31-
throw new Error('Cannot resolve');
40+
vi.mocked(resolveModulePath).mockImplementation(() => {
41+
throw new Error('cannot resolve');
3242
});
3343

3444
expect(() => validateFrameworkName('foo')).toThrow();

code/core/src/common/utils/validate-config.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { join } from 'node:path';
2-
31
import {
42
CouldNotEvaluateFrameworkError,
53
InvalidFrameworkNameError,
@@ -35,7 +33,7 @@ export function validateFrameworkName(
3533

3634
// If it's not a known framework, we need to validate that it's a valid package at least
3735
try {
38-
resolveModulePath(join(frameworkName, 'preset'), {
36+
resolveModulePath(`${frameworkName}/preset`, {
3937
extensions: ['.mjs', '.js', '.cjs'],
4038
conditions: ['node', 'import', 'require'],
4139
});

code/core/src/core-server/dev-server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export async function storybookDevServer(options: Options) {
5050

5151
(await getMiddleware(options.configDir))(app);
5252

53+
// Apply experimental_devServer preset to allow addons/frameworks to extend the dev server with middlewares, etc.
54+
await options.presets.apply('experimental_devServer', app);
55+
5356
const { port, host, initialPath } = options;
5457
invariant(port, 'expected options to have a port');
5558
const proto = options.https ? 'https' : 'http';
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/instrumenter/instrumenter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class Instrumenter {
223223
}) => {
224224
const { isDebugging } = this.getState(storyId);
225225
if (newPhase === 'preparing' && isDebugging) {
226-
return resetState({ storyId, renderPhase: newPhase });
226+
return resetState({ storyId, renderPhase: newPhase, isDebugging });
227227
} else if (newPhase === 'playing') {
228228
return resetState({ storyId, renderPhase: newPhase, isDebugging });
229229
}

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
@@ -34,7 +34,6 @@ export type EventType =
3434
| 'onboarding-survey'
3535
| 'mocking'
3636
| 'preview-first-load';
37-
3837
export interface Dependency {
3938
version: string | undefined;
4039
versionSpecifier?: string;
@@ -90,6 +89,10 @@ export interface Payload {
9089
[key: string]: any;
9190
}
9291

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

0 commit comments

Comments
 (0)