Skip to content

Commit 8127c75

Browse files
authored
[telemetry] Improve request body size, update dependencies, and optimize SSR handling (#17008)
Signed-off-by: Vadym Raksha <[email protected]>
1 parent b7b2b36 commit 8127c75

File tree

10 files changed

+215
-88
lines changed

10 files changed

+215
-88
lines changed

packages/x-telemetry/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"sideEffects": false,
2727
"packageScripts": {
28-
"postinstall": "node ./postinstall/index.js"
28+
"postinstall": "node ./esm/postinstall/index.js"
2929
},
3030
"repository": {
3131
"type": "git",
@@ -34,11 +34,11 @@
3434
},
3535
"dependencies": {
3636
"@babel/runtime": "^7.26.10",
37-
"@fingerprintjs/fingerprintjs": "^4.6.1",
37+
"@fingerprintjs/fingerprintjs": "^3.4.2",
3838
"@mui/utils": "^7.0.0 || ^7.0.0-beta",
3939
"ci-info": "^4.2.0",
40-
"conf": "^5.0.0",
41-
"is-docker": "^2.2.1",
40+
"conf": "^11.0.0",
41+
"is-docker": "^3.0.0",
4242
"node-machine-id": "^1.1.12"
4343
},
4444
"devDependencies": {

packages/x-telemetry/src/context.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export interface TelemetryContextType {
66
isInitialized: boolean;
77
};
88
traits: Record<string, any> & {
9+
fingerprint?: {
10+
fullHash?: string | null;
11+
coreHash?: string | null;
12+
components?: Record<string, any> | null;
13+
} | null;
914
machineId?: string | null;
1015
projectId?: string | null;
1116
sessionId?: string | null;

packages/x-telemetry/src/postinstall/get-machine-id.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { machineId } from 'node-machine-id';
1+
import nodeMachineId from 'node-machine-id';
22
import { createHash } from 'crypto';
33

44
async function getRawMachineId(): Promise<string | null> {
55
try {
6-
return await machineId(true);
6+
return await nodeMachineId.machineId(true);
77
} catch (_) {
88
return null;
99
}

packages/x-telemetry/src/postinstall/index.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { randomBytes } from 'crypto';
4+
import { fileURLToPath } from 'url';
45
import type { TelemetryContextType } from '../context';
56
import getEnvironmentInfo from './get-environment-info';
67
import getAnonymousProjectId from './get-project-id';
78
import getAnonymousMachineId from './get-machine-id';
89
import { TelemetryStorage } from './storage';
910

11+
const dirname =
12+
typeof __dirname === 'string'
13+
? __dirname // cjs build in root dir
14+
: (() => {
15+
const filename = fileURLToPath(import.meta.url);
16+
17+
// esm build in `esm` directory, so we need to go up two levels
18+
return path.dirname(path.dirname(filename));
19+
})();
20+
1021
(async () => {
1122
// If Node.js support permissions, we need to check if the current user has
1223
// the necessary permissions to write to the file system.
@@ -17,8 +28,8 @@ import { TelemetryStorage } from './storage';
1728
return;
1829
}
1930

20-
const storage = new TelemetryStorage({
21-
distDir: path.join(process.cwd()),
31+
const storage = TelemetryStorage.init({
32+
distDir: process.cwd(),
2233
});
2334

2435
const [environmentInfo, projectId, machineId] = await Promise.all([
@@ -41,7 +52,7 @@ import { TelemetryStorage } from './storage';
4152
};
4253

4354
const writeContextData = (filePath: string, format: (content: string) => string) => {
44-
const targetPath = path.resolve(__dirname, '..', filePath, 'context.js');
55+
const targetPath = path.resolve(dirname, '..', filePath, 'context.js');
4556
fs.writeFileSync(targetPath, format(JSON.stringify(contextData, null, 2)));
4657
};
4758

packages/x-telemetry/src/postinstall/storage.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { randomBytes } from 'crypto';
2-
import path from 'path';
32
import Conf from 'conf';
4-
import isDockerFunction from 'is-docker';
5-
import ciEnvironment from 'ci-info';
3+
import path from 'path';
64
import notifyAboutMuiXTelemetry from './notify';
5+
import getEnvironmentInfo from './get-environment-info';
76

87
// This is the key that specifies when the user was informed about telemetry collection.
98
const TELEMETRY_KEY_NOTIFY_DATE = 'telemetry.notifiedAt';
@@ -13,7 +12,8 @@ const TELEMETRY_KEY_NOTIFY_DATE = 'telemetry.notifiedAt';
1312
const TELEMETRY_KEY_ID = `telemetry.anonymousId`;
1413

1514
function getStorageDirectory(distDir: string): string | undefined {
16-
const isLikelyEphemeral = ciEnvironment.isCI || isDockerFunction();
15+
const env = getEnvironmentInfo();
16+
const isLikelyEphemeral = env.isCI || env.isDocker;
1717

1818
if (isLikelyEphemeral) {
1919
return path.join(distDir, 'cache');
@@ -23,19 +23,22 @@ function getStorageDirectory(distDir: string): string | undefined {
2323
}
2424

2525
export class TelemetryStorage {
26-
private readonly conf: Conf<any> | null;
27-
28-
constructor({ distDir }: { distDir: string }) {
26+
public static init({ distDir }: { distDir: string }) {
2927
const storageDirectory = getStorageDirectory(distDir);
30-
28+
let conf: Conf<any> | null = null;
3129
try {
3230
// `conf` incorrectly throws a permission error during initialization
3331
// instead of waiting for first use. We need to handle it, otherwise the
3432
// process may crash.
35-
this.conf = new Conf({ projectName: 'mui-x', cwd: storageDirectory });
33+
conf = new Conf({ projectName: 'mui-x', cwd: storageDirectory });
3634
} catch (_) {
37-
this.conf = null;
35+
conf = null;
3836
}
37+
38+
return new TelemetryStorage(conf);
39+
}
40+
41+
private constructor(private readonly conf: Conf<any> | null) {
3942
this.notify();
4043
}
4144

packages/x-telemetry/src/runtime/get-context.ts

+81-38
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import telemetryContext from '../context';
22
import type { TelemetryContextType } from '../context';
3-
import {
4-
getWindowStorageItem,
5-
setWindowStorageItem,
6-
isWindowStorageAvailable,
7-
} from './window-storage';
3+
import { getWindowStorageItem, setWindowStorageItem } from './window-storage';
84

95
function generateId(length: number): string {
106
let result = '';
@@ -18,62 +14,109 @@ function generateId(length: number): string {
1814
return result;
1915
}
2016

21-
const getMachineId =
17+
function pick(obj: any, keys: string[]) {
18+
return keys.reduce((acc, key) => {
19+
acc[key] = obj[key];
20+
return acc;
21+
}, {} as any);
22+
}
23+
24+
const getBrowserFingerprint =
2225
typeof window === 'undefined' || process.env.NODE_ENV === 'test'
23-
? () => ''
26+
? () => undefined
2427
: async () => {
25-
const FingerprintJS = await import('@fingerprintjs/fingerprintjs');
26-
const fpPromise = FingerprintJS.load();
27-
const fp = await fpPromise;
28-
const result = await fp.get();
29-
return result.visitorId;
28+
const fingerprintLCKey = 'fingerprint';
29+
30+
try {
31+
const existingFingerprint = getWindowStorageItem('localStorage', fingerprintLCKey);
32+
if (existingFingerprint) {
33+
return JSON.parse(existingFingerprint);
34+
}
35+
36+
const FingerprintJS = await import('@fingerprintjs/fingerprintjs');
37+
const fp = await FingerprintJS.load({ monitoring: false } as any);
38+
const fpResult = await fp.get();
39+
40+
const components: any = { ...fpResult.components };
41+
delete components.cookiesEnabled;
42+
43+
const fullHash = FingerprintJS.hashComponents(components);
44+
const coreHash = FingerprintJS.hashComponents({
45+
...pick(components, [
46+
'fonts',
47+
'audio',
48+
'languages',
49+
'deviceMemory',
50+
'timezone',
51+
'sessionStorage',
52+
'localStorage',
53+
'indexedDB',
54+
'openDatabase',
55+
'platform',
56+
'canvas',
57+
'vendor',
58+
'vendorFlavors',
59+
'colorGamut',
60+
'forcedColors',
61+
'monochrome',
62+
'contrast',
63+
'reducedMotion',
64+
'math',
65+
'videoCard',
66+
'architecture',
67+
]),
68+
});
69+
70+
const result = { fullHash, coreHash };
71+
setWindowStorageItem('localStorage', fingerprintLCKey, JSON.stringify(result));
72+
return result;
73+
} catch (_) {
74+
return null;
75+
}
3076
};
3177

3278
function getAnonymousId(): string {
33-
if (isWindowStorageAvailable('localStorage')) {
34-
const localStorageKey = 'anonymous_id';
35-
const existingAnonymousId = getWindowStorageItem('localStorage', localStorageKey);
36-
if (existingAnonymousId) {
37-
return existingAnonymousId;
38-
}
39-
40-
const generated = generateId(32);
41-
if (setWindowStorageItem('localStorage', localStorageKey, generated)) {
42-
return generated;
43-
}
79+
const localStorageKey = 'anonymous_id';
80+
const existingAnonymousId = getWindowStorageItem('localStorage', localStorageKey);
81+
if (existingAnonymousId) {
82+
return existingAnonymousId;
83+
}
84+
85+
const generated = `anid_${generateId(32)}`;
86+
if (setWindowStorageItem('localStorage', localStorageKey, generated)) {
87+
return generated;
4488
}
4589

4690
return '';
4791
}
4892

4993
function getSessionId(): string {
50-
if (isWindowStorageAvailable('sessionStorage')) {
51-
const localStorageKey = 'session_id';
52-
const existingSessionId = getWindowStorageItem('sessionStorage', localStorageKey);
53-
if (existingSessionId) {
54-
return existingSessionId;
55-
}
56-
57-
const generated = generateId(32);
58-
if (setWindowStorageItem('sessionStorage', localStorageKey, generated)) {
59-
return generated;
60-
}
94+
const localStorageKey = 'session_id';
95+
const existingSessionId = getWindowStorageItem('sessionStorage', localStorageKey);
96+
if (existingSessionId) {
97+
return existingSessionId;
6198
}
6299

63-
return generateId(32);
100+
const generated = `sesid_${generateId(32)}`;
101+
if (setWindowStorageItem('sessionStorage', localStorageKey, generated)) {
102+
return generated;
103+
}
104+
105+
return `sestp_${generateId(32)}`;
64106
}
65107

66108
async function getTelemetryContext(): Promise<TelemetryContextType> {
109+
telemetryContext.traits.sessionId = getSessionId();
110+
67111
// Initialize the context if it hasn't been initialized yet
68112
// (e.g. postinstall not run)
69113
if (!telemetryContext.config.isInitialized) {
70114
telemetryContext.traits.anonymousId = getAnonymousId();
71-
telemetryContext.traits.sessionId = getSessionId();
72115
telemetryContext.config.isInitialized = true;
73116
}
74117

75-
if (!telemetryContext.traits.machineId) {
76-
telemetryContext.traits.machineId = await getMachineId();
118+
if (!telemetryContext.traits.fingerprint) {
119+
telemetryContext.traits.fingerprint = await getBrowserFingerprint();
77120
}
78121

79122
return telemetryContext;

packages/x-telemetry/src/runtime/sender.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { getTelemetryEnvConfigValue } from './config';
33
import { TelemetryEvent } from '../types';
44
import { fetchWithRetry } from './fetcher';
55

6+
const sendMuiXTelemetryRetries = 3;
7+
68
function shouldSendTelemetry(telemetryContext: TelemetryContextType): boolean {
9+
// Disable reporting in SSR / Node.js
10+
if (typeof window === 'undefined') {
11+
return false;
12+
}
13+
714
// Priority to the config (e.g. in code, env)
815
const envIsCollecting = getTelemetryEnvConfigValue('IS_COLLECTING');
916
if (typeof envIsCollecting === 'boolean') {
@@ -20,8 +27,6 @@ function shouldSendTelemetry(telemetryContext: TelemetryContextType): boolean {
2027
return false;
2128
}
2229

23-
const sendMuiXTelemetryRetries = 3;
24-
2530
async function sendMuiXTelemetryEvent(event: TelemetryEvent | null) {
2631
try {
2732
// Disable collection of the telemetry
@@ -51,13 +56,13 @@ async function sendMuiXTelemetryEvent(event: TelemetryEvent | null) {
5156

5257
// TODO: batch events and send them in a single request when there will be more
5358
await fetchWithRetry(
54-
'https://x-telemetry.mui.com/api/v1/telemetry/record',
59+
'https://x-telemetry.mui.com/v2/telemetry/record',
5560
{
5661
method: 'POST',
5762
headers: {
5863
'Content-Type': 'application/json',
5964
'X-Telemetry-Client-Version': process.env.MUI_VERSION ?? '<dev>',
60-
'X-Telemetry-Node-Env': (process.env.NODE_ENV as any) ?? '<unknown>',
65+
'X-Telemetry-Node-Env': process.env.NODE_ENV ?? '<unknown>',
6166
},
6267
body: JSON.stringify([eventPayload]),
6368
},

packages/x-telemetry/src/runtime/window-storage.ts

-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,3 @@ export function getWindowStorageItem(type: WindowStorageType, key: string): stri
3030

3131
return null;
3232
}
33-
34-
export function isWindowStorageAvailable(type: WindowStorageType): boolean {
35-
return typeof window !== 'undefined' && !!window[type];
36-
}

0 commit comments

Comments
 (0)