diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 46d665df1ad28..f91db1f6c49bc 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -4,6 +4,7 @@ "node": "20", "packages": [ "packages/x-license", + "packages/x-telemetry", "packages/x-data-grid", "packages/x-data-grid-pro", "packages/x-data-grid-premium", @@ -19,6 +20,7 @@ ], "publishDirectory": { "@mui/x-license": "packages/x-license/build", + "@mui/x-telemetry": "packages/x-telemetry/build", "@mui/x-data-grid": "packages/x-data-grid/build", "@mui/x-data-grid-pro": "packages/x-data-grid-pro/build", "@mui/x-data-grid-premium": "packages/x-data-grid-premium/build", diff --git a/.eslintrc.js b/.eslintrc.js index 1f8b8a66d3f38..ff78c211d8983 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -299,6 +299,12 @@ module.exports = { ], }, }, + { + files: ['packages/x-telemetry/**/*{.tsx,.ts,.js}'], + rules: { + 'no-console': 'off', + }, + }, ...buildPackageRestrictedImports('@mui/x-charts', 'x-charts', false), ...buildPackageRestrictedImports('@mui/x-charts-pro', 'x-charts-pro', false), ...buildPackageRestrictedImports('@mui/x-codemod', 'x-codemod', false), @@ -311,6 +317,7 @@ module.exports = { ...buildPackageRestrictedImports('@mui/x-tree-view', 'x-tree-view', false), ...buildPackageRestrictedImports('@mui/x-tree-view-pro', 'x-tree-view-pro', false), ...buildPackageRestrictedImports('@mui/x-license', 'x-license'), + ...buildPackageRestrictedImports('@mui/x-telemetry', 'x-telemetry'), ...addReactCompilerRule(chartsPackages, ENABLE_REACT_COMPILER_PLUGIN_CHARTS), ...addReactCompilerRule(dataGridPackages, ENABLE_REACT_COMPILER_PLUGIN_DATA_GRID), diff --git a/babel.config.js b/babel.config.js index 943fa0e38d098..2b711d68ce801 100644 --- a/babel.config.js +++ b/babel.config.js @@ -22,6 +22,7 @@ const defaultAlias = { '@mui/x-data-grid-pro': resolveAliasPath('./packages/x-data-grid-pro/src'), '@mui/x-data-grid-premium': resolveAliasPath('./packages/x-data-grid-premium/src'), '@mui/x-license': resolveAliasPath('./packages/x-license/src'), + '@mui/x-telemetry': resolveAliasPath('./packages/x-telemetry/src'), '@mui/x-date-pickers': resolveAliasPath('./packages/x-date-pickers/src'), '@mui/x-date-pickers-pro': resolveAliasPath('./packages/x-date-pickers-pro/src'), '@mui/x-charts': resolveAliasPath('./packages/x-charts/src'), diff --git a/docs/data/guides/telemetry/telemetry.md b/docs/data/guides/telemetry/telemetry.md new file mode 100644 index 0000000000000..bb02c69c83093 --- /dev/null +++ b/docs/data/guides/telemetry/telemetry.md @@ -0,0 +1,75 @@ +--- +packageName: '@mui/x-telemetry' +--- + +# MUI X Telemetry guide + +

MUI X Telemetry collects anonymous usage data to help improve the library. This guide walk you through how to opt-in, opt-out, and configure telemetry.

+ +## Opting In + +Currently, **Telemetry is disabled by default**. To opt-in, you can use one of the following methods: + +### Setting the Environment Variable + +You can set the `MUI_X_TELEMETRY_DISABLED` environment variable to `false` to enable telemetry: + +```bash +MUI_X_TELEMETRY_DISABLED=false +``` + +> Note that some frameworks may require you to prefix the environment variable with `REACT_APP_`, `NEXT_PUBLIC_`, etc. + +### Import telemetry settings from `@mui/x-license` package + +You can use `muiXTelemetrySettings` to enable telemetry: + +```js +import { muiXTelemetrySettings } from '@mui/x-license'; + +muiXTelemetrySettings.enableTelemetry(); +``` + +### Setting the Flag in Your Application + +You can set the `__MUI_X_TELEMETRY_DISABLED__` flag in your application to `false` to enable telemetry: + +```js +import { ponyfillGlobal } from '@mui/utils'; + +ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = false; +``` + +## Opting Out + +To opt-out of telemetry, you can use one of the following methods: + +### Setting the Environment Variable + +You can set the `MUI_X_TELEMETRY_DISABLED` environment variable to `true` to disable telemetry: + +```bash +MUI_X_TELEMETRY_DISABLED=true +``` + +> Note that some frameworks may require you to prefix the environment variable with `REACT_APP_`, `NEXT_PUBLIC_`, etc. + +### Import telemetry settings from `@mui/x-license` package + +You can use `muiXTelemetrySettings` to disable telemetry: + +```js +import { muiXTelemetrySettings } from '@mui/x-license'; + +muiXTelemetrySettings.disableTelemetry(); +``` + +### Setting the Flag in Your Application + +You can set the `__MUI_X_TELEMETRY_DISABLED__` flag in your application to `true` to disable telemetry: + +```js +import { ponyfillGlobal } from '@mui/utils'; + +ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = true; +``` diff --git a/docs/pages/_app.js b/docs/pages/_app.js index 9186f4d33159e..ede2609f21ca0 100644 --- a/docs/pages/_app.js +++ b/docs/pages/_app.js @@ -8,6 +8,7 @@ import NextHead from 'next/head'; import PropTypes from 'prop-types'; import { useRouter } from 'next/router'; import { LicenseInfo } from '@mui/x-license'; +import { muiXTelemetrySettings } from '@mui/x-telemetry'; import { ponyfillGlobal } from '@mui/utils'; import PageContext from 'docs/src/modules/components/PageContext'; import GoogleAnalytics from 'docs/src/modules/components/GoogleAnalytics'; @@ -24,6 +25,8 @@ import { DocsProvider } from '@mui/docs/DocsProvider'; import { mapTranslations } from '@mui/docs/i18n'; import * as config from '../config'; +// Enable telemetry for internal purposes +muiXTelemetrySettings.enableTelemetry(); // Remove the license warning from demonstration purposes LicenseInfo.setLicenseKey(process.env.NEXT_PUBLIC_MUI_LICENSE); diff --git a/docs/pages/x/guides/telemetry.js b/docs/pages/x/guides/telemetry.js new file mode 100644 index 0000000000000..d227cc838b320 --- /dev/null +++ b/docs/pages/x/guides/telemetry.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/guides/telemetry/telemetry.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/public/_redirects b/docs/public/_redirects index bbefb47723e45..7ecfd87a34398 100644 --- a/docs/public/_redirects +++ b/docs/public/_redirects @@ -19,6 +19,7 @@ /r/x-pro-svg-link https://mui.com/x/introduction/licensing/#pro-plan 302 /r/x-premium-svg https://mui.com/static/x/premium.svg 302 /r/x-premium-svg-link https://mui.com/x/introduction/licensing/#premium-plan 302 +/r/x-telemetry-postinstall-troubleshoot https://github.com/mui/mui-x/issues/new?assignees=&labels=status%3A+waiting+for+maintainer%2Cbug+%F0%9F%90%9B&projects=&template=1.bug.yml&title=MUI+X+Telemetry+failed+to+make+initialization 302 # Legacy redirection # Added in chronological order (the last line is the most recent one) diff --git a/packages/x-license/package.json b/packages/x-license/package.json index 3495d866a9661..b29647abfc9ac 100644 --- a/packages/x-license/package.json +++ b/packages/x-license/package.json @@ -36,6 +36,7 @@ "dependencies": { "@babel/runtime": "^7.26.9", "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0-alpha", + "@mui/x-telemetry": "workspace:*", "@mui/x-internals": "workspace:*" }, "peerDependencies": { diff --git a/packages/x-license/src/index.ts b/packages/x-license/src/index.ts index 6a67f030d94ef..57f1291aecb80 100644 --- a/packages/x-license/src/index.ts +++ b/packages/x-license/src/index.ts @@ -4,3 +4,4 @@ export * from './verifyLicense'; export * from './useLicenseVerifier'; export * from './Watermark'; export * from './Unstable_LicenseInfoProvider'; +export { muiXTelemetrySettings } from '@mui/x-telemetry'; diff --git a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts index 72fb2bf48f100..c52d97988e596 100644 --- a/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts +++ b/packages/x-license/src/useLicenseVerifier/useLicenseVerifier.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { sendMuiXTelemetryEvent, muiXTelemetryEvents } from '@mui/x-telemetry'; import { verifyLicense } from '../verifyLicense/verifyLicense'; import { LicenseInfo } from '../utils/licenseInfo'; import { @@ -50,6 +51,17 @@ export function useLicenseVerifier( const fullPackageName = `@mui/${packageName}`; + sendMuiXTelemetryEvent( + muiXTelemetryEvents.licenseVerification( + { licenseKey }, + { + packageName, + packageReleaseInfo: releaseInfo, + licenseStatus: licenseStatus?.status, + }, + ), + ); + if (licenseStatus.status === LICENSE_STATUS.Valid) { // Skip } else if (licenseStatus.status === LICENSE_STATUS.Invalid) { diff --git a/packages/x-license/tsconfig.build.json b/packages/x-license/tsconfig.build.json index 3ac51848ef120..c945961210133 100644 --- a/packages/x-license/tsconfig.build.json +++ b/packages/x-license/tsconfig.build.json @@ -10,7 +10,10 @@ "outDir": "build/esm", "rootDir": "./src" }, - "references": [{ "path": "../x-internals/tsconfig.build.json" }], + "references": [ + { "path": "../x-internals/tsconfig.build.json" }, + { "path": "../x-telemetry/tsconfig.build.json" } + ], "include": ["src/**/*.ts*"], "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/x-telemetry/LICENSE b/packages/x-telemetry/LICENSE new file mode 100644 index 0000000000000..bda47bde65477 --- /dev/null +++ b/packages/x-telemetry/LICENSE @@ -0,0 +1,11 @@ +Commercial License + +Copyright (c) 2020 Material-UI SAS + +MUI X Pro (https://mui.com/pricing/) is commercial software. You MUST agree to the +End User License Agreement (EULA: https://mui.com/r/x-license-eula) to be able to +use the software. + +This means that you either need to purchase a commercial license at +https://mui.com/r/x-get-license?scope=pro or be eligible for the Evaluation (trial) +licenses detailed at https://mui.com/r/x-license-trial. diff --git a/packages/x-telemetry/README.md b/packages/x-telemetry/README.md new file mode 100644 index 0000000000000..2b6e539f612ae --- /dev/null +++ b/packages/x-telemetry/README.md @@ -0,0 +1,51 @@ +# @mui/x-telemetry + +Package used by some of MUI X to collects **anonymous** telemetry data about general usage. Participation in this anonymous program is optional, and you may opt-out if you'd not like to share any information. + +## How to opt-in + +Currently, **it's disabled by default,** and you could opt-in to it in 3 ways: + +1. By setting it directly to package settings on the application start (e.g. in main file). + +```js +import { muiXTelemetrySettings } from '@mui/x-telemetry'; +// or +import { muiXTelemetrySettings } from '@mui/x-license'; + +muiXTelemetrySettings.enableTelemetry(); // to enable telemetry collection and sending +// or +muiXTelemetrySettings.disableTelemetry(); // to disable telemetry collection and sending +``` + +2. By setting the environment variable. + +```dotenv +MUI_X_TELEMETRY_DISABLED=false # Enable telemetry +# or +MUI_X_TELEMETRY_DISABLED=true # Enable telemetry +``` + +> ⚠️ Note that some frameworks requires to prefix the variable with `REACT_APP_`, `NEXT_PUBLIC_`, etc. + +3. By setting the flag to global object on the application start (e.g. in main file). + +```js +import { ponyfillGlobal } from '@mui/utils'; + +ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = false; // enabled +// or +ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = true; // disabled +``` + +OR + +```js +if (typeof window !== 'undefined') { + window.__MUI_X_TELEMETRY_DISABLED__ = false; // enabled +} +// or +if (typeof window !== 'undefined') { + window.__MUI_X_TELEMETRY_DISABLED__ = true; // disabled +} +``` diff --git a/packages/x-telemetry/package.json b/packages/x-telemetry/package.json new file mode 100644 index 0000000000000..275e9763ec147 --- /dev/null +++ b/packages/x-telemetry/package.json @@ -0,0 +1,52 @@ +{ + "name": "@mui/x-telemetry", + "version": "8.0.0-alpha.12", + "description": "MUI X Telemetry", + "author": "MUI Team", + "main": "src/index.ts", + "license": "SEE LICENSE IN LICENSE", + "bugs": { + "url": "https://github.com/mui/mui-x/issues" + }, + "homepage": "https://mui.com/x/guides/telemetry/", + "publishConfig": { + "access": "public", + "directory": "build" + }, + "scripts": { + "typescript": "tsc -p tsconfig.json", + "build": "pnpm build:modern && pnpm build:node && pnpm build:stable && pnpm build:types && pnpm build:copy-files ", + "build:modern": "node ../../scripts/build.mjs modern", + "build:node": "node ../../scripts/build.mjs node", + "build:stable": "node ../../scripts/build.mjs stable", + "build:copy-files": "node ../../scripts/copyFiles.mjs && node ./scripts/addPackageScripts.js", + "build:types": "tsx ../../scripts/buildTypes.mts", + "prebuild": "rimraf build tsconfig.build.tsbuildinfo" + }, + "sideEffects": false, + "packageScripts": { + "postinstall": "node ./postinstall/index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/mui/mui-x.git", + "directory": "packages/x-telemetry" + }, + "dependencies": { + "@babel/runtime": "^7.26.9", + "@fingerprintjs/fingerprintjs": "^4.6.0", + "@mui/utils": "^5.16.6 || ^6.0.0", + "ci-info": "^4.0.0", + "conf": "^5.0.0", + "is-docker": "^2.2.1", + "node-machine-id": "^1.1.12" + }, + "devDependencies": { + "@mui/internal-test-utils": "^2.0.2", + "@types/device-uuid": "^1.0.3", + "rimraf": "^6.0.1" + }, + "engines": { + "node": ">=14.0.0" + } +} diff --git a/packages/x-telemetry/scripts/addPackageScripts.js b/packages/x-telemetry/scripts/addPackageScripts.js new file mode 100644 index 0000000000000..922365e607b60 --- /dev/null +++ b/packages/x-telemetry/scripts/addPackageScripts.js @@ -0,0 +1,19 @@ +const fs = require('fs'); +const path = require('path'); + +const packageDir = process.cwd(); + +const packageJsonPath = path.join(packageDir, 'package.json'); +const packageJsonFileContent = fs.readFileSync(packageJsonPath, 'utf8'); +const packageJson = JSON.parse(packageJsonFileContent); + +if (packageJson.packageScripts) { + const buildPackageJsonPath = path.join(packageDir, 'build/package.json'); + const buildPackageJsonFileContent = fs.readFileSync(buildPackageJsonPath, 'utf8'); + const buildPackageJson = JSON.parse(buildPackageJsonFileContent); + + buildPackageJson.scripts = packageJson.packageScripts; + delete buildPackageJson.packageScripts; + + fs.writeFileSync(buildPackageJsonPath, JSON.stringify(buildPackageJson, null, 2)); +} diff --git a/packages/x-telemetry/src/context.ts b/packages/x-telemetry/src/context.ts new file mode 100644 index 0000000000000..bc8c987086202 --- /dev/null +++ b/packages/x-telemetry/src/context.ts @@ -0,0 +1,25 @@ +// This file will be modified by the `postinstall` script. +// See postinstall/index.ts for more information. + +export interface TelemetryContextType { + config: { + isInitialized: boolean; + }; + traits: Record & { + machineId?: string | null; + projectId?: string | null; + sessionId?: string | null; + anonymousId?: string | null; + isDocker?: boolean; + isCI?: boolean; + }; +} + +const defaultValue: TelemetryContextType = { + config: { + isInitialized: false, + }, + traits: {}, +}; + +export default defaultValue; diff --git a/packages/x-telemetry/src/index.ts b/packages/x-telemetry/src/index.ts new file mode 100644 index 0000000000000..6dc5619e93cc9 --- /dev/null +++ b/packages/x-telemetry/src/index.ts @@ -0,0 +1,21 @@ +import muiXTelemetryEvents from './runtime/events'; +import sendMuiXTelemetryEventOriginal from './runtime/sender'; +import muiXTelemetrySettingsOriginal from './runtime/settings'; + +const noop = () => {}; + +// To cut unused imports in production as early as possible +const sendMuiXTelemetryEvent = + process.env.NODE_ENV === 'production' ? noop : sendMuiXTelemetryEventOriginal; + +// To cut unused imports in production as early as possible +const muiXTelemetrySettings = + process.env.NODE_ENV === 'production' + ? { + enableDebug: noop, + enableTelemetry: noop, + disableTelemetry: noop, + } + : muiXTelemetrySettingsOriginal; + +export { muiXTelemetryEvents, sendMuiXTelemetryEvent, muiXTelemetrySettings }; diff --git a/packages/x-telemetry/src/postinstall/get-environment-info.ts b/packages/x-telemetry/src/postinstall/get-environment-info.ts new file mode 100644 index 0000000000000..8efe6ea4a2c08 --- /dev/null +++ b/packages/x-telemetry/src/postinstall/get-environment-info.ts @@ -0,0 +1,20 @@ +import isDockerFunction from 'is-docker'; +import ciEnvironment from 'ci-info'; + +interface EnvironmentInfo { + isDocker: boolean; + isCI: boolean; +} + +let traits: EnvironmentInfo | undefined; + +export default function getEnvironmentInfo(): EnvironmentInfo { + if (!traits) { + traits = { + isDocker: isDockerFunction(), + isCI: ciEnvironment.isCI, + }; + } + + return traits; +} diff --git a/packages/x-telemetry/src/postinstall/get-machine-id.ts b/packages/x-telemetry/src/postinstall/get-machine-id.ts new file mode 100644 index 0000000000000..9c603cce76dcb --- /dev/null +++ b/packages/x-telemetry/src/postinstall/get-machine-id.ts @@ -0,0 +1,19 @@ +import { machineId } from 'node-machine-id'; +import { createHash } from 'crypto'; + +async function getRawMachineId(): Promise { + try { + return await machineId(true); + } catch (_) { + return null; + } +} + +export default async function getAnonymousMachineId(): Promise { + const rawMachineId = await getRawMachineId(); + if (!rawMachineId) { + return null; + } + + return createHash('sha256').update(rawMachineId).digest('hex'); +} diff --git a/packages/x-telemetry/src/postinstall/get-project-id.ts b/packages/x-telemetry/src/postinstall/get-project-id.ts new file mode 100644 index 0000000000000..5782c5eb37beb --- /dev/null +++ b/packages/x-telemetry/src/postinstall/get-project-id.ts @@ -0,0 +1,39 @@ +import { exec } from 'child_process'; +import { createHash } from 'crypto'; +import util from 'util'; + +const asyncExec = util.promisify(exec); + +async function execCLI(command: string): Promise { + try { + const response = await asyncExec(command, { + timeout: 1000, + windowsHide: true, + }); + + return String(response).trim(); + } catch (_) { + return null; + } +} + +// Q: Why does MUI need a project ID? Why is it looking at my git remote? +// 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. + +async function getRawProjectId(): Promise { + return ( + (await execCLI(`git config --local --get remote.origin.url`)) || + process.env.REPOSITORY_URL || + (await execCLI(`git rev-parse --show-toplevel`)) || + process.cwd() + ); +} + +export default async function getAnonymousProjectId(): Promise { + const rawProjectId = await getRawProjectId(); + return createHash('sha256').update(rawProjectId).digest('hex'); +} diff --git a/packages/x-telemetry/src/postinstall/index.ts b/packages/x-telemetry/src/postinstall/index.ts new file mode 100644 index 0000000000000..7079cc71b4a68 --- /dev/null +++ b/packages/x-telemetry/src/postinstall/index.ts @@ -0,0 +1,64 @@ +import fs from 'fs'; +import path from 'path'; +import { randomBytes } from 'crypto'; +import type { TelemetryContextType } from '../context'; +import getEnvironmentInfo from './get-environment-info'; +import getAnonymousProjectId from './get-project-id'; +import getAnonymousMachineId from './get-machine-id'; +import { TelemetryStorage } from './storage'; + +(async () => { + // If Node.js support permissions, we need to check if the current user has + // the necessary permissions to write to the file system. + if ( + typeof process.permission !== 'undefined' && + !(process.permission.has('fs.read') && process.permission.has('fs.write')) + ) { + return; + } + + const storage = new TelemetryStorage({ + distDir: path.join(process.cwd()), + }); + + const [environmentInfo, projectId, machineId] = await Promise.all([ + getEnvironmentInfo(), + getAnonymousProjectId(), + getAnonymousMachineId(), + ]); + + const contextData: TelemetryContextType = { + config: { + isInitialized: true, + }, + traits: { + ...environmentInfo, + machineId, + projectId, + sessionId: randomBytes(32).toString('hex'), + anonymousId: storage.anonymousId, + }, + }; + + const writeContextData = (filePath: string, format: (content: string) => string) => { + const targetPath = path.resolve(__dirname, '..', filePath, 'context.js'); + fs.writeFileSync(targetPath, format(JSON.stringify(contextData, null, 2))); + }; + + writeContextData('modern', (content) => `export default ${content};`); + 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'), + ); +})().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, + ); +}); diff --git a/packages/x-telemetry/src/postinstall/notify.ts b/packages/x-telemetry/src/postinstall/notify.ts new file mode 100644 index 0000000000000..37618604edd0e --- /dev/null +++ b/packages/x-telemetry/src/postinstall/notify.ts @@ -0,0 +1,9 @@ +export default function notifyAboutMuiXTelemetry() { + console.log(`[Attention]: MUI X now may collect completely anonymous telemetry regarding usage.`); + console.log(`This information is used to shape MUI's roadmap and prioritize features.`); + console.log( + `You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:`, + ); + console.log('https://mui.com/x/guides/telemetry'); + console.log(); +} diff --git a/packages/x-telemetry/src/postinstall/storage.ts b/packages/x-telemetry/src/postinstall/storage.ts new file mode 100644 index 0000000000000..e674d16e29eda --- /dev/null +++ b/packages/x-telemetry/src/postinstall/storage.ts @@ -0,0 +1,73 @@ +import { randomBytes } from 'crypto'; +import path from 'path'; +import Conf from 'conf'; +import isDockerFunction from 'is-docker'; +import ciEnvironment from 'ci-info'; +import notifyAboutMuiXTelemetry from './notify'; + +// This is the key that specifies when the user was informed about telemetry collection. +const TELEMETRY_KEY_NOTIFY_DATE = 'telemetry.notifiedAt'; + +// This is a quasi-persistent identifier used to dedupe recurring events. It's +// generated from random data and completely anonymous. +const TELEMETRY_KEY_ID = `telemetry.anonymousId`; + +function getStorageDirectory(distDir: string): string | undefined { + const isLikelyEphemeral = ciEnvironment.isCI || isDockerFunction(); + + if (isLikelyEphemeral) { + return path.join(distDir, 'cache'); + } + + return undefined; +} + +export class TelemetryStorage { + private readonly conf: Conf | null; + + constructor({ distDir }: { distDir: string }) { + const storageDirectory = getStorageDirectory(distDir); + + try { + // `conf` incorrectly throws a permission error during initialization + // instead of waiting for first use. We need to handle it, otherwise the + // process may crash. + this.conf = new Conf({ projectName: 'mui-x', cwd: storageDirectory }); + } catch (_) { + this.conf = null; + } + this.notify(); + } + + private notify = () => { + if (!this.conf) { + return; + } + + // The end-user has already been notified about our telemetry integration. We + // don't need to constantly annoy them about it. + // We will re-inform users about the telemetry if significant changes are + // ever made. + if (this.conf.get(TELEMETRY_KEY_NOTIFY_DATE, '')) { + return; + } + this.conf.set(TELEMETRY_KEY_NOTIFY_DATE, Date.now().toString()); + + notifyAboutMuiXTelemetry(); + }; + + get configPath(): string | undefined { + return this.conf?.path; + } + + get anonymousId(): string { + const val = this.conf && this.conf.get(TELEMETRY_KEY_ID); + if (val) { + return val; + } + + const generated = randomBytes(32).toString('hex'); + this.conf?.set(TELEMETRY_KEY_ID, generated); + return generated; + } +} diff --git a/packages/x-telemetry/src/runtime/config.import-meta.ts b/packages/x-telemetry/src/runtime/config.import-meta.ts new file mode 100644 index 0000000000000..52c5344a2866d --- /dev/null +++ b/packages/x-telemetry/src/runtime/config.import-meta.ts @@ -0,0 +1,4 @@ +// @ts-ignore +const importMetaEnv: Record | undefined = import.meta.env; + +export { importMetaEnv }; diff --git a/packages/x-telemetry/src/runtime/config.test.ts b/packages/x-telemetry/src/runtime/config.test.ts new file mode 100644 index 0000000000000..bcc62532fb65b --- /dev/null +++ b/packages/x-telemetry/src/runtime/config.test.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-underscore-dangle */ + +import sinon from 'sinon'; +import { expect } from 'chai'; +import { ponyfillGlobal } from '@mui/utils'; +import { getTelemetryEnvConfig } from './config'; +import { muiXTelemetrySettings } from '../index'; + +describe('Telemetry: getTelemetryConfig', () => { + beforeEach(() => { + sinon.stub(process, 'env').value({ + NODE_ENV: 'development', + }); + }); + + afterEach(() => { + sinon.restore(); + // Reset env config cache + getTelemetryEnvConfig(true); + }); + + it('should be disabled by default', () => { + expect(getTelemetryEnvConfig(true).IS_COLLECTING).not.equal(true); + }); + + function testConfigWithDisabledEnv(envKey: string) { + it(`should be disabled, if ${envKey} is set to '1'`, () => { + sinon.stub(process, 'env').value({ [envKey]: '1' }); + expect(getTelemetryEnvConfig(true).IS_COLLECTING).equal(false); + }); + + it(`should be enabled, if ${envKey} is set to '0'`, () => { + sinon.stub(process, 'env').value({ [envKey]: '0' }); + expect(getTelemetryEnvConfig(true).IS_COLLECTING).equal(true); + }); + } + + testConfigWithDisabledEnv('MUI_X_TELEMETRY_DISABLED'); + testConfigWithDisabledEnv('REACT_APP_MUI_X_TELEMETRY_DISABLED'); + testConfigWithDisabledEnv('NEXT_PUBLIC_MUI_X_TELEMETRY_DISABLED'); + + it('should be disabled if global.__MUI_X_TELEMETRY_DISABLED__ is set to `1`', () => { + ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = undefined; + sinon.stub(ponyfillGlobal, '__MUI_X_TELEMETRY_DISABLED__').value(true); + + expect(getTelemetryEnvConfig(true).IS_COLLECTING).equal(false); + }); + + it('should be enabled if global.__MUI_X_TELEMETRY_DISABLED__ is set to `0`', () => { + ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__ = undefined; + sinon.stub(ponyfillGlobal, '__MUI_X_TELEMETRY_DISABLED__').value(false); + + expect(getTelemetryEnvConfig(true).IS_COLLECTING).equal(true); + }); + + it('should be changed with `muiXTelemetrySettings`', () => { + muiXTelemetrySettings.enableTelemetry(); + expect(getTelemetryEnvConfig().IS_COLLECTING).equal(true); + + muiXTelemetrySettings.disableTelemetry(); + expect(getTelemetryEnvConfig().IS_COLLECTING).equal(false); + + muiXTelemetrySettings.enableTelemetry(); + expect(getTelemetryEnvConfig().IS_COLLECTING).equal(true); + }); + + it('debug should be enabled with `muiXTelemetrySettings.enableDebug()`', () => { + expect(getTelemetryEnvConfig().DEBUG).equal(false); + + muiXTelemetrySettings.enableDebug(); + expect(getTelemetryEnvConfig().DEBUG).equal(true); + }); + + it('debug should be enabled if env MUI_X_TELEMETRY_DEBUG is set to `1`', () => { + sinon.stub(process, 'env').value({ MUI_X_TELEMETRY_DEBUG: '1' }); + process.stdout.write(`${JSON.stringify(getTelemetryEnvConfig(true), null, 2)}\n`); + expect(getTelemetryEnvConfig(true).DEBUG).equal(true); + + sinon.stub(process, 'env').value({ MUI_X_TELEMETRY_DEBUG: '0' }); + process.stdout.write(`${JSON.stringify(getTelemetryEnvConfig(true), null, 2)}\n`); + expect(getTelemetryEnvConfig(true).DEBUG).equal(false); + }); +}); diff --git a/packages/x-telemetry/src/runtime/config.ts b/packages/x-telemetry/src/runtime/config.ts new file mode 100644 index 0000000000000..e5ba550975bc8 --- /dev/null +++ b/packages/x-telemetry/src/runtime/config.ts @@ -0,0 +1,195 @@ +import { ponyfillGlobal } from '@mui/utils'; + +interface TelemetryEnvConfig { + NODE_ENV: string | ''; + IS_COLLECTING: boolean | undefined; + DEBUG: boolean; +} + +const envEnabledValues = ['1', 'true', 'yes', 'y']; +const envDisabledValues = ['0', 'false', 'no', 'n']; + +function getBooleanEnv(value?: string): boolean | undefined { + if (!value) { + return undefined; + } + if (envEnabledValues.includes(value)) { + return true; + } + if (envDisabledValues.includes(value)) { + return false; + } + return undefined; +} + +function getBooleanEnvFromEnvObject(envKey: string, envObj: Record) { + const keys = Object.keys(envObj); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + if (!key.endsWith(envKey)) { + continue; + } + const value = getBooleanEnv(envObj[key]?.toLowerCase()); + if (typeof value === 'boolean') { + return value; + } + } + + return undefined; +} + +function getIsTelemetryCollecting(): boolean | undefined { + // Check global variable + // eslint-disable-next-line no-underscore-dangle + const globalValue = ponyfillGlobal.__MUI_X_TELEMETRY_DISABLED__; + if (typeof globalValue === 'boolean') { + // If disabled=true, telemetry is disabled + // If disabled=false, telemetry is enabled + return !globalValue; + } + + try { + if (typeof process !== 'undefined' && process.env && typeof process.env === 'object') { + const result = getBooleanEnvFromEnvObject('MUI_X_TELEMETRY_DISABLED', process.env); + if (typeof result === 'boolean') { + // If disabled=true, telemetry is disabled + // If disabled=false, telemetry is enabled + return !result; + } + } + } catch (_) { + // If there is an error, return the default value + } + + try { + // e.g. Vite.js + // eslint-disable-next-line global-require + const { importMetaEnv } = require('./config.import-meta'); + if (importMetaEnv) { + const result = getBooleanEnvFromEnvObject('MUI_X_TELEMETRY_DISABLED', importMetaEnv); + if (typeof result === 'boolean') { + // If disabled=true, telemetry is disabled + // If disabled=false, telemetry is enabled + return !result; + } + } + } catch (_) { + // If there is an error, return the default value + } + + try { + // Some build tools replace env variables on compilation + // e.g. Next.js, webpack EnvironmentPlugin + const envValue = + process.env.MUI_X_TELEMETRY_DISABLED || + process.env.NEXT_PUBLIC_MUI_X_TELEMETRY_DISABLED || + process.env.GATSBY_MUI_X_TELEMETRY_DISABLED || + process.env.REACT_APP_MUI_X_TELEMETRY_DISABLED || + process.env.PUBLIC_MUI_X_TELEMETRY_DISABLED; + const result = getBooleanEnv(envValue); + if (typeof result === 'boolean') { + // If disabled=true, telemetry is disabled + // If disabled=false, telemetry is enabled + return !result; + } + } catch (_) { + // If there is an error, return the default value + } + + return undefined; +} + +function getIsDebugModeEnabled(): boolean { + try { + // Check global variable + // eslint-disable-next-line no-underscore-dangle + const globalValue = ponyfillGlobal.__MUI_X_TELEMETRY_DEBUG__; + if (typeof globalValue === 'boolean') { + return globalValue; + } + + if (typeof process !== 'undefined' && process.env && typeof process.env === 'object') { + const result = getBooleanEnvFromEnvObject('MUI_X_TELEMETRY_DEBUG', process.env); + if (typeof result === 'boolean') { + return result; + } + } + + // e.g. Webpack EnvironmentPlugin + if (process.env.MUI_X_TELEMETRY_DEBUG) { + const result = getBooleanEnv(process.env.MUI_X_TELEMETRY_DEBUG); + if (typeof result === 'boolean') { + return result; + } + } + } catch (_) { + // If there is an error, return the default value + } + + try { + // e.g. Vite.js + // eslint-disable-next-line global-require + const { importMetaEnv } = require('./config.import-meta'); + if (importMetaEnv) { + const result = getBooleanEnvFromEnvObject('MUI_X_TELEMETRY_DEBUG', importMetaEnv); + if (typeof result === 'boolean') { + return result; + } + } + } catch (_) { + // If there is an error, return the default value + } + + try { + // e.g. Next.js, webpack EnvironmentPlugin + const envValue = + process.env.MUI_X_TELEMETRY_DEBUG || + process.env.NEXT_PUBLIC_MUI_X_TELEMETRY_DEBUG || + process.env.GATSBY_MUI_X_TELEMETRY_DEBUG || + process.env.REACT_APP_MUI_X_TELEMETRY_DEBUG || + process.env.PUBLIC_MUI_X_TELEMETRY_DEBUG; + const result = getBooleanEnv(envValue); + if (typeof result === 'boolean') { + return result; + } + } catch (_) { + // If there is an error, return the default value + } + + return false; +} + +function getNodeEnv(): string { + try { + return process.env.NODE_ENV ?? ''; + } catch (_) { + return ''; + } +} + +let cachedEnv: TelemetryEnvConfig | null = null; + +export function getTelemetryEnvConfig(skipCache: boolean = false): TelemetryEnvConfig { + if (skipCache || !cachedEnv) { + cachedEnv = { + NODE_ENV: getNodeEnv(), + IS_COLLECTING: getIsTelemetryCollecting(), + DEBUG: getIsDebugModeEnabled(), + }; + } + + return cachedEnv; +} + +export function getTelemetryEnvConfigValue( + key: K, +): TelemetryEnvConfig[K] { + return getTelemetryEnvConfig()[key]; +} + +export function setTelemetryEnvConfigValue( + key: K, + value: NonNullable, +) { + getTelemetryEnvConfig()[key] = value; +} diff --git a/packages/x-telemetry/src/runtime/events.ts b/packages/x-telemetry/src/runtime/events.ts new file mode 100644 index 0000000000000..bb2e4d05e9c1e --- /dev/null +++ b/packages/x-telemetry/src/runtime/events.ts @@ -0,0 +1,18 @@ +import { TelemetryEventContext } from '../types'; + +const muiXTelemetryEvents = { + licenseVerification: ( + context: TelemetryEventContext, + payload: { + packageReleaseInfo: string; + packageName: string; + licenseStatus?: string; + }, + ) => ({ + eventName: 'licenseVerification', + payload, + context, + }), +}; + +export default muiXTelemetryEvents; diff --git a/packages/x-telemetry/src/runtime/fetcher.ts b/packages/x-telemetry/src/runtime/fetcher.ts new file mode 100644 index 0000000000000..860a9eb05e718 --- /dev/null +++ b/packages/x-telemetry/src/runtime/fetcher.ts @@ -0,0 +1,22 @@ +async function fetchWithRetry(url: string, options: RequestInit, retries = 3): Promise { + try { + const response = await fetch(url, options); + if (response.ok) { + return response; + } + + throw new Error(`Request failed with status ${response.status}`); + } catch (error) { + if (retries === 0) { + throw error; + } + + return new Promise((resolve) => { + setTimeout(() => { + resolve(fetchWithRetry(url, options, retries - 1)); + }, Math.random() * 3_000); + }); + } +} + +export { fetchWithRetry }; diff --git a/packages/x-telemetry/src/runtime/get-context.ts b/packages/x-telemetry/src/runtime/get-context.ts new file mode 100644 index 0000000000000..631cf386ec1a7 --- /dev/null +++ b/packages/x-telemetry/src/runtime/get-context.ts @@ -0,0 +1,83 @@ +import telemetryContext from '../context'; +import type { TelemetryContextType } from '../context'; +import { + getWindowStorageItem, + setWindowStorageItem, + isWindowStorageAvailable, +} from './window-storage'; + +function generateId(length: number): string { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + let counter = 0; + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; +} + +const getMachineId = + typeof window === 'undefined' || process.env.NODE_ENV === 'test' + ? () => '' + : async () => { + const FingerprintJS = await import('@fingerprintjs/fingerprintjs'); + const fpPromise = FingerprintJS.load(); + const fp = await fpPromise; + const result = await fp.get(); + return result.visitorId; + }; + +function getAnonymousId(): string { + if (isWindowStorageAvailable('localStorage')) { + const localStorageKey = 'anonymous_id'; + const existingAnonymousId = getWindowStorageItem('localStorage', localStorageKey); + if (existingAnonymousId) { + return existingAnonymousId; + } + + const generated = generateId(32); + if (setWindowStorageItem('localStorage', localStorageKey, generated)) { + return generated; + } + } + + return ''; +} + +function getSessionId(): string { + if (isWindowStorageAvailable('sessionStorage')) { + const localStorageKey = 'session_id'; + const existingSessionId = getWindowStorageItem('sessionStorage', localStorageKey); + if (existingSessionId) { + return existingSessionId; + } + + const generated = generateId(32); + if (setWindowStorageItem('sessionStorage', localStorageKey, generated)) { + return generated; + } + } + + return generateId(32); +} + +async function getTelemetryContext(): Promise { + // Initialize the context if it hasn't been initialized yet + // (e.g. postinstall not run) + if (!telemetryContext.config.isInitialized) { + telemetryContext.traits.anonymousId = getAnonymousId(); + telemetryContext.traits.sessionId = getSessionId(); + telemetryContext.config.isInitialized = true; + } + + if (!telemetryContext.traits.machineId) { + telemetryContext.traits.machineId = await getMachineId(); + } + + return telemetryContext; +} + +export { TelemetryContextType }; +export default getTelemetryContext; diff --git a/packages/x-telemetry/src/runtime/sender.ts b/packages/x-telemetry/src/runtime/sender.ts new file mode 100644 index 0000000000000..ef33d8ae52d0c --- /dev/null +++ b/packages/x-telemetry/src/runtime/sender.ts @@ -0,0 +1,72 @@ +import type { TelemetryContextType } from './get-context'; +import { getTelemetryEnvConfigValue } from './config'; +import { TelemetryEvent } from '../types'; +import { fetchWithRetry } from './fetcher'; +import * as packageJson from '../../package.json'; + +function shouldSendTelemetry(telemetryContext: TelemetryContextType): boolean { + // Priority to the config (e.g. in code, env) + const envIsCollecting = getTelemetryEnvConfigValue('IS_COLLECTING'); + if (typeof envIsCollecting === 'boolean') { + return envIsCollecting; + } + + // Disable collection of the telemetry in CI builds, + // as it not related to development process + if (telemetryContext.traits.isCI) { + return false; + } + + // Disabled by default + return false; +} + +const sendMuiXTelemetryRetries = 3; + +async function sendMuiXTelemetryEvent(event: TelemetryEvent | null) { + try { + // Disable collection of the telemetry + // in production environment + if (process.env.NODE_ENV === 'production') { + return; + } + + const { default: getTelemetryContext } = await import('./get-context'); + const telemetryContext = await getTelemetryContext(); + if (!event || !shouldSendTelemetry(telemetryContext)) { + return; + } + + const eventPayload = { + ...event, + context: { + ...telemetryContext.traits, + ...event.context, + }, + }; + + if (getTelemetryEnvConfigValue('DEBUG')) { + console.log('[mui-x-telemetry] event', JSON.stringify(eventPayload, null, 2)); + return; + } + + // TODO: batch events and send them in a single request when there will be more + await fetchWithRetry( + 'https://x-telemetry.mui.com/api/v1/telemetry/record', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Telemetry-Client-Version': packageJson.version, + 'X-Telemetry-Node-Env': (process.env.NODE_ENV as any) ?? '', + }, + body: JSON.stringify([eventPayload]), + }, + sendMuiXTelemetryRetries, + ); + } catch (_) { + console.log('[mui-x-telemetry] error', _); + } +} + +export default sendMuiXTelemetryEvent; diff --git a/packages/x-telemetry/src/runtime/settings.ts b/packages/x-telemetry/src/runtime/settings.ts new file mode 100644 index 0000000000000..39c79598308d3 --- /dev/null +++ b/packages/x-telemetry/src/runtime/settings.ts @@ -0,0 +1,15 @@ +import { setTelemetryEnvConfigValue } from './config'; + +const muiXTelemetrySettings = { + enableDebug: () => { + setTelemetryEnvConfigValue('DEBUG', true); + }, + enableTelemetry: () => { + setTelemetryEnvConfigValue('IS_COLLECTING', true); + }, + disableTelemetry: () => { + setTelemetryEnvConfigValue('IS_COLLECTING', false); + }, +}; + +export default muiXTelemetrySettings; diff --git a/packages/x-telemetry/src/runtime/window-storage.ts b/packages/x-telemetry/src/runtime/window-storage.ts new file mode 100644 index 0000000000000..76704165fc597 --- /dev/null +++ b/packages/x-telemetry/src/runtime/window-storage.ts @@ -0,0 +1,36 @@ +type WindowStorageType = 'localStorage' | 'sessionStorage'; + +const prefix = '__mui_x_telemetry_'; + +function getStorageKey(key: string): string { + return prefix + btoa(key); +} + +export function setWindowStorageItem(type: WindowStorageType, key: string, value: string): boolean { + try { + if (typeof window !== 'undefined' && window[type]) { + window[type].setItem(getStorageKey(key), value); + return true; + } + } catch (_) { + // Storage is unavailable, skip it + } + + return false; +} + +export function getWindowStorageItem(type: WindowStorageType, key: string): string | null { + try { + if (typeof window !== 'undefined' && window[type]) { + return window[type].getItem(getStorageKey(key)); + } + } catch (_) { + // Storage is unavailable, skip it + } + + return null; +} + +export function isWindowStorageAvailable(type: WindowStorageType): boolean { + return typeof window !== 'undefined' && !!window[type]; +} diff --git a/packages/x-telemetry/src/types.ts b/packages/x-telemetry/src/types.ts new file mode 100644 index 0000000000000..7475dce5ef9f4 --- /dev/null +++ b/packages/x-telemetry/src/types.ts @@ -0,0 +1,9 @@ +export interface TelemetryEventContext { + licenseKey?: string; +} + +export interface TelemetryEvent { + eventName: string; + payload: Record; + context: TelemetryEventContext; +} diff --git a/packages/x-telemetry/tsconfig.build.json b/packages/x-telemetry/tsconfig.build.json new file mode 100644 index 0000000000000..6571cf43751bb --- /dev/null +++ b/packages/x-telemetry/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + // This config is for emitting declarations (.d.ts) only + // Actual .ts source files are transpiled via babel + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "declaration": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "build", + "rootDir": "./src" + }, + "include": ["src/**/*.ts*", "src/compiled/**/*.js*"], + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] +} diff --git a/packages/x-telemetry/tsconfig.json b/packages/x-telemetry/tsconfig.json new file mode 100644 index 0000000000000..f2c43c4db2af0 --- /dev/null +++ b/packages/x-telemetry/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["@mui/internal-test-utils/initMatchers", "chai-dom", "mocha", "node"] + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59e8cf3325729..6649b0062df47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1419,6 +1419,9 @@ importers: '@mui/x-internals': specifier: workspace:* version: link:../x-internals/build + '@mui/x-telemetry': + specifier: workspace:* + version: link:../x-telemetry/build devDependencies: '@mui/internal-test-utils': specifier: ^2.0.2 @@ -1434,6 +1437,41 @@ importers: version: 6.0.1 publishDirectory: build + packages/x-telemetry: + dependencies: + '@babel/runtime': + specifier: ^7.26.9 + version: 7.26.9 + '@fingerprintjs/fingerprintjs': + specifier: ^4.6.0 + version: 4.6.0 + '@mui/utils': + specifier: ^5.16.6 || ^6.0.0 + version: 5.16.14(@types/react@19.0.10)(react@19.0.0) + ci-info: + specifier: ^4.0.0 + version: 4.1.0 + conf: + specifier: ^5.0.0 + version: 5.0.0 + is-docker: + specifier: ^2.2.1 + version: 2.2.1 + node-machine-id: + specifier: ^1.1.12 + version: 1.1.12 + devDependencies: + '@mui/internal-test-utils': + specifier: ^2.0.2 + version: 2.0.2(@babel/core@7.26.9)(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@types/device-uuid': + specifier: ^1.0.3 + version: 1.0.3 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + publishDirectory: build + packages/x-tree-view: dependencies: '@babel/runtime': @@ -2923,6 +2961,9 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} + '@fingerprintjs/fingerprintjs@4.6.0': + resolution: {integrity: sha512-g2z4lF2saGxVT+AQSmJhPSwW/hBn8vnFJMW6UYOMl9ipJT7re0RZbr2+lB2eCZj/lJ89wWc21FMA14v9iOKroQ==} + '@floating-ui/core@1.6.8': resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} @@ -4177,6 +4218,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/device-uuid@1.0.3': + resolution: {integrity: sha512-z1FXOPDVE85XSjOFtTYXPm4Hyvi1ByjkndfdwU3cBNYGKfYhL4WN6qwYfvMSUNz8j7QGeO+mLJYMfgiyRywzhg==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} @@ -5338,6 +5382,10 @@ packages: engines: {node: '>=18'} hasBin: true + conf@5.0.0: + resolution: {integrity: sha512-lRNyt+iRD4plYaOSVTxu1zPWpaH0EOxgFIR1l3mpC/DGZ7XzhoGFMKmbl54LAgXcSu6knqWgOwdINkqm58N85A==} + engines: {node: '>=8'} + confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} @@ -7351,6 +7399,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -11877,6 +11928,10 @@ snapshots: '@fastify/busboy@2.1.1': {} + '@fingerprintjs/fingerprintjs@4.6.0': + dependencies: + tslib: 2.8.1 + '@floating-ui/core@1.6.8': dependencies: '@floating-ui/utils': 0.2.8 @@ -13364,6 +13419,8 @@ snapshots: dependencies: '@types/ms': 0.7.34 + '@types/device-uuid@1.0.3': {} + '@types/doctrine@0.0.9': {} '@types/eslint-scope@3.7.7': @@ -14728,6 +14785,16 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + conf@5.0.0: + dependencies: + ajv: 6.12.6 + dot-prop: 5.3.0 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + make-dir: 3.1.0 + pkg-up: 3.1.0 + write-file-atomic: 3.0.3 + confusing-browser-globals@1.0.11: {} connect@3.7.0: @@ -17130,6 +17197,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@7.0.3: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-nice@1.1.4: {} diff --git a/scripts/x-license.exports.json b/scripts/x-license.exports.json index d8d94c55e3ed1..70da8f9f63251 100644 --- a/scripts/x-license.exports.json +++ b/scripts/x-license.exports.json @@ -8,6 +8,7 @@ { "name": "LicenseStatus", "kind": "TypeAlias" }, { "name": "MuiCommercialPackageName", "kind": "TypeAlias" }, { "name": "MuiLicenseInfo", "kind": "Interface" }, + { "name": "muiXTelemetrySettings", "kind": "Variable" }, { "name": "PlanScope", "kind": "TypeAlias" }, { "name": "showExpiredAnnualGraceLicenseKeyError", "kind": "Function" }, { "name": "showExpiredAnnualLicenseKeyError", "kind": "Function" }, diff --git a/tsconfig.json b/tsconfig.json index a83b65a5e4b7e..ed9328c304d56 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,8 @@ "@mui/x-license/*": ["./packages/x-license/src/*"], "@mui/x-internals": ["./packages/x-internals/src"], "@mui/x-internals/*": ["./packages/x-internals/src/*"], + "@mui/x-telemetry": ["./packages/x-telemetry/src"], + "@mui/x-telemetry/*": ["./packages/x-telemetry/src/*"], "@mui/docs": ["./node_modules/@mui/monorepo/packages/mui-docs/src"], "@mui/docs/*": ["./node_modules/@mui/monorepo/packages/mui-docs/src/*"], "@mui-internal/api-docs-builder": ["./node_modules/@mui/monorepo/packages/api-docs-builder"], diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js index bfbf7530bafa8..fccc311ffd066 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -20,6 +20,7 @@ module.exports = { '@mui/x-tree-view': path.resolve(__dirname, './packages/x-tree-view/src'), '@mui/x-tree-view-pro': path.resolve(__dirname, './packages/x-tree-view-pro/src'), '@mui/x-license': path.resolve(__dirname, './packages/x-license/src'), + '@mui/x-telemetry': path.resolve(__dirname, './packages/x-telemetry/src'), '@mui/x-internals': path.resolve(__dirname, './packages/x-internals/src'), '@mui/material-nextjs': path.resolve( __dirname,