Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Telemetry: Persist sessionId across runs #22325

Merged
merged 4 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
100 changes: 100 additions & 0 deletions code/lib/telemetry/src/session-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { nanoid } from 'nanoid';
import { cache } from '@storybook/core-common';
import { resetSessionIdForTest, getSessionId, SESSION_TIMEOUT } from './session-id';

jest.mock('@storybook/core-common', () => {
const actual = jest.requireActual('@storybook/core-common');
return {
...actual,
cache: {
get: jest.fn(),
set: jest.fn(),
},
};
});
jest.mock('nanoid');

const spy = (x: any) => x as jest.SpyInstance;

describe('getSessionId', () => {
beforeEach(() => {
jest.clearAllMocks();
resetSessionIdForTest();
});

test('returns existing sessionId when cached in memory and does not fetch from disk', async () => {
const existingSessionId = 'memory-session-id';
resetSessionIdForTest(existingSessionId);

const sessionId = await getSessionId();

expect(cache.get).not.toHaveBeenCalled();
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledWith(
'session',
expect.objectContaining({ id: existingSessionId })
);
expect(sessionId).toBe(existingSessionId);
});

test('returns existing sessionId when cached on disk and not expired', async () => {
const existingSessionId = 'existing-session-id';
const existingSession = {
id: existingSessionId,
lastUsed: Date.now() - SESSION_TIMEOUT + 1000,
};

spy(cache.get).mockResolvedValueOnce(existingSession);

const sessionId = await getSessionId();

expect(cache.get).toHaveBeenCalledTimes(1);
expect(cache.get).toHaveBeenCalledWith('session');
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledWith(
'session',
expect.objectContaining({ id: existingSessionId })
);
expect(sessionId).toBe(existingSessionId);
});

test('generates new sessionId when none exists', async () => {
const newSessionId = 'new-session-id';
(nanoid as any as jest.SpyInstance).mockReturnValueOnce(newSessionId);

spy(cache.get).mockResolvedValueOnce(undefined);

const sessionId = await getSessionId();

expect(cache.get).toHaveBeenCalledTimes(1);
expect(cache.get).toHaveBeenCalledWith('session');
expect(nanoid).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledWith(
'session',
expect.objectContaining({ id: newSessionId })
);
expect(sessionId).toBe(newSessionId);
});

test('generates new sessionId when existing one is expired', async () => {
const expiredSessionId = 'expired-session-id';
const expiredSession = { id: expiredSessionId, lastUsed: Date.now() - SESSION_TIMEOUT - 1000 };
const newSessionId = 'new-session-id';
spy(nanoid).mockReturnValueOnce(newSessionId);

spy(cache.get).mockResolvedValueOnce(expiredSession);

const sessionId = await getSessionId();

expect(cache.get).toHaveBeenCalledTimes(1);
expect(cache.get).toHaveBeenCalledWith('session');
expect(nanoid).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledTimes(1);
expect(cache.set).toHaveBeenCalledWith(
'session',
expect.objectContaining({ id: newSessionId })
);
expect(sessionId).toBe(newSessionId);
});
});
29 changes: 29 additions & 0 deletions code/lib/telemetry/src/session-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { nanoid } from 'nanoid';
import { cache } from '@storybook/core-common';

export const SESSION_TIMEOUT = 1000 * 60 * 60 * 2; // 2h

interface Session {
id: string;
lastUsed: number;
}

let sessionId: string | undefined;

export const resetSessionIdForTest = (val: string | undefined = undefined) => {
sessionId = val;
};

export const getSessionId = async () => {
const now = Date.now();
if (!sessionId) {
const session: Session | undefined = await cache.get('session');
if (session && session.lastUsed >= now - SESSION_TIMEOUT) {
sessionId = session.id;
} else {
sessionId = nanoid();
}
}
await cache.set('session', { id: sessionId, lastUsed: now });
return sessionId;
};
8 changes: 3 additions & 5 deletions code/lib/telemetry/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ import { nanoid } from 'nanoid';
import type { Options, TelemetryData } from './types';
import { getAnonymousProjectId } from './anonymous-id';
import { set as saveToCache } from './event-cache';
import { getSessionId } from './session-id';

const URL = process.env.STORYBOOK_TELEMETRY_URL || 'https://storybook.js.org/event-log';

const fetch = retry(originalFetch);

let tasks: Promise<any>[] = [];

// getStorybookMetadata -> packagejson + Main.js
// event specific data: sessionId, ip, etc..
// send telemetry
const sessionId = nanoid();

export const addToGlobalContext = (key: string, value: any) => {
globalContext[key] = value;
};
Expand Down Expand Up @@ -44,6 +40,8 @@ export async function sendTelemetry(
...globalContext,
anonymousId: getAnonymousProjectId(),
};

const sessionId = await getSessionId();
const eventId = nanoid();
const body = { ...rest, eventType, eventId, sessionId, metadata, payload, context };
let request: Promise<any>;
Expand Down