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,