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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 9.1.16

- CLI: Fix Nextjs project creation in empty directories - [#32828](https://github.com/storybookjs/storybook/pull/32828), thanks @yannbf!
- Core: Add `experimental_devServer` preset - [#32862](https://github.com/storybookjs/storybook/pull/32862), thanks @yannbf!
- Telemetry: Fix preview-first-load event - [#32859](https://github.com/storybookjs/storybook/pull/32859), thanks @shilman!

## 9.1.15

- Core: Add `preview-first-load` telemetry - [#32770](https://github.com/storybookjs/storybook/pull/32770), thanks @shilman!
Expand Down
3 changes: 3 additions & 0 deletions code/core/src/core-server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export async function storybookDevServer(options: Options) {

getMiddleware(options.configDir)(app);

// Apply experimental_devServer preset to allow addons/frameworks to extend the dev server with middlewares, etc.
await options.presets.apply('experimental_devServer', app);

const { port, host, initialPath } = options;
invariant(port, 'expected options to have a port');
const proto = options.https ? 'https' : 'http';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { makePayload } from './preview-initialized-channel';

describe('makePayload', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});

it('new user init session', () => {
const userAgent = 'Mozilla/5.0';
const sessionId = 'session-123';
const lastInit = {
timestamp: Date.now() - 3000,
body: {
sessionId,
payload: { newUser: true },
},
};

expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
{
"isNewUser": true,
"timeSinceInit": 3000,
"userAgent": "Mozilla/5.0",
}
`);
});

it('existing user init session', () => {
const userAgent = 'Mozilla/5.0';
const sessionId = 'session-123';
const lastInit = {
timestamp: Date.now() - 3000,
body: {
sessionId,
payload: {},
},
};

expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
{
"isNewUser": false,
"timeSinceInit": 3000,
"userAgent": "Mozilla/5.0",
}
`);
});

it('no init session', () => {
const userAgent = 'Mozilla/5.0';
const sessionId = 'session-123';
const lastInit = undefined;

expect(makePayload(userAgent, lastInit, sessionId)).toMatchInlineSnapshot(`
{
"isNewUser": false,
"timeSinceInit": undefined,
"userAgent": "Mozilla/5.0",
}
`);
});

it('init session with different sessionId', () => {
const userAgent = 'Mozilla/5.0';
const sessionId = 'session-123';
const lastInit = {
timestamp: Date.now() - 3000,
body: {
sessionId: 'session-456',
},
};

expect(makePayload(userAgent, lastInit as any, sessionId)).toMatchInlineSnapshot(`
{
"isNewUser": false,
"timeSinceInit": undefined,
"userAgent": "Mozilla/5.0",
}
`);
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import type { Channel } from 'storybook/internal/channels';
import { PREVIEW_INITIALIZED } from 'storybook/internal/core-events';
import { telemetry } from 'storybook/internal/telemetry';
import { type InitPayload, telemetry } from 'storybook/internal/telemetry';
import type { CoreConfig, Options } from 'storybook/internal/types';

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

export const makePayload = (
userAgent: string,
lastInit: CacheEntry | undefined,
sessionId: string
) => {
let timeSinceInit: number | undefined;
const payload = {
userAgent,
isNewUser: false,
timeSinceInit,
};

if (sessionId && lastInit?.body?.sessionId === sessionId) {
payload.timeSinceInit = Date.now() - lastInit.timestamp;
payload.isNewUser = !!(lastInit.body.payload as InitPayload).newUser;
}
return payload;
};

export function initPreviewInitializedChannel(
channel: Channel,
options: Options,
Expand All @@ -19,9 +38,8 @@ export function initPreviewInitializedChannel(
const lastInit = lastEvents.init;
const lastPreviewFirstLoad = lastEvents['preview-first-load'];
if (!lastPreviewFirstLoad) {
const isInitSession = lastInit?.body.sessionId === sessionId;
const timeSinceInit = lastInit ? Date.now() - lastInit.body.timestamp : undefined;
telemetry('preview-first-load', { timeSinceInit, isInitSession, userAgent });
const payload = makePayload(userAgent, lastInit, sessionId);
telemetry('preview-first-load', payload);
}
} catch (e) {
// do nothing
Expand Down
15 changes: 10 additions & 5 deletions code/core/src/telemetry/event-cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cache } from 'storybook/internal/common';

import type { EventType } from './types';
import type { EventType, TelemetryEvent } from './types';

interface UpgradeSummary {
timestamp: number;
Expand All @@ -9,9 +9,14 @@ interface UpgradeSummary {
sessionId?: string;
}

export interface CacheEntry {
timestamp: number;
body: TelemetryEvent;
}

let operation: Promise<any> = Promise.resolve();

const setHelper = async (eventType: EventType, body: any) => {
const setHelper = async (eventType: EventType, body: TelemetryEvent) => {
const lastEvents = (await cache.get('lastEvents')) || {};
lastEvents[eventType] = { body, timestamp: Date.now() };
await cache.set('lastEvents', lastEvents);
Expand All @@ -23,16 +28,16 @@ export const set = async (eventType: EventType, body: any) => {
return operation;
};

export const get = async (eventType: EventType) => {
export const get = async (eventType: EventType): Promise<CacheEntry | undefined> => {
const lastEvents = await getLastEvents();
return lastEvents[eventType];
};

export const getLastEvents = async () => {
export const getLastEvents = async (): Promise<Record<EventType, CacheEntry>> => {
return (await cache.get('lastEvents')) || {};
};

const upgradeFields = (event: any): UpgradeSummary => {
const upgradeFields = (event: CacheEntry): UpgradeSummary => {
const { body, timestamp } = event;
return {
timestamp,
Expand Down
10 changes: 5 additions & 5 deletions code/core/src/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,21 @@ export const telemetry = async (
telemetryData.metadata = await getStorybookMetadata(options?.configDir);
}
} catch (error: any) {
telemetryData.payload.metadataErrorMessage = sanitizeError(error).message;
payload.metadataErrorMessage = sanitizeError(error).message;

if (options?.enableCrashReports) {
telemetryData.payload.metadataError = sanitizeError(error);
payload.metadataError = sanitizeError(error);
}
} finally {
const { error } = telemetryData.payload;
const { error } = payload;
// make sure to anonymise possible paths from error messages

// make sure to anonymise possible paths from error messages
if (error) {
telemetryData.payload.error = sanitizeError(error);
payload.error = sanitizeError(error);
}

if (!telemetryData.payload.error || options?.enableCrashReports) {
if (!payload.error || options?.enableCrashReports) {
if (process.env?.STORYBOOK_TELEMETRY_DEBUG) {
logger.info('\n[telemetry]');
logger.info(JSON.stringify(telemetryData, null, 2));
Expand Down
19 changes: 18 additions & 1 deletion code/core/src/telemetry/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export type EventType =
| 'onboarding-survey'
| 'mocking'
| 'preview-first-load';

export interface Dependency {
version: string | undefined;
versionSpecifier?: string;
Expand Down Expand Up @@ -89,6 +88,10 @@ export interface Payload {
[key: string]: any;
}

export interface Context {
[key: string]: any;
}

export interface Options {
retryDelay: number;
immediate: boolean;
Expand All @@ -103,3 +106,17 @@ export interface TelemetryData {
payload: Payload;
metadata?: StorybookMetadata;
}

export interface TelemetryEvent extends TelemetryData {
eventId: string;
sessionId: string;
context: Context;
}

export interface InitPayload {
projectType: string;
features: { dev: boolean; docs: boolean; test: boolean; onboarding: boolean };
newUser: boolean;
versionSpecifier: string | undefined;
cliIntegration: string | undefined;
}
4 changes: 3 additions & 1 deletion code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export type Middleware<T extends IncomingMessage = IncomingMessage> = (
next: (err?: string | Error) => Promise<void> | void
) => Promise<void> | void;

interface ServerApp<T extends IncomingMessage = IncomingMessage> {
export interface ServerApp<T extends IncomingMessage = IncomingMessage> {
server: NetServer;

use(pattern: RegExp | string, ...handlers: Middleware<T>[]): this;
Expand Down Expand Up @@ -476,6 +476,8 @@ export interface StorybookConfigRaw {

experimental_indexers?: Indexer[];

experimental_devServer?: ServerApp;

docs?: DocsOptions;

previewHead?: string;
Expand Down
3 changes: 2 additions & 1 deletion code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "9.1.16"
}
13 changes: 10 additions & 3 deletions docs/_snippets/addon-vitest-set-project-annotations-simple.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
```tsx filename=".storybook/vitest.setup.ts" renderer="react" language="ts"
```ts filename=".storybook/vitest.setup.ts" renderer="react" language="ts"
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import { setProjectAnnotations } from '@storybook/your-framework';
import * as previewAnnotations from './preview';

const annotations = setProjectAnnotations([previewAnnotations]);
```

```tsx filename=".storybook/vitest.setup.ts" renderer="svelte" language="ts"
```ts filename=".storybook/vitest.setup.ts" renderer="svelte" language="ts"
// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite
import { setProjectAnnotations } from '@storybook/your-framework';
import * as previewAnnotations from './preview';

const annotations = setProjectAnnotations([previewAnnotations]);
```

```tsx filename=".storybook/vitest.setup.ts" renderer="vue" language="ts"
```ts filename=".storybook/vitest.setup.ts" renderer="vue" language="ts"
import { setProjectAnnotations } from '@storybook/vue3-vite';
import * as previewAnnotations from './preview';

const annotations = setProjectAnnotations([previewAnnotations]);
```

```ts filename=".storybook/vitest.setup.ts" renderer="web-components" language="ts"
import { setProjectAnnotations } from '@storybook/web-components-vite';
import * as previewAnnotations from './preview';

const annotations = setProjectAnnotations([previewAnnotations]);
```
Loading