@mui/x-telemetry
Advanced tools
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { | ||
| value: true | ||
| }); | ||
| exports.sha256 = sha256; | ||
| var _crypto = require("crypto"); | ||
| function sha256(value) { | ||
| return (0, _crypto.createHash)('sha256').update(value).digest('hex'); | ||
| } |
| import { createHash } from 'crypto'; | ||
| export function sha256(value) { | ||
| return createHash('sha256').update(value).digest('hex'); | ||
| } |
| export declare function hashString(str: string): Promise<string>; |
| export declare function hashString(str: string): Promise<string>; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { | ||
| value: true | ||
| }); | ||
| exports.hashString = hashString; | ||
| async function hashString(str) { | ||
| const data = new TextEncoder().encode(str); | ||
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); | ||
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | ||
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | ||
| } |
| export async function hashString(str) { | ||
| const data = new TextEncoder().encode(str); | ||
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); | ||
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | ||
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | ||
| } |
+6
-0
| export interface TelemetryContextType { | ||
| config: { | ||
| isInitialized: boolean; | ||
| /** Tracks whether we've already attempted runtime projectId resolution (avoids repeated fetch) */ | ||
| runtimePackageNameHashResolved?: boolean; | ||
| }; | ||
@@ -12,2 +14,6 @@ traits: Record<string, any> & { | ||
| } | null; | ||
| repoHash?: string | null; | ||
| postinstallPackageNameHash?: string | null; | ||
| runtimePackageNameHash?: string | null; | ||
| rootPathHash?: string | null; | ||
| projectId?: string | null; | ||
@@ -14,0 +20,0 @@ anonymousId?: string | null; |
+6
-0
| export interface TelemetryContextType { | ||
| config: { | ||
| isInitialized: boolean; | ||
| /** Tracks whether we've already attempted runtime projectId resolution (avoids repeated fetch) */ | ||
| runtimePackageNameHashResolved?: boolean; | ||
| }; | ||
@@ -12,2 +14,6 @@ traits: Record<string, any> & { | ||
| } | null; | ||
| repoHash?: string | null; | ||
| postinstallPackageNameHash?: string | null; | ||
| runtimePackageNameHash?: string | null; | ||
| rootPathHash?: string | null; | ||
| projectId?: string | null; | ||
@@ -14,0 +20,0 @@ anonymousId?: string | null; |
+1
-1
| /** | ||
| * @mui/x-telemetry v9.0.0-alpha.2 | ||
| * @mui/x-telemetry v9.0.0-rc.0 | ||
| * | ||
@@ -4,0 +4,0 @@ * @license SEE LICENSE IN LICENSE |
+1
-1
| /** | ||
| * @mui/x-telemetry v9.0.0-alpha.2 | ||
| * @mui/x-telemetry v9.0.0-rc.0 | ||
| * | ||
@@ -4,0 +4,0 @@ * @license SEE LICENSE IN LICENSE |
+2
-2
| { | ||
| "name": "@mui/x-telemetry", | ||
| "version": "9.0.0-alpha.2", | ||
| "version": "9.0.0-rc.0", | ||
| "author": "MUI Team", | ||
@@ -23,3 +23,3 @@ "description": "MUI X Telemetry.", | ||
| "@fingerprintjs/fingerprintjs": "^3.4.2", | ||
| "ci-info": "^4.3.1", | ||
| "ci-info": "^4.4.0", | ||
| "is-docker": "^4.0.0", | ||
@@ -26,0 +26,0 @@ "node-machine-id": "^1.1.12" |
@@ -9,15 +9,71 @@ "use strict"; | ||
| var _interopRequireWildcard2 = _interopRequireDefault(require("@babel/runtime/helpers/interopRequireWildcard")); | ||
| var _crypto = require("crypto"); | ||
| var _fs = _interopRequireDefault(require("fs")); | ||
| var _isDocker = _interopRequireDefault(require("is-docker")); | ||
| var _os = _interopRequireDefault(require("os")); | ||
| var _hash = require("./hash"); | ||
| // Q: Why does MUI need a machine ID? | ||
| // A: | ||
| // MUI's telemetry uses a hashed machine ID to approximate unique developer counts. | ||
| // Without a stable machine ID, every session in containers or CI creates a new | ||
| // persona, inflating seat counts. | ||
| // | ||
| // Fallback chain: | ||
| // 1. node-machine-id — most reliable, uses platform-specific APIs | ||
| // (IOPlatformUUID on macOS, MachineGuid on Windows, /var/lib/dbus/machine-id on Linux) | ||
| // 2. /etc/machine-id — present in most Linux distros and containers, | ||
| // stable across container restarts (only changes on image rebuild) | ||
| // 3. /var/lib/dbus/machine-id — alternative Linux path, some distros | ||
| // use this instead of /etc/machine-id | ||
| // 4. os.hostname() — used on macOS/Windows where hostnames are user-set and stable, | ||
| // and in Docker where the hostname is the container ID (random but unique per container, | ||
| // stable for the container's lifetime). Skipped on non-Docker Linux because hostnames | ||
| // are often generic defaults (e.g., "localhost", "ubuntu") which would conflate | ||
| // different machines into the same identity. | ||
| async function getAnonymousMachineId() { | ||
| // 1. Try node-machine-id (platform-specific, most reliable) | ||
| try { | ||
| const nodeMachineId = await Promise.resolve().then(() => (0, _interopRequireWildcard2.default)(require('node-machine-id'))); | ||
| const rawMachineId = await nodeMachineId.machineId(true); | ||
| if (!rawMachineId) { | ||
| return null; | ||
| const id = await nodeMachineId.machineId(true); | ||
| if (id) { | ||
| return (0, _hash.sha256)(id); | ||
| } | ||
| return (0, _crypto.createHash)('sha256').update(rawMachineId).digest('hex'); | ||
| } catch (_) { | ||
| // Ignore any errors | ||
| return null; | ||
| } catch { | ||
| // Not available (e.g., sandboxed env, missing native deps) | ||
| } | ||
| // 2. Try /etc/machine-id (Linux, most containers) | ||
| try { | ||
| const id = _fs.default.readFileSync('/etc/machine-id', 'utf-8').trim(); | ||
| if (id) { | ||
| return (0, _hash.sha256)(id); | ||
| } | ||
| } catch { | ||
| // File doesn't exist (macOS, Windows, or minimal container image) | ||
| } | ||
| // 3. Try /var/lib/dbus/machine-id (alternative Linux path) | ||
| try { | ||
| const id = _fs.default.readFileSync('/var/lib/dbus/machine-id', 'utf-8').trim(); | ||
| if (id) { | ||
| return (0, _hash.sha256)(id); | ||
| } | ||
| } catch { | ||
| // File doesn't exist | ||
| } | ||
| // 4. Try hostname — only on macOS/Windows where it's user-set and stable. | ||
| // On Linux, container runtimes assign random hostnames per run, | ||
| // which would generate a different hash each session and inflate counts. | ||
| if (process.platform !== 'linux' || (0, _isDocker.default)()) { | ||
| try { | ||
| const hostname = _os.default.hostname(); | ||
| if (hostname) { | ||
| return (0, _hash.sha256)(hostname); | ||
| } | ||
| } catch { | ||
| // os.hostname() failed | ||
| } | ||
| } | ||
| return null; | ||
| } |
@@ -1,14 +0,71 @@ | ||
| import { createHash } from 'crypto'; | ||
| import fs from 'fs'; | ||
| import isDocker from 'is-docker'; | ||
| import os from 'os'; | ||
| import { sha256 } from "./hash.mjs"; | ||
| // Q: Why does MUI need a machine ID? | ||
| // A: | ||
| // MUI's telemetry uses a hashed machine ID to approximate unique developer counts. | ||
| // Without a stable machine ID, every session in containers or CI creates a new | ||
| // persona, inflating seat counts. | ||
| // | ||
| // Fallback chain: | ||
| // 1. node-machine-id — most reliable, uses platform-specific APIs | ||
| // (IOPlatformUUID on macOS, MachineGuid on Windows, /var/lib/dbus/machine-id on Linux) | ||
| // 2. /etc/machine-id — present in most Linux distros and containers, | ||
| // stable across container restarts (only changes on image rebuild) | ||
| // 3. /var/lib/dbus/machine-id — alternative Linux path, some distros | ||
| // use this instead of /etc/machine-id | ||
| // 4. os.hostname() — used on macOS/Windows where hostnames are user-set and stable, | ||
| // and in Docker where the hostname is the container ID (random but unique per container, | ||
| // stable for the container's lifetime). Skipped on non-Docker Linux because hostnames | ||
| // are often generic defaults (e.g., "localhost", "ubuntu") which would conflate | ||
| // different machines into the same identity. | ||
| export default async function getAnonymousMachineId() { | ||
| // 1. Try node-machine-id (platform-specific, most reliable) | ||
| try { | ||
| const nodeMachineId = await import('node-machine-id'); | ||
| const rawMachineId = await nodeMachineId.machineId(true); | ||
| if (!rawMachineId) { | ||
| return null; | ||
| const id = await nodeMachineId.machineId(true); | ||
| if (id) { | ||
| return sha256(id); | ||
| } | ||
| return createHash('sha256').update(rawMachineId).digest('hex'); | ||
| } catch (_) { | ||
| // Ignore any errors | ||
| return null; | ||
| } catch { | ||
| // Not available (e.g., sandboxed env, missing native deps) | ||
| } | ||
| // 2. Try /etc/machine-id (Linux, most containers) | ||
| try { | ||
| const id = fs.readFileSync('/etc/machine-id', 'utf-8').trim(); | ||
| if (id) { | ||
| return sha256(id); | ||
| } | ||
| } catch { | ||
| // File doesn't exist (macOS, Windows, or minimal container image) | ||
| } | ||
| // 3. Try /var/lib/dbus/machine-id (alternative Linux path) | ||
| try { | ||
| const id = fs.readFileSync('/var/lib/dbus/machine-id', 'utf-8').trim(); | ||
| if (id) { | ||
| return sha256(id); | ||
| } | ||
| } catch { | ||
| // File doesn't exist | ||
| } | ||
| // 4. Try hostname — only on macOS/Windows where it's user-set and stable. | ||
| // On Linux, container runtimes assign random hostnames per run, | ||
| // which would generate a different hash each session and inflate counts. | ||
| if (process.platform !== 'linux' || isDocker()) { | ||
| try { | ||
| const hostname = os.hostname(); | ||
| if (hostname) { | ||
| return sha256(hostname); | ||
| } | ||
| } catch { | ||
| // os.hostname() failed | ||
| } | ||
| } | ||
| return null; | ||
| } |
@@ -7,6 +7,11 @@ "use strict"; | ||
| }); | ||
| exports.default = getAnonymousProjectId; | ||
| exports.getAnonymousPackageNameHash = getAnonymousPackageNameHash; | ||
| exports.getAnonymousRepoHash = getAnonymousRepoHash; | ||
| exports.getAnonymousRootPathHash = getAnonymousRootPathHash; | ||
| exports.getPackageName = getPackageName; | ||
| var _child_process = require("child_process"); | ||
| var _crypto = require("crypto"); | ||
| var _fs = _interopRequireDefault(require("fs")); | ||
| var _path = _interopRequireDefault(require("path")); | ||
| var _util = _interopRequireDefault(require("util")); | ||
| var _hash = require("./hash"); | ||
| const asyncExec = _util.default.promisify(_child_process.exec); | ||
@@ -19,3 +24,3 @@ async function execCLI(command) { | ||
| }); | ||
| return String(response).trim(); | ||
| return String(response.stdout).trim(); | ||
| } catch (_) { | ||
@@ -25,16 +30,47 @@ return null; | ||
| } | ||
| function getPackageName() { | ||
| const cwd = process.cwd(); | ||
| const segments = cwd.split(_path.default.sep); | ||
| for (let i = segments.length; i > 0; i -= 1) { | ||
| const dir = segments.slice(0, i).join(_path.default.sep) || _path.default.sep; | ||
| try { | ||
| const content = _fs.default.readFileSync(_path.default.join(dir, 'package.json'), 'utf-8'); | ||
| const pkg = JSON.parse(content); | ||
| if (pkg.name && typeof pkg.name === 'string') { | ||
| return pkg.name; | ||
| } | ||
| } catch (_) { | ||
| // No package.json at this level, continue walking up | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| // Q: Why does MUI need a project ID? Why is it looking at my git remote? | ||
| // Q: Why does MUI send multiple project identifiers? | ||
| // A: | ||
| // MUI's telemetry always anonymizes these values. We need a way to | ||
| // differentiate different projects to track feature usage accurately. | ||
| // For example, to prevent a feature from appearing to be constantly `used` | ||
| // and then `unused` when switching between local projects. | ||
| // MUI's telemetry always anonymizes these values. We send three separate | ||
| // signals (repoHash, postinstallPackageNameHash, rootPathHash) plus a computed projectId | ||
| // so we can handle monorepos (same repo, different apps) and micro-frontends | ||
| // (different repos, same app) correctly on the backend. | ||
| async function getRawProjectId() { | ||
| return (await execCLI(`git config --local --get remote.origin.url`)) || process.env.REPOSITORY_URL || (await execCLI(`git rev-parse --show-toplevel`)) || process.cwd(); | ||
| // Raw repo identifier: git remote URL (hashed later into repoHash) | ||
| async function getRawRepoId() { | ||
| return (await execCLI(`git config --local --get remote.upstream.url`)) || (await execCLI(`git config --local --get remote.origin.url`)) || process.env.REPOSITORY_URL || null; | ||
| } | ||
| async function getAnonymousProjectId() { | ||
| const rawProjectId = await getRawProjectId(); | ||
| return (0, _crypto.createHash)('sha256').update(rawProjectId).digest('hex'); | ||
| // Raw root path: git root or cwd (hashed later into rootPathHash, unique per developer) | ||
| async function getRawRootPathId() { | ||
| return (await execCLI(`git rev-parse --show-toplevel`)) || process.cwd(); | ||
| } | ||
| async function getAnonymousRepoHash() { | ||
| const raw = await getRawRepoId(); | ||
| return raw ? (0, _hash.sha256)(raw) : null; | ||
| } | ||
| async function getAnonymousPackageNameHash() { | ||
| const raw = getPackageName(); | ||
| return raw ? (0, _hash.sha256)(raw) : null; | ||
| } | ||
| async function getAnonymousRootPathHash() { | ||
| const raw = await getRawRootPathId(); | ||
| return (0, _hash.sha256)(raw); | ||
| } |
| import { exec } from 'child_process'; | ||
| import { createHash } from 'crypto'; | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import util from 'util'; | ||
| import { sha256 } from "./hash.mjs"; | ||
| const asyncExec = util.promisify(exec); | ||
@@ -11,3 +13,3 @@ async function execCLI(command) { | ||
| }); | ||
| return String(response).trim(); | ||
| return String(response.stdout).trim(); | ||
| } catch (_) { | ||
@@ -17,16 +19,47 @@ return null; | ||
| } | ||
| export function getPackageName() { | ||
| const cwd = process.cwd(); | ||
| const segments = cwd.split(path.sep); | ||
| for (let i = segments.length; i > 0; i -= 1) { | ||
| const dir = segments.slice(0, i).join(path.sep) || path.sep; | ||
| try { | ||
| const content = fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'); | ||
| const pkg = JSON.parse(content); | ||
| if (pkg.name && typeof pkg.name === 'string') { | ||
| return pkg.name; | ||
| } | ||
| } catch (_) { | ||
| // No package.json at this level, continue walking up | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| // Q: Why does MUI need a project ID? Why is it looking at my git remote? | ||
| // Q: Why does MUI send multiple project identifiers? | ||
| // A: | ||
| // MUI's telemetry always anonymizes these values. We need a way to | ||
| // differentiate different projects to track feature usage accurately. | ||
| // For example, to prevent a feature from appearing to be constantly `used` | ||
| // and then `unused` when switching between local projects. | ||
| // MUI's telemetry always anonymizes these values. We send three separate | ||
| // signals (repoHash, postinstallPackageNameHash, rootPathHash) plus a computed projectId | ||
| // so we can handle monorepos (same repo, different apps) and micro-frontends | ||
| // (different repos, same app) correctly on the backend. | ||
| async function getRawProjectId() { | ||
| return (await execCLI(`git config --local --get remote.origin.url`)) || process.env.REPOSITORY_URL || (await execCLI(`git rev-parse --show-toplevel`)) || process.cwd(); | ||
| // Raw repo identifier: git remote URL (hashed later into repoHash) | ||
| async function getRawRepoId() { | ||
| return (await execCLI(`git config --local --get remote.upstream.url`)) || (await execCLI(`git config --local --get remote.origin.url`)) || process.env.REPOSITORY_URL || null; | ||
| } | ||
| export default async function getAnonymousProjectId() { | ||
| const rawProjectId = await getRawProjectId(); | ||
| return createHash('sha256').update(rawProjectId).digest('hex'); | ||
| // Raw root path: git root or cwd (hashed later into rootPathHash, unique per developer) | ||
| async function getRawRootPathId() { | ||
| return (await execCLI(`git rev-parse --show-toplevel`)) || process.cwd(); | ||
| } | ||
| export async function getAnonymousRepoHash() { | ||
| const raw = await getRawRepoId(); | ||
| return raw ? sha256(raw) : null; | ||
| } | ||
| export async function getAnonymousPackageNameHash() { | ||
| const raw = getPackageName(); | ||
| return raw ? sha256(raw) : null; | ||
| } | ||
| export async function getAnonymousRootPathHash() { | ||
| const raw = await getRawRootPathId(); | ||
| return sha256(raw); | ||
| } |
+20
-15
@@ -10,12 +10,9 @@ "use strict"; | ||
| var _getEnvironmentInfo = _interopRequireDefault(require("./get-environment-info")); | ||
| var _getProjectId = _interopRequireDefault(require("./get-project-id")); | ||
| var _getProjectId = require("./get-project-id"); | ||
| var _getMachineId = _interopRequireDefault(require("./get-machine-id")); | ||
| var _storage = require("./storage"); | ||
| const dirname = typeof __dirname === 'string' ? __dirname // cjs build in root dir | ||
| : (() => { | ||
| const filename = (0, _url.fileURLToPath)(require('url').pathToFileURL(__filename).toString()); | ||
| // esm build in `esm` directory, so we need to go up two levels | ||
| return _path.default.dirname(_path.default.dirname(filename)); | ||
| })(); | ||
| // It's a flat build, both CJS and ESM files live in the same directory. | ||
| // postinstall/index.mjs is at <pkg-root>/postinstall/index.mjs, | ||
| // so we go up one level to reach the package root. | ||
| const dirname = typeof __dirname === 'string' ? __dirname : _path.default.dirname((0, _url.fileURLToPath)(require('url').pathToFileURL(__filename).toString())); | ||
| (async () => { | ||
@@ -30,3 +27,6 @@ // If Node.js support permissions, we need to check if the current user has | ||
| }); | ||
| const [environmentInfo, projectId, machineId] = await Promise.all([(0, _getEnvironmentInfo.default)(), (0, _getProjectId.default)(), (0, _getMachineId.default)()]); | ||
| const [environmentInfo, repoHash, postinstallPackageNameHash, rootPathHash, machineId] = await Promise.all([(0, _getEnvironmentInfo.default)(), (0, _getProjectId.getAnonymousRepoHash)(), (0, _getProjectId.getAnonymousPackageNameHash)(), (0, _getProjectId.getAnonymousRootPathHash)(), (0, _getMachineId.default)()]); | ||
| // Compute projectId from the resolved signals (no duplicate calls) | ||
| const projectId = repoHash || postinstallPackageNameHash || rootPathHash; | ||
| const contextData = { | ||
@@ -38,2 +38,5 @@ config: { | ||
| machineId, | ||
| repoHash, | ||
| postinstallPackageNameHash, | ||
| rootPathHash, | ||
| projectId, | ||
@@ -44,10 +47,12 @@ sessionId: (0, _crypto.randomBytes)(32).toString('hex'), | ||
| }; | ||
| const writeContextData = (filePath, format) => { | ||
| const targetPath = _path.default.resolve(dirname, '..', filePath, 'context.js'); | ||
| _fs.default.writeFileSync(targetPath, format(JSON.stringify(contextData, null, 2))); | ||
| }; | ||
| writeContextData('esm', content => `export default ${content};`); | ||
| writeContextData('', content => [`"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`, `exports.default = void 0;`, `var _default = exports.default = ${content};`].join('\n')); | ||
| const packageRoot = _path.default.resolve(dirname, '..'); | ||
| const content = JSON.stringify(contextData, null, 2); | ||
| // ESM: context.mjs | ||
| _fs.default.writeFileSync(_path.default.resolve(packageRoot, 'context.mjs'), `export default ${content};`); | ||
| // CJS: context.js | ||
| _fs.default.writeFileSync(_path.default.resolve(packageRoot, 'context.js'), [`"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`, `exports.default = void 0;`, `var _default = exports.default = ${content};`].join('\n')); | ||
| })().catch(error => { | ||
| console.error('[telemetry] Failed to make initialization. Please, report error to MUI X team:\n' + 'https://mui.com/r/x-telemetry-postinstall-troubleshoot\n', error); | ||
| }); |
+20
-14
@@ -7,12 +7,10 @@ import _extends from "@babel/runtime/helpers/esm/extends"; | ||
| import getEnvironmentInfo from "./get-environment-info.mjs"; | ||
| import getAnonymousProjectId from "./get-project-id.mjs"; | ||
| import { getAnonymousRepoHash, getAnonymousPackageNameHash, getAnonymousRootPathHash } from "./get-project-id.mjs"; | ||
| import getAnonymousMachineId from "./get-machine-id.mjs"; | ||
| import { TelemetryStorage } from "./storage.mjs"; | ||
| const dirname = typeof __dirname === 'string' ? __dirname // cjs build in root dir | ||
| : (() => { | ||
| const filename = fileURLToPath(import.meta.url); | ||
| // esm build in `esm` directory, so we need to go up two levels | ||
| return path.dirname(path.dirname(filename)); | ||
| })(); | ||
| // It's a flat build, both CJS and ESM files live in the same directory. | ||
| // postinstall/index.mjs is at <pkg-root>/postinstall/index.mjs, | ||
| // so we go up one level to reach the package root. | ||
| const dirname = typeof __dirname === 'string' ? __dirname : path.dirname(fileURLToPath(import.meta.url)); | ||
| (async () => { | ||
@@ -27,3 +25,6 @@ // If Node.js support permissions, we need to check if the current user has | ||
| }); | ||
| const [environmentInfo, projectId, machineId] = await Promise.all([getEnvironmentInfo(), getAnonymousProjectId(), getAnonymousMachineId()]); | ||
| const [environmentInfo, repoHash, postinstallPackageNameHash, rootPathHash, machineId] = await Promise.all([getEnvironmentInfo(), getAnonymousRepoHash(), getAnonymousPackageNameHash(), getAnonymousRootPathHash(), getAnonymousMachineId()]); | ||
| // Compute projectId from the resolved signals (no duplicate calls) | ||
| const projectId = repoHash || postinstallPackageNameHash || rootPathHash; | ||
| const contextData = { | ||
@@ -35,2 +36,5 @@ config: { | ||
| machineId, | ||
| repoHash, | ||
| postinstallPackageNameHash, | ||
| rootPathHash, | ||
| projectId, | ||
@@ -41,10 +45,12 @@ sessionId: randomBytes(32).toString('hex'), | ||
| }; | ||
| const writeContextData = (filePath, format) => { | ||
| const targetPath = path.resolve(dirname, '..', filePath, 'context.js'); | ||
| fs.writeFileSync(targetPath, format(JSON.stringify(contextData, null, 2))); | ||
| }; | ||
| writeContextData('esm', content => `export default ${content};`); | ||
| writeContextData('', content => [`"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`, `exports.default = void 0;`, `var _default = exports.default = ${content};`].join('\n')); | ||
| const packageRoot = path.resolve(dirname, '..'); | ||
| const content = JSON.stringify(contextData, null, 2); | ||
| // ESM: context.mjs | ||
| fs.writeFileSync(path.resolve(packageRoot, 'context.mjs'), `export default ${content};`); | ||
| // CJS: context.js | ||
| fs.writeFileSync(path.resolve(packageRoot, 'context.js'), [`"use strict";`, `Object.defineProperty(exports, "__esModule", { value: true });`, `exports.default = void 0;`, `var _default = exports.default = ${content};`].join('\n')); | ||
| })().catch(error => { | ||
| console.error('[telemetry] Failed to make initialization. Please, report error to MUI X team:\n' + 'https://mui.com/r/x-telemetry-postinstall-troubleshoot\n', error); | ||
| }); |
+125
-0
@@ -38,1 +38,126 @@ # @mui/x-telemetry | ||
| ``` | ||
| ## How to test | ||
| ### Postinstall output | ||
| The postinstall script writes to two places: | ||
| 1. **`<pkg-root>/context.js` + `<pkg-root>/context.mjs`** — all traits (`machineId`, `repoHash`, `postinstallPackageNameHash`, `rootPathHash`, `projectId`, `anonymousId`, `sessionId`, `isDocker`, `isCI`). Lives inside `node_modules` and gets regenerated on each install. | ||
| 2. **Platform-specific config directory** — persists `anonymousId` and `notifiedAt` across reinstalls, so the `anonymousId` stays stable even if `node_modules` is wiped. | ||
| - **macOS:** `~/Library/Preferences/mui-x/config.json` | ||
| - **Windows:** `%APPDATA%\mui-x\Config\config.json` | ||
| - **Linux:** `$XDG_CONFIG_HOME/mui-x/config.json` (defaults to `~/.config/mui-x/config.json`) | ||
| - **CI/Docker:** `<cwd>/cache/mui-x/config.json` (ephemeral, inside the project) | ||
| ### Testing the postinstall locally | ||
| ```bash | ||
| # 1. Build the package | ||
| pnpm --filter @mui/x-telemetry run build | ||
| # 2. Clean previous output (start fresh) | ||
| rm -f packages/x-telemetry/build/context.js packages/x-telemetry/build/context.mjs ~/Library/Preferences/mui-x/config.json | ||
| # 3. Run the postinstall script | ||
| # Local (macOS/Linux) | ||
| node packages/x-telemetry/build/postinstall/index.js | ||
| # Docker | ||
| docker run --rm -v $(pwd):/repo -w /repo/packages/x-telemetry/build node:20 node ./postinstall/index.mjs | ||
| # 4. Verify the output | ||
| cat packages/x-telemetry/build/context.js # CJS context | ||
| cat packages/x-telemetry/build/context.mjs # ESM context | ||
| cat ~/Library/Preferences/mui-x/config.json # Persistent config (macOS) | ||
| ``` | ||
| ### Testing the package.json name fallback (no git) | ||
| ```bash | ||
| # 1. Build the package | ||
| pnpm --filter @mui/x-telemetry run build | ||
| # 2. Create a temp directory with only a package.json (no .git) | ||
| mkdir -p /tmp/test-pkg && echo '{"name": "my-test-app"}' > /tmp/test-pkg/package.json | ||
| # 3. Clean previous output | ||
| rm -f packages/x-telemetry/build/context.js packages/x-telemetry/build/context.mjs | ||
| # 4. Run postinstall from the temp directory | ||
| cd /tmp/test-pkg && node /path/to/mui-x/packages/x-telemetry/build/postinstall/index.js | ||
| # 5. Verify postinstallPackageNameHash and projectId match sha256("my-test-app") | ||
| grep -E 'postinstallPackageNameHash|projectId' /path/to/mui-x/packages/x-telemetry/build/context.js | ||
| echo -n "my-test-app" | shasum -a 256 | ||
| # postinstallPackageNameHash and projectId should both match the hash | ||
| # 6. Go back to the repo and clean up | ||
| cd /path/to/mui-x | ||
| rm -rf /tmp/test-pkg | ||
| ``` | ||
| ### Testing the runtime fallback (npm_package_name) | ||
| ```bash | ||
| # 1. Build the package | ||
| pnpm --filter @mui/x-telemetry run build | ||
| # 2. Run with npm_package_name set (simulates npm/pnpm run context) | ||
| node -e " | ||
| process.env.npm_package_name = 'my-test-app'; | ||
| import('./packages/x-telemetry/build/runtime/get-context.mjs').then(m => | ||
| m.default().then(ctx => { | ||
| console.log('runtimePackageNameHash:', ctx.traits.runtimePackageNameHash); | ||
| console.log('projectId:', ctx.traits.projectId); | ||
| }) | ||
| ); | ||
| " | ||
| # 3. Verify runtimePackageNameHash matches sha256("my-test-app") | ||
| echo -n "my-test-app" | shasum -a 256 | ||
| # runtimePackageNameHash should output: 04a0c785... | ||
| ``` | ||
| ### Testing the runtime fallback (browser fetch) | ||
| ```bash | ||
| # 1. Create a temp directory with a package.json | ||
| mkdir -p /tmp/test-fetch && cd /tmp/test-fetch | ||
| echo '{"name": "fetch-test-app"}' > package.json | ||
| # 2. Create an index.html that simulates the runtime fetch fallback | ||
| cat > index.html << 'HTMLEOF' | ||
| <script type="module"> | ||
| async function hashString(str) { | ||
| const data = new TextEncoder().encode(str); | ||
| const hashBuffer = await crypto.subtle.digest('SHA-256', data); | ||
| const hashArray = Array.from(new Uint8Array(hashBuffer)); | ||
| return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); | ||
| } | ||
| const res = await fetch('/package.json'); | ||
| const pkg = await res.json(); | ||
| const projectId = await hashString(pkg.name); | ||
| console.log('projectId:', projectId); | ||
| document.body.innerText = 'projectId: ' + projectId; | ||
| </script> | ||
| HTMLEOF | ||
| # 3. Serve and open http://localhost:3000 | ||
| npx serve . | ||
| # 4. Verify the browser console output matches sha256("fetch-test-app") | ||
| echo -n "fetch-test-app" | shasum -a 256 | ||
| # Both should output: 1dcce451... | ||
| # 5. Clean up | ||
| rm -rf /tmp/test-fetch | ||
| ``` | ||
| ### Unit tests | ||
| ```bash | ||
| pnpm test:unit --project "x-telemetry" --run | ||
| ``` |
| import type { TelemetryContextType } from "../context.mjs"; | ||
| declare function getRuntimePackageHash(): Promise<string | null>; | ||
| declare function getTelemetryContext(): Promise<TelemetryContextType>; | ||
| export { TelemetryContextType }; | ||
| export { TelemetryContextType, getRuntimePackageHash }; | ||
| export default getTelemetryContext; |
| import type { TelemetryContextType } from "../context.js"; | ||
| declare function getRuntimePackageHash(): Promise<string | null>; | ||
| declare function getTelemetryContext(): Promise<TelemetryContextType>; | ||
| export { TelemetryContextType }; | ||
| export { TelemetryContextType, getRuntimePackageHash }; | ||
| export default getTelemetryContext; |
@@ -8,5 +8,7 @@ "use strict"; | ||
| exports.default = void 0; | ||
| exports.getRuntimePackageHash = getRuntimePackageHash; | ||
| var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); | ||
| var _interopRequireWildcard2 = _interopRequireDefault(require("@babel/runtime/helpers/interopRequireWildcard")); | ||
| var _context = _interopRequireDefault(require("../context")); | ||
| var _hashString = require("./hash-string"); | ||
| var _windowStorage = require("./window-storage"); | ||
@@ -80,2 +82,25 @@ function generateId(length) { | ||
| } | ||
| async function getRuntimePackageHash() { | ||
| // npm/pnpm scripts automatically set npm_package_name | ||
| if (typeof process !== 'undefined' && process.env?.npm_package_name) { | ||
| return (0, _hashString.hashString)(process.env.npm_package_name); | ||
| } | ||
| // Dev servers often serve package.json from project root | ||
| // (works with Vite, webpack-dev-server, and most static dev setups) | ||
| if (typeof window !== 'undefined') { | ||
| try { | ||
| const res = await fetch('/package.json'); | ||
| if (res.ok) { | ||
| const pkg = await res.json(); | ||
| if (pkg.name && typeof pkg.name === 'string') { | ||
| return (0, _hashString.hashString)(pkg.name); | ||
| } | ||
| } | ||
| } catch (_) { | ||
| // Not served by this dev server, skip | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| async function getTelemetryContext() { | ||
@@ -90,2 +115,13 @@ _context.default.traits.sessionId = getSessionId(); | ||
| } | ||
| // Always resolve runtimePackageNameHash (individual app name in monorepos) | ||
| if (!_context.default.traits.runtimePackageNameHash && !_context.default.config.runtimePackageNameHashResolved) { | ||
| _context.default.config.runtimePackageNameHashResolved = true; | ||
| const runtimePackageHash = await getRuntimePackageHash(); | ||
| _context.default.traits.runtimePackageNameHash = runtimePackageHash; | ||
| if (runtimePackageHash) { | ||
| // Recompute projectId: repoHash || runtimePackageNameHash || postinstallPackageNameHash || rootPathHash | ||
| _context.default.traits.projectId = _context.default.traits.repoHash || runtimePackageHash || _context.default.traits.postinstallPackageNameHash || _context.default.traits.rootPathHash || _context.default.traits.projectId; | ||
| } | ||
| } | ||
| if (!_context.default.traits.fingerprint) { | ||
@@ -92,0 +128,0 @@ _context.default.traits.fingerprint = await getBrowserFingerprint(); |
| import _extends from "@babel/runtime/helpers/esm/extends"; | ||
| import telemetryContext from "../context.mjs"; | ||
| import { hashString } from "./hash-string.mjs"; | ||
| import { getWindowStorageItem, setWindowStorageItem } from "./window-storage.mjs"; | ||
@@ -71,2 +72,25 @@ function generateId(length) { | ||
| } | ||
| async function getRuntimePackageHash() { | ||
| // npm/pnpm scripts automatically set npm_package_name | ||
| if (typeof process !== 'undefined' && process.env?.npm_package_name) { | ||
| return hashString(process.env.npm_package_name); | ||
| } | ||
| // Dev servers often serve package.json from project root | ||
| // (works with Vite, webpack-dev-server, and most static dev setups) | ||
| if (typeof window !== 'undefined') { | ||
| try { | ||
| const res = await fetch('/package.json'); | ||
| if (res.ok) { | ||
| const pkg = await res.json(); | ||
| if (pkg.name && typeof pkg.name === 'string') { | ||
| return hashString(pkg.name); | ||
| } | ||
| } | ||
| } catch (_) { | ||
| // Not served by this dev server, skip | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| async function getTelemetryContext() { | ||
@@ -81,2 +105,13 @@ telemetryContext.traits.sessionId = getSessionId(); | ||
| } | ||
| // Always resolve runtimePackageNameHash (individual app name in monorepos) | ||
| if (!telemetryContext.traits.runtimePackageNameHash && !telemetryContext.config.runtimePackageNameHashResolved) { | ||
| telemetryContext.config.runtimePackageNameHashResolved = true; | ||
| const runtimePackageHash = await getRuntimePackageHash(); | ||
| telemetryContext.traits.runtimePackageNameHash = runtimePackageHash; | ||
| if (runtimePackageHash) { | ||
| // Recompute projectId: repoHash || runtimePackageNameHash || postinstallPackageNameHash || rootPathHash | ||
| telemetryContext.traits.projectId = telemetryContext.traits.repoHash || runtimePackageHash || telemetryContext.traits.postinstallPackageNameHash || telemetryContext.traits.rootPathHash || telemetryContext.traits.projectId; | ||
| } | ||
| } | ||
| if (!telemetryContext.traits.fingerprint) { | ||
@@ -87,2 +122,3 @@ telemetryContext.traits.fingerprint = await getBrowserFingerprint(); | ||
| } | ||
| export { getRuntimePackageHash }; | ||
| export default getTelemetryContext; |
@@ -61,3 +61,3 @@ "use strict"; | ||
| 'Content-Type': 'application/json', | ||
| 'X-Telemetry-Client-Version': "9.0.0-alpha.2" ?? '<dev>', | ||
| 'X-Telemetry-Client-Version': "9.0.0-rc.0" ?? '<dev>', | ||
| 'X-Telemetry-Node-Env': process.env.NODE_ENV ?? '<unknown>' | ||
@@ -64,0 +64,0 @@ }, |
@@ -53,3 +53,3 @@ import _extends from "@babel/runtime/helpers/esm/extends"; | ||
| 'Content-Type': 'application/json', | ||
| 'X-Telemetry-Client-Version': "9.0.0-alpha.2" ?? '<dev>', | ||
| 'X-Telemetry-Client-Version': "9.0.0-rc.0" ?? '<dev>', | ||
| 'X-Telemetry-Node-Env': process.env.NODE_ENV ?? '<unknown>' | ||
@@ -56,0 +56,0 @@ }, |
| export {}; | ||
| declare global { | ||
| interface MUIEnv { | ||
| npm_package_name?: string; | ||
| MUI_VERSION?: string; | ||
@@ -5,0 +6,0 @@ MUI_X_TELEMETRY_DISABLED?: string; |
| export {}; | ||
| declare global { | ||
| interface MUIEnv { | ||
| npm_package_name?: string; | ||
| MUI_VERSION?: string; | ||
@@ -5,0 +6,0 @@ MUI_X_TELEMETRY_DISABLED?: string; |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 16 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 15 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
423955
10.59%64
10.34%1570
21.71%163
328.95%69
13.11%8
60%Updated