aboutsummaryrefslogtreecommitdiff
path: root/packages/telemetry
diff options
context:
space:
mode:
Diffstat (limited to 'packages/telemetry')
-rw-r--r--packages/telemetry/CHANGELOG.md248
-rw-r--r--packages/telemetry/README.md17
-rw-r--r--packages/telemetry/package.json53
-rw-r--r--packages/telemetry/src/config-keys.ts8
-rw-r--r--packages/telemetry/src/config.ts88
-rw-r--r--packages/telemetry/src/index.ts172
-rw-r--r--packages/telemetry/src/post.ts9
-rw-r--r--packages/telemetry/src/project-info.ts116
-rw-r--r--packages/telemetry/src/system-info.ts78
-rw-r--r--packages/telemetry/test/config.test.js10
-rw-r--r--packages/telemetry/test/index.test.js84
-rw-r--r--packages/telemetry/tsconfig.json7
12 files changed, 890 insertions, 0 deletions
diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md
new file mode 100644
index 000000000..3824ff52b
--- /dev/null
+++ b/packages/telemetry/CHANGELOG.md
@@ -0,0 +1,248 @@
+# @astrojs/telemetry
+
+## 3.3.0
+
+### Minor Changes
+
+- [#13809](https://github.com/withastro/astro/pull/13809) [`3c3b492`](https://github.com/withastro/astro/commit/3c3b492375bd6a63f1fb6cede3685aff999be3c9) Thanks [@ascorbic](https://github.com/ascorbic)! - Increases minimum Node.js version to 18.20.8
+
+ Node.js 18 has now reached end-of-life and should not be used. For now, Astro will continue to support Node.js 18.20.8, which is the final LTS release of Node.js 18, as well as Node.js 20 and Node.js 22 or later. We will drop support for Node.js 18 in a future release, so we recommend upgrading to Node.js 22 as soon as possible. See Astro's [Node.js support policy](https://docs.astro.build/en/upgrade-astro/#support) for more details.
+
+ :warning: **Important note for users of Cloudflare Pages**: The current build image for Cloudflare Pages uses Node.js 18.17.1 by default, which is no longer supported by Astro. If you are using Cloudflare Pages you should [override the default Node.js version](https://developers.cloudflare.com/pages/configuration/build-image/#override-default-versions) to Node.js 22. This does not affect users of Cloudflare Workers, which uses Node.js 22 by default.
+
+## 3.2.1
+
+### Patch Changes
+
+- [#13591](https://github.com/withastro/astro/pull/13591) [`5dd2d3f`](https://github.com/withastro/astro/commit/5dd2d3fde8a138ed611dedf39ffa5dfeeed315f8) Thanks [@florian-lefebvre](https://github.com/florian-lefebvre)! - Removes unused code
+
+## 3.2.0
+
+### Minor Changes
+
+- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support
+
+## 3.2.0-beta.0
+
+### Minor Changes
+
+- [#12539](https://github.com/withastro/astro/pull/12539) [`827093e`](https://github.com/withastro/astro/commit/827093e6175549771f9d93ddf3f2be4c2c60f0b7) Thanks [@bluwy](https://github.com/bluwy)! - Drops node 21 support
+
+## 3.1.0
+
+### Minor Changes
+
+- [#10689](https://github.com/withastro/astro/pull/10689) [`683d51a5eecafbbfbfed3910a3f1fbf0b3531b99`](https://github.com/withastro/astro/commit/683d51a5eecafbbfbfed3910a3f1fbf0b3531b99) Thanks [@ematipico](https://github.com/ematipico)! - Deprecate support for versions of Node.js older than `v18.17.1` for Node.js 18, older than `v20.0.3` for Node.js 20, and the complete Node.js v19 release line.
+
+ This change is in line with Astro's [Node.js support policy](https://docs.astro.build/en/upgrade-astro/#support).
+
+## 3.0.4
+
+### Patch Changes
+
+- [#8900](https://github.com/withastro/astro/pull/8900) [`341ef6578`](https://github.com/withastro/astro/commit/341ef6578528a00f89bf6da5e4243b0fde272816) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Track if the Astro CLI is running in a [`TTY`](nodejs.org/api/process.html#a-note-on-process-io) context.
+
+ This information helps us better understand scripted use of Astro vs. direct terminal use of Astro CLI by a user, especially the `astro dev` command.
+
+## 3.0.3
+
+### Patch Changes
+
+- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI
+
+- [#8729](https://github.com/withastro/astro/pull/8729) [`21e0757ea`](https://github.com/withastro/astro/commit/21e0757ea22a57d344c934045ca19db93b684436) Thanks [@lilnasy](https://github.com/lilnasy)! - Removed an unnecessary dependency.
+
+## 3.0.2
+
+### Patch Changes
+
+- [#8600](https://github.com/withastro/astro/pull/8600) [`ed54d4644`](https://github.com/withastro/astro/commit/ed54d46449accc99ad117d6b0d50a8905e4d65d7) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Improve config info telemetry
+
+## 3.0.1
+
+### Patch Changes
+
+- [#8363](https://github.com/withastro/astro/pull/8363) [`0ce0720c7`](https://github.com/withastro/astro/commit/0ce0720c7f2c7ba21dddfea0b75d1e9b39c6a274) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Wrap `JSON.parse` in `try`/`catch`
+
+## 3.0.0
+
+### Major Changes
+
+- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+### Patch Changes
+
+- [#8234](https://github.com/withastro/astro/pull/8234) [`0c7b42dc6`](https://github.com/withastro/astro/commit/0c7b42dc6780e687e416137539f55a3a427d1d10) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update telemetry notice
+
+- [#8130](https://github.com/withastro/astro/pull/8130) [`3e834293d`](https://github.com/withastro/astro/commit/3e834293d47ab2761a7aa013916e8371871efb7f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add some polyfills for Stackblitz until they support Node 18. Running Astro on Node 16 is still not officially supported, however.
+
+- [#8188](https://github.com/withastro/astro/pull/8188) [`b675acb2a`](https://github.com/withastro/astro/commit/b675acb2aa820448e9c0d363339a37fbac873215) Thanks [@ematipico](https://github.com/ematipico)! - Remove undici dependency
+
+## 3.0.0-rc.4
+
+### Patch Changes
+
+- [#8234](https://github.com/withastro/astro/pull/8234) [`0c7b42dc6`](https://github.com/withastro/astro/commit/0c7b42dc6780e687e416137539f55a3a427d1d10) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update telemetry notice
+
+## 3.0.0-rc.3
+
+### Major Changes
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+## 3.0.0-beta.2
+
+### Patch Changes
+
+- [#8130](https://github.com/withastro/astro/pull/8130) [`3e834293d`](https://github.com/withastro/astro/commit/3e834293d47ab2761a7aa013916e8371871efb7f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add some polyfills for Stackblitz until they support Node 18. Running Astro on Node 16 is still not officially supported, however.
+
+## 3.0.0-beta.1
+
+### Patch Changes
+
+- [#7952](https://github.com/withastro/astro/pull/7952) [`b675acb2a`](https://github.com/withastro/astro/commit/b675acb2aa820448e9c0d363339a37fbac873215) Thanks [@astrobot-houston](https://github.com/astrobot-houston)! - Remove undici dependency
+
+## 3.0.0-beta.0
+
+### Major Changes
+
+- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+## 2.1.1
+
+### Patch Changes
+
+- [#6929](https://github.com/withastro/astro/pull/6929) [`ac57b5549`](https://github.com/withastro/astro/commit/ac57b5549f828a17bdbebdaca7ace075307a3c9d) Thanks [@bluwy](https://github.com/bluwy)! - Upgrade undici to v5.22.0
+
+## 2.1.0
+
+### Minor Changes
+
+- [#6213](https://github.com/withastro/astro/pull/6213) [`afbbc4d5b`](https://github.com/withastro/astro/commit/afbbc4d5bfafc1779bac00b41c2a1cb1c90f2808) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Updated compilation settings to disable downlevelling for Node 14
+
+## 2.0.1
+
+### Patch Changes
+
+- [#6355](https://github.com/withastro/astro/pull/6355) [`5aa6580f7`](https://github.com/withastro/astro/commit/5aa6580f775405a4443835bf7eb81f0c65e5aed6) Thanks [@ematipico](https://github.com/ematipico)! - Update `undici` to v5.20.0
+
+## 2.0.0
+
+### Major Changes
+
+- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0
+
+## 2.0.0-beta.0
+
+<details>
+<summary>See changes in 2.0.0-beta.0</summary>
+
+### Major Changes
+
+- [#5782](https://github.com/withastro/astro/pull/5782) [`1f92d64ea`](https://github.com/withastro/astro/commit/1f92d64ea35c03fec43aff64eaf704dc5a9eb30a) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 14. Minimum supported Node version is now >=16.12.0
+
+</details>
+
+## 1.0.1
+
+### Patch Changes
+
+- [#4842](https://github.com/withastro/astro/pull/4842) [`812658ad2`](https://github.com/withastro/astro/commit/812658ad2ab3732a99e35c4fd903e302e723db46) Thanks [@bluwy](https://github.com/bluwy)! - Add missing dependencies, support strict dependency installation (e.g. pnpm)
+
+## 1.0.0
+
+### Major Changes
+
+- [`04ad44563`](https://github.com/withastro/astro/commit/04ad445632c67bdd60c1704e1e0dcbcaa27b9308) - > Astro v1.0 is out! Read the [official announcement post](https://astro.build/blog/astro-1/).
+
+ **No breaking changes**. This package is now officially stable and compatible with `astro@1.0.0`!
+
+## 0.4.1
+
+### Patch Changes
+
+- [#3937](https://github.com/withastro/astro/pull/3937) [`31f9c0bf0`](https://github.com/withastro/astro/commit/31f9c0bf029ffa4b470e620f2c32e1370643e81e) Thanks [@delucis](https://github.com/delucis)! - Roll back supported Node engines
+
+## 0.4.0
+
+### Minor Changes
+
+- [#3914](https://github.com/withastro/astro/pull/3914) [`b48767985`](https://github.com/withastro/astro/commit/b48767985359bd359df8071324952ea5f2bc0d86) Thanks [@ran-dall](https://github.com/ran-dall)! - Rollback supported `node@16` version. Minimum versions are now `node@14.20.0` or `node@16.14.0`.
+
+## 0.3.1
+
+### Patch Changes
+
+- [#3898](https://github.com/withastro/astro/pull/3898) [`c4f6fdf37`](https://github.com/withastro/astro/commit/c4f6fdf3722b9bc2192cab735498f4e0c30c982e) Thanks [@leader22](https://github.com/leader22)! - Remove unused dependencies
+
+## 0.3.0
+
+### Minor Changes
+
+- [#3871](https://github.com/withastro/astro/pull/3871) [`1cc5b7890`](https://github.com/withastro/astro/commit/1cc5b78905633608e5b07ad291f916f54e67feb1) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Update supported `node` versions. Minimum versions are now `node@14.20.0` or `node@16.16.0`.
+
+## 0.2.5
+
+### Patch Changes
+
+- [#3847](https://github.com/withastro/astro/pull/3847) [`eedb32c7`](https://github.com/withastro/astro/commit/eedb32c79716a8e04acd46cb2c74c5af112e016f) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Detect package manager, improve types
+
+## 0.2.4
+
+### Patch Changes
+
+- [#3822](https://github.com/withastro/astro/pull/3822) [`e4b2dca1`](https://github.com/withastro/astro/commit/e4b2dca1f3f03bd951f1d623695631cebf638c67) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Fix an issue where handled error output was piped to the user
+
+## 0.2.3
+
+### Patch Changes
+
+- [#3677](https://github.com/withastro/astro/pull/3677) [`8045c8ad`](https://github.com/withastro/astro/commit/8045c8ade16fe4306448b7f98a4560ef0557d378) Thanks [@Jutanium](https://github.com/Jutanium)! - Update READMEs
+
+## 0.2.2
+
+### Patch Changes
+
+- [#3750](https://github.com/withastro/astro/pull/3750) [`dd176ca5`](https://github.com/withastro/astro/commit/dd176ca58d9ce8ab757075491568a014c0943de2) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Add basic error reporting to astro telemetry
+
+## 0.2.1
+
+### Patch Changes
+
+- [#3753](https://github.com/withastro/astro/pull/3753) [`cabd9dcc`](https://github.com/withastro/astro/commit/cabd9dcc8079b55bf16bf05da53bd36f41b7f766) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Fix issue where project id fallback was not getting hashed
+
+## 0.2.0
+
+### Minor Changes
+
+- [#3713](https://github.com/withastro/astro/pull/3713) [`ebd7e7ad`](https://github.com/withastro/astro/commit/ebd7e7ad81e5245deffa331f11e5196ff1b21d84) Thanks [@FredKSchott](https://github.com/FredKSchott)! - Update telemetry to support a more anonymized project id. `anonymousProjectId` is now hashed based on anonymous git data instead of your git remote URL.
+
+## 0.1.3
+
+### Patch Changes
+
+- [#3614](https://github.com/withastro/astro/pull/3614) [`9c8a7c0b`](https://github.com/withastro/astro/commit/9c8a7c0b09db2fb6925929d4efe01d5ececbf08e) Thanks [@okikio](https://github.com/okikio)! - Fix telemetry crashing astro build/dev when using optional integrations
+
+ Telemetry will now ignore falsy integration values but will gather a count of how many integrations out of the total are now optional integrations
+
+* [#3614](https://github.com/withastro/astro/pull/3614) [`9c8a7c0b`](https://github.com/withastro/astro/commit/9c8a7c0b09db2fb6925929d4efe01d5ececbf08e) Thanks [@okikio](https://github.com/okikio)! - Add's optional integrations field to `@astrojs/telemetry`'s payload
+
+## 0.1.2
+
+### Patch Changes
+
+- [#3299](https://github.com/withastro/astro/pull/3299) [`8021998b`](https://github.com/withastro/astro/commit/8021998bb6011e31aa736abeafa4f1cf8f5a180c) Thanks [@matthewp](https://github.com/matthewp)! - Update to telemetry to include AstroConfig keys used
+
+## 0.1.1
+
+### Patch Changes
+
+- [#3276](https://github.com/withastro/astro/pull/3276) [`6d5ef41b`](https://github.com/withastro/astro/commit/6d5ef41b1ed77ccc67f71e91adeab63a50a494a8) Thanks [@FredKSchott](https://github.com/FredKSchott)! - fix "cannot exit astro" bug
+
+## 0.1.0
+
+### Minor Changes
+
+- [#3256](https://github.com/withastro/astro/pull/3256) [`f76038ac`](https://github.com/withastro/astro/commit/f76038ac7db986a13701fd316e53142b52e011c8) Thanks [@matthewp](https://github.com/matthewp)! - Adds anonymous telemetry data to the cli
diff --git a/packages/telemetry/README.md b/packages/telemetry/README.md
new file mode 100644
index 000000000..c9dc896fc
--- /dev/null
+++ b/packages/telemetry/README.md
@@ -0,0 +1,17 @@
+# Astro Telemetry
+
+This package is used to collect anonymous telemetry data within the Astro CLI.
+
+It can be disabled in Astro using either method documented below:
+
+```shell
+# Option 1: Run this to disable telemetry globally across your entire machine.
+astro telemetry disable
+```
+
+```shell
+# Option 2: The ASTRO_TELEMETRY_DISABLED environment variable disables telemetry when set.
+ASTRO_TELEMETRY_DISABLED=1 astro dev
+```
+
+Visit https://astro.build/telemetry/ for more information about our approach to anonymous telemetry in Astro.
diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json
new file mode 100644
index 000000000..17ce82aee
--- /dev/null
+++ b/packages/telemetry/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@astrojs/telemetry",
+ "version": "3.3.0",
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/withastro/astro.git",
+ "directory": "packages/telemetry"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "homepage": "https://astro.build",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "scripts": {
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc",
+ "build:ci": "astro-scripts build \"src/**/*.ts\"",
+ "dev": "astro-scripts dev \"src/**/*.ts\"",
+ "test": "astro-scripts test \"test/**/*.test.js\""
+ },
+ "files": [
+ "dist"
+ ],
+ "dependencies": {
+ "ci-info": "^4.2.0",
+ "debug": "^4.4.0",
+ "dlv": "^1.1.3",
+ "dset": "^3.1.4",
+ "is-docker": "^3.0.0",
+ "is-wsl": "^3.1.0",
+ "which-pm-runs": "^1.1.0"
+ },
+ "devDependencies": {
+ "@types/debug": "^4.1.12",
+ "@types/dlv": "^1.1.5",
+ "@types/node": "^18.17.8",
+ "@types/which-pm-runs": "^1.0.2",
+ "astro-scripts": "workspace:*"
+ },
+ "engines": {
+ "node": "18.20.8 || ^20.3.0 || >=22.0.0"
+ },
+ "publishConfig": {
+ "provenance": true
+ }
+}
diff --git a/packages/telemetry/src/config-keys.ts b/packages/telemetry/src/config-keys.ts
new file mode 100644
index 000000000..932e602e2
--- /dev/null
+++ b/packages/telemetry/src/config-keys.ts
@@ -0,0 +1,8 @@
+// Global Config Keys
+
+/** Specifies whether or not telemetry is enabled or disabled. */
+export const TELEMETRY_ENABLED = 'telemetry.enabled';
+/** Specifies when the user was informed of anonymous telemetry. */
+export const TELEMETRY_NOTIFY_DATE = 'telemetry.notifiedAt';
+/** Specifies an anonymous identifier used to dedupe events for a user. */
+export const TELEMETRY_ID = `telemetry.anonymousId`;
diff --git a/packages/telemetry/src/config.ts b/packages/telemetry/src/config.ts
new file mode 100644
index 000000000..359b1e11f
--- /dev/null
+++ b/packages/telemetry/src/config.ts
@@ -0,0 +1,88 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import process from 'node:process';
+import dget from 'dlv';
+import { dset } from 'dset';
+
+interface ConfigOptions {
+ name: string;
+}
+
+// Adapted from https://github.com/sindresorhus/env-paths
+function getConfigDir(name: string) {
+ const homedir = os.homedir();
+ const macos = () => path.join(homedir, 'Library', 'Preferences', name);
+ const win = () => {
+ const { APPDATA = path.join(homedir, 'AppData', 'Roaming') } = process.env;
+ return path.join(APPDATA, name, 'Config');
+ };
+ const linux = () => {
+ const { XDG_CONFIG_HOME = path.join(homedir, '.config') } = process.env;
+ return path.join(XDG_CONFIG_HOME, name);
+ };
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+ switch (process.platform) {
+ case 'darwin':
+ return macos();
+ case 'win32':
+ return win();
+ default:
+ return linux();
+ }
+}
+
+export class GlobalConfig {
+ private dir: string;
+ private file: string;
+
+ constructor(private project: ConfigOptions) {
+ this.dir = getConfigDir(this.project.name);
+ this.file = path.join(this.dir, 'config.json');
+ }
+
+ private _store?: Record<string, any>;
+ private get store(): Record<string, any> {
+ if (this._store) return this._store;
+ this.ensureDir();
+ if (fs.existsSync(this.file)) {
+ try {
+ this._store = JSON.parse(fs.readFileSync(this.file).toString());
+ } catch {}
+ }
+ if (!this._store) {
+ this._store = {};
+ this.write();
+ }
+ return this._store;
+ }
+ private set store(value: Record<string, any>) {
+ this._store = value;
+ this.write();
+ }
+ private ensureDir() {
+ fs.mkdirSync(this.dir, { recursive: true });
+ }
+ write() {
+ fs.writeFileSync(this.file, JSON.stringify(this.store, null, '\t'));
+ }
+ clear(): void {
+ this.store = {};
+ fs.rmSync(this.file, { recursive: true });
+ }
+ delete(key: string): boolean {
+ dset(this.store, key, undefined);
+ this.write();
+ return true;
+ }
+ get(key: string): any {
+ return dget(this.store, key);
+ }
+ has(key: string): boolean {
+ return typeof this.get(key) !== 'undefined';
+ }
+ set(key: string, value: any): void {
+ dset(this.store, key, value);
+ this.write();
+ }
+}
diff --git a/packages/telemetry/src/index.ts b/packages/telemetry/src/index.ts
new file mode 100644
index 000000000..ba7cea108
--- /dev/null
+++ b/packages/telemetry/src/index.ts
@@ -0,0 +1,172 @@
+import { randomBytes } from 'node:crypto';
+import { isCI } from 'ci-info';
+import debug from 'debug';
+import * as KEY from './config-keys.js';
+import { GlobalConfig } from './config.js';
+import { post } from './post.js';
+import { type ProjectInfo, getProjectInfo } from './project-info.js';
+import { type SystemInfo, getSystemInfo } from './system-info.js';
+
+export type AstroTelemetryOptions = { astroVersion: string; viteVersion: string };
+export type TelemetryEvent = { eventName: string; payload: Record<string, any> };
+
+// In the event of significant policy changes, update this!
+const VALID_TELEMETRY_NOTICE_DATE = '2023-08-25';
+
+type EventMeta = SystemInfo;
+interface EventContext extends ProjectInfo {
+ anonymousId: string;
+ anonymousSessionId: string;
+}
+export class AstroTelemetry {
+ private _anonymousSessionId: string | undefined;
+ private _anonymousProjectInfo: ProjectInfo | undefined;
+ private config = new GlobalConfig({ name: 'astro' });
+ private debug = debug('astro:telemetry');
+ private isCI = isCI;
+ private env = process.env;
+
+ private get astroVersion() {
+ return this.opts.astroVersion;
+ }
+ private get viteVersion() {
+ return this.opts.viteVersion;
+ }
+ private get ASTRO_TELEMETRY_DISABLED() {
+ return this.env.ASTRO_TELEMETRY_DISABLED;
+ }
+ private get TELEMETRY_DISABLED() {
+ return this.env.TELEMETRY_DISABLED;
+ }
+
+ constructor(private opts: AstroTelemetryOptions) {
+ // TODO: When the process exits, flush any queued promises
+ // This caused a "cannot exist astro" error when it ran, so it was removed.
+ // process.on('SIGINT', () => this.flush());
+ }
+
+ /**
+ * Get value from either the global config or the provided fallback.
+ * If value is not set, the fallback is saved to the global config,
+ * persisted for later sessions.
+ */
+ private getConfigWithFallback<T>(key: string, getValue: () => T): T {
+ const currentValue = this.config.get(key);
+ if (currentValue !== undefined) {
+ return currentValue;
+ }
+ const newValue = getValue();
+ this.config.set(key, newValue);
+ return newValue;
+ }
+
+ private get enabled(): boolean {
+ return this.getConfigWithFallback(KEY.TELEMETRY_ENABLED, () => true);
+ }
+
+ private get notifyDate(): string {
+ return this.getConfigWithFallback(KEY.TELEMETRY_NOTIFY_DATE, () => '');
+ }
+
+ private get anonymousId(): string {
+ return this.getConfigWithFallback(KEY.TELEMETRY_ID, () => randomBytes(32).toString('hex'));
+ }
+
+ private get anonymousSessionId(): string {
+ // NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
+ this._anonymousSessionId = this._anonymousSessionId || randomBytes(32).toString('hex');
+ return this._anonymousSessionId;
+ }
+
+ private get anonymousProjectInfo(): ProjectInfo {
+ // NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
+ this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(this.isCI);
+ return this._anonymousProjectInfo;
+ }
+
+ private get isDisabled(): boolean {
+ if (Boolean(this.ASTRO_TELEMETRY_DISABLED || this.TELEMETRY_DISABLED)) {
+ return true;
+ }
+ return this.enabled === false;
+ }
+
+ setEnabled(value: boolean) {
+ this.config.set(KEY.TELEMETRY_ENABLED, value);
+ }
+
+ clear() {
+ return this.config.clear();
+ }
+
+ isValidNotice() {
+ if (!this.notifyDate) return false;
+ const current = Number(this.notifyDate);
+ const valid = new Date(VALID_TELEMETRY_NOTICE_DATE).valueOf();
+
+ return current > valid;
+ }
+
+ async notify(callback: () => boolean | Promise<boolean>) {
+ if (this.isDisabled || this.isCI) {
+ this.debug(`[notify] telemetry has been disabled`);
+ return;
+ }
+ // The end-user has already been notified about our telemetry integration!
+ // Don't bother them about it again.
+ if (this.isValidNotice()) {
+ this.debug(`[notify] last notified on ${this.notifyDate}`);
+ return;
+ }
+ const enabled = await callback();
+ this.config.set(KEY.TELEMETRY_NOTIFY_DATE, new Date().valueOf().toString());
+ this.config.set(KEY.TELEMETRY_ENABLED, enabled);
+ this.debug(`[notify] telemetry has been ${enabled ? 'enabled' : 'disabled'}`);
+ }
+
+ async record(event: TelemetryEvent | TelemetryEvent[] = []) {
+ const events: TelemetryEvent[] = Array.isArray(event) ? event : [event];
+ if (events.length < 1) {
+ return Promise.resolve();
+ }
+
+ // Skip recording telemetry if the feature is disabled
+ if (this.isDisabled) {
+ this.debug('[record] telemetry has been disabled');
+ return Promise.resolve();
+ }
+
+ const meta: EventMeta = {
+ ...getSystemInfo({ astroVersion: this.astroVersion, viteVersion: this.viteVersion }),
+ };
+
+ const context: EventContext = {
+ ...this.anonymousProjectInfo,
+ anonymousId: this.anonymousId,
+ anonymousSessionId: this.anonymousSessionId,
+ };
+
+ // Every CI session also creates a new user, which blows up telemetry.
+ // To solve this, we track all CI runs under a single "CI" anonymousId.
+ if (meta.isCI) {
+ context.anonymousId = `CI.${meta.ciName || 'UNKNOWN'}`;
+ }
+
+ if (this.debug.enabled) {
+ // Print to standard error to simplify selecting the output
+ this.debug({ context, meta });
+ this.debug(JSON.stringify(events, null, 2));
+ // Do not send the telemetry data if debugging. Users may use this feature
+ // to preview what data would be sent.
+ return Promise.resolve();
+ }
+ return post({
+ context,
+ meta,
+ events,
+ }).catch((err) => {
+ // Log the error to the debugger, but otherwise do nothing.
+ this.debug(`Error sending event: ${err.message}`);
+ });
+ }
+}
diff --git a/packages/telemetry/src/post.ts b/packages/telemetry/src/post.ts
new file mode 100644
index 000000000..6aef03bc9
--- /dev/null
+++ b/packages/telemetry/src/post.ts
@@ -0,0 +1,9 @@
+const ASTRO_TELEMETRY_ENDPOINT = `https://telemetry.astro.build/api/v1/record`;
+
+export function post(body: Record<string, any>): Promise<any> {
+ return fetch(ASTRO_TELEMETRY_ENDPOINT, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: { 'content-type': 'application/json' },
+ });
+}
diff --git a/packages/telemetry/src/project-info.ts b/packages/telemetry/src/project-info.ts
new file mode 100644
index 000000000..79b9e4f44
--- /dev/null
+++ b/packages/telemetry/src/project-info.ts
@@ -0,0 +1,116 @@
+import { execSync } from 'node:child_process';
+import type { BinaryLike } from 'node:crypto';
+import { createHash } from 'node:crypto';
+import detectPackageManager from 'which-pm-runs';
+
+/**
+ * Astro Telemetry -- Project Info
+ *
+ * To better understand our telemetry insights, Astro attempts to create an anonymous identifier
+ * for each Astro project. This value is meant to be unique to each project but common across
+ * multiple different users on the same project.
+ *
+ * To do this, we generate a unique, anonymous hash from your working git repository data. This is
+ * ideal because git data is shared across all users on the same repository, but the data itself
+ * that we generate our hash from does not contain any personal or otherwise identifying information.
+ *
+ * We do not use your repository's remote URL, GitHub URL, or any other personally identifying
+ * information to generate the project identifier hash. In this way it is almost completely anonymous.
+ *
+ * If you are running Astro outside of a git repository, then we will generate a unique, anonymous project
+ * identifier by hashing your project's file path on your machine.
+ *
+ * ~~~
+ *
+ * Q: Can this project identifier be traced back to me?
+ *
+ * A: If your repository is private, there is no way for anyone to trace your unique
+ * project identifier back to you, your organization, or your project. This is because it is itself
+ * a hash of a commit hash, and a commit hash does not include any identifying information.
+ *
+ * If your repository is publicly available, then it is possible for someone to generate this unique
+ * project identifier themselves by cloning your repo. Specifically, someone would need access to run
+ * the `git rev-list` command below to generate this hash. Without this access, it is impossible to
+ * trace the project identifier back to you or your project.
+ *
+ * If you are running Astro outside of a git repository, then the project identifier could be matched
+ * back to the exact file path on your machine. It is unlikely (but still possible) for this to happen
+ * without access to your machine or knowledge of your machine's file system.
+ *
+ * ~~~
+ *
+ * Q: I don't want Astro to collect a project identifier. How can I disable it?
+ *
+ * A: You can disable telemetry completely at any time by running `astro telemetry disable`. There is
+ * currently no way to disable just this identifier while keeping the rest of telemetry enabled.
+ */
+
+export interface ProjectInfo {
+ /* Your unique project identifier. This will be hashed again before sending. */
+ anonymousProjectId: string | undefined;
+ /* true if your project is connected to a git repository. false otherwise. */
+ isGit: boolean;
+ /* The package manager used to run Astro */
+ packageManager: string | undefined;
+ /* The version of the package manager used to run Astro */
+ packageManagerVersion: string | undefined;
+}
+
+function createAnonymousValue(payload: BinaryLike): string {
+ // We use empty string to represent an empty value. Avoid hashing this
+ // since that would create a real hash and remove its "empty" meaning.
+ if (payload === '') {
+ return payload;
+ }
+ // Otherwise, create a new hash from the payload and return it.
+ const hash = createHash('sha256');
+ hash.update(payload);
+ return hash.digest('hex');
+}
+
+function getProjectIdFromGit(): string | null {
+ try {
+ const originBuffer = execSync(`git rev-list --max-parents=0 HEAD`, {
+ timeout: 500,
+ stdio: ['ignore', 'pipe', 'ignore'],
+ });
+ return String(originBuffer).trim();
+ } catch (_) {
+ return null;
+ }
+}
+
+function getProjectId(isCI: boolean): Pick<ProjectInfo, 'anonymousProjectId' | 'isGit'> {
+ const projectIdFromGit = getProjectIdFromGit();
+ if (projectIdFromGit) {
+ return {
+ isGit: true,
+ anonymousProjectId: createAnonymousValue(projectIdFromGit),
+ };
+ }
+ // If we're running in CI, the current working directory is not unique.
+ // If the cwd is a single level deep (ex: '/app'), it's probably not unique.
+ const cwd = process.cwd();
+ const isCwdGeneric = (cwd.match(/[/|\\]/g) || []).length === 1;
+ if (isCI || isCwdGeneric) {
+ return {
+ isGit: false,
+ anonymousProjectId: undefined,
+ };
+ }
+ return {
+ isGit: false,
+ anonymousProjectId: createAnonymousValue(cwd),
+ };
+}
+
+export function getProjectInfo(isCI: boolean): ProjectInfo {
+ const projectId = getProjectId(isCI);
+ const packageManager = detectPackageManager();
+ return {
+ ...projectId,
+ packageManager: packageManager?.name,
+ packageManagerVersion: packageManager?.version,
+ };
+}
+//
diff --git a/packages/telemetry/src/system-info.ts b/packages/telemetry/src/system-info.ts
new file mode 100644
index 000000000..2913b6941
--- /dev/null
+++ b/packages/telemetry/src/system-info.ts
@@ -0,0 +1,78 @@
+import os from 'node:os';
+import { name as ciName, isCI } from 'ci-info';
+import isDocker from 'is-docker';
+import isWSL from 'is-wsl';
+
+/**
+ * Astro Telemetry -- System Info
+ *
+ * To better understand our telemetry insights, Astro collects the following anonymous information
+ * about the system that it runs on. This helps us prioritize fixes and new features based on a
+ * better understanding of our real-world system requirements.
+ *
+ * ~~~
+ *
+ * Q: Can this system info be traced back to me?
+ *
+ * A: No personally identifiable information is contained in the system info that we collect. It could
+ * be possible for someone with direct access to your machine to collect this information themselves
+ * and then attempt to match it all together with our collected telemetry data, however most users'
+ * systems are probably not uniquely identifiable via their system info alone.
+ *
+ * ~~~
+ *
+ * Q: I don't want Astro to collect system info. How can I disable it?
+ *
+ * A: You can disable telemetry completely at any time by running `astro telemetry disable`. There is
+ * currently no way to disable this otherwise while keeping the rest of telemetry enabled.
+ */
+
+export type SystemInfo = {
+ systemPlatform: NodeJS.Platform;
+ systemRelease: string;
+ systemArchitecture: string;
+ astroVersion: string;
+ nodeVersion: string;
+ viteVersion: string;
+ cpuCount: number;
+ cpuModel: string | null;
+ cpuSpeed: number | null;
+ memoryInMb: number;
+ isDocker: boolean;
+ isTTY: boolean;
+ isWSL: boolean;
+ isCI: boolean;
+ ciName: string | null;
+};
+
+let meta: SystemInfo | undefined;
+
+export function getSystemInfo(versions: { viteVersion: string; astroVersion: string }): SystemInfo {
+ if (meta) {
+ return meta;
+ }
+
+ const cpus = os.cpus() || [];
+
+ return {
+ // Version information
+ nodeVersion: process.version.replace(/^v?/, ''),
+ viteVersion: versions.viteVersion,
+ astroVersion: versions.astroVersion,
+ // Software information
+ systemPlatform: os.platform(),
+ systemRelease: os.release(),
+ systemArchitecture: os.arch(),
+ // Machine information
+ cpuCount: cpus.length,
+ cpuModel: cpus.length ? cpus[0].model : null,
+ cpuSpeed: cpus.length ? cpus[0].speed : null,
+ memoryInMb: Math.trunc(os.totalmem() / Math.pow(1024, 2)),
+ // Environment information
+ isDocker: isDocker(),
+ isTTY: process.stdout.isTTY,
+ isWSL,
+ isCI,
+ ciName,
+ };
+}
diff --git a/packages/telemetry/test/config.test.js b/packages/telemetry/test/config.test.js
new file mode 100644
index 000000000..d68663b77
--- /dev/null
+++ b/packages/telemetry/test/config.test.js
@@ -0,0 +1,10 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+import { GlobalConfig } from '../dist/config.js';
+
+describe('GlobalConfig', () => {
+ it('initializes when expected arguments are given', () => {
+ const config = new GlobalConfig({ name: 'TEST_NAME' });
+ assert(config instanceof GlobalConfig);
+ });
+});
diff --git a/packages/telemetry/test/index.test.js b/packages/telemetry/test/index.test.js
new file mode 100644
index 000000000..47d64198c
--- /dev/null
+++ b/packages/telemetry/test/index.test.js
@@ -0,0 +1,84 @@
+import assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import { AstroTelemetry } from '../dist/index.js';
+
+function setup() {
+ const config = new Map();
+ const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
+ const logs = [];
+ // Stub isCI to false so we can test user-facing behavior
+ telemetry.isCI = false;
+ // Stub process.env to properly test in Astro's own CI
+ telemetry.env = {};
+ // Override config so we can inspect it
+ telemetry.config = config;
+ // Override debug so we can inspect it
+ telemetry.debug.enabled = true;
+ telemetry.debug.log = (...args) => logs.push(args);
+
+ return { telemetry, config, logs };
+}
+describe('AstroTelemetry', () => {
+ let oldCI;
+ before(() => {
+ oldCI = process.env.CI;
+ // Stub process.env.CI to `false`
+ process.env.CI = 'false';
+ });
+ after(() => {
+ process.env.CI = oldCI;
+ });
+ it('initializes when expected arguments are given', () => {
+ const { telemetry } = setup();
+ assert(telemetry instanceof AstroTelemetry);
+ });
+ it('does not record event if disabled', async () => {
+ const { telemetry, config, logs } = setup();
+ telemetry.setEnabled(false);
+ const [key] = Array.from(config.keys());
+ assert.notEqual(key, undefined);
+ assert.equal(config.get(key), false);
+ assert.equal(telemetry.enabled, false);
+ assert.equal(telemetry.isDisabled, true);
+ const result = await telemetry.record(['TEST']);
+ assert.equal(result, undefined);
+ const [log] = logs;
+ assert.notEqual(log, undefined);
+ assert.match(logs.join(''), /disabled/);
+ });
+ it('records event if enabled', async () => {
+ const { telemetry, config, logs } = setup();
+ telemetry.setEnabled(true);
+ const [key] = Array.from(config.keys());
+ assert.notEqual(key, undefined);
+ assert.equal(config.get(key), true);
+ assert.equal(telemetry.enabled, true);
+ assert.equal(telemetry.isDisabled, false);
+ await telemetry.record(['TEST']);
+ assert.equal(logs.length, 2);
+ });
+ it('respects disable from notify', async () => {
+ const { telemetry, config, logs } = setup();
+ await telemetry.notify(() => false);
+ const [key] = Array.from(config.keys());
+ assert.notEqual(key, undefined);
+ assert.equal(config.get(key), false);
+ assert.equal(telemetry.enabled, false);
+ assert.equal(telemetry.isDisabled, true);
+ const [log] = logs;
+ assert.notEqual(log, undefined);
+ assert.match(logs.join(''), /disabled/);
+ });
+ it('respects enable from notify', async () => {
+ const { telemetry, config, logs } = setup();
+ await telemetry.notify(() => true);
+ const [key] = Array.from(config.keys());
+ assert.notEqual(key, undefined);
+ assert.equal(config.get(key), true);
+ assert.equal(telemetry.enabled, true);
+ assert.equal(telemetry.isDisabled, false);
+ const [log] = logs;
+ assert.notEqual(log, undefined);
+ assert.match(logs.join(''), /enabled/);
+ });
+});
diff --git a/packages/telemetry/tsconfig.json b/packages/telemetry/tsconfig.json
new file mode 100644
index 000000000..18443cddf
--- /dev/null
+++ b/packages/telemetry/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "./dist"
+ }
+}