-
-
Notifications
You must be signed in to change notification settings - Fork 10k
Expand file tree
/
Copy pathtelemetry.ts
More file actions
129 lines (108 loc) · 3.69 KB
/
telemetry.ts
File metadata and controls
129 lines (108 loc) · 3.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/// <reference types="node" />
import { readFileSync } from 'node:fs';
import * as os from 'node:os';
import { join } from 'node:path';
import { isCI } from 'storybook/internal/common';
import retry from 'fetch-retry';
import { nanoid } from 'nanoid';
import { version } from '../../package.json';
import { resolvePackageDir } from '../shared/utils/module';
import { getAnonymousProjectId, getProjectSince } from './anonymous-id';
import { detectAgent } from './detect-agent';
import { set as saveToCache } from './event-cache';
import { fetch } from './fetch';
import { getSessionId } from './session-id';
import type { Options, TelemetryData } from './types';
const retryingFetch = retry(fetch);
const URL = process.env.STORYBOOK_TELEMETRY_URL || 'https://storybook.js.org/event-log';
let tasks: Promise<any>[] = [];
export const addToGlobalContext = (key: string, value: any) => {
globalContext[key] = value;
};
const getOperatingSystem = (): 'Windows' | 'macOS' | 'Linux' | `Other: ${string}` | 'Unknown' => {
try {
const platform = os.platform();
if (platform === 'win32') {
return 'Windows';
}
if (platform === 'darwin') {
return 'macOS';
}
if (platform === 'linux') {
return 'Linux';
}
return `Other: ${platform}`;
} catch (_err) {
return 'Unknown';
}
};
// context info sent with all events, provided
// by the app. currently:
// - cliVersion
const inCI = isCI();
const agentDetection = detectAgent();
const globalContext = {
inCI,
isTTY: process.stdout.isTTY,
agent: agentDetection,
platform: getOperatingSystem(),
nodeVersion: process.versions.node,
storybookVersion: getVersionNumber(),
} as Record<string, any>;
const prepareRequest = async (data: TelemetryData, context: Record<string, any>, options: any) => {
const { eventType, payload, metadata, ...rest } = data;
const sessionId = await getSessionId();
const eventId = nanoid();
const body = { ...rest, eventType, eventId, sessionId, metadata, payload, context };
return retryingFetch(URL, {
method: 'post',
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
retries: 3,
retryOn: [503, 504],
retryDelay: (attempt: number) =>
2 ** attempt *
(typeof options?.retryDelay === 'number' && !Number.isNaN(options?.retryDelay)
? options.retryDelay
: 1000),
});
};
function getVersionNumber() {
try {
return JSON.parse(readFileSync(join(resolvePackageDir('storybook'), 'package.json'), 'utf8'))
.version;
} catch (e) {
return version;
}
}
export async function sendTelemetry(
data: TelemetryData,
options: Partial<Options> = { retryDelay: 1000, immediate: false }
) {
const { eventType, payload, metadata, ...rest } = data;
// We use this id so we can de-dupe events that arrive at the index multiple times due to the
// use of retries. There are situations in which the request "5xx"s (or times-out), but
// the server actually gets the request and stores it anyway.
// flatten the data before we send it
const context = options.stripMetadata
? globalContext
: {
...globalContext,
anonymousId: getAnonymousProjectId(),
projectSince: getProjectSince()?.getTime(),
};
let request: any;
try {
request = prepareRequest(data, context, options);
tasks.push(request);
const sessionId = await getSessionId();
const eventId = nanoid();
const body = { ...rest, eventType, eventId, sessionId, metadata, payload, context };
const waitFor = options.immediate ? tasks : [request];
await Promise.all([...waitFor, saveToCache(eventType, body)]);
} catch (err) {
//
} finally {
tasks = tasks.filter((task) => task !== request);
}
}