Skip to content

Commit e2a3de6

Browse files
authored
more precise telemetry types (#1765)
1 parent 21ac188 commit e2a3de6

File tree

5 files changed

+87
-106
lines changed

5 files changed

+87
-106
lines changed

docs/telemetry.md

Lines changed: 0 additions & 61 deletions
This file was deleted.

docs/telemetry.md.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {readFile} from "node:fs/promises";
2+
3+
process.stdout.write(`# Telemetry
4+
5+
Observable Framework collects anonymous usage data to help us improve the product. This data is sent to Observable and is not shared with third parties. Telemetry data is covered by [Observable’s privacy policy](https://observablehq.com/privacy-policy).
6+
7+
You can [opt-out of telemetry](#disabling-telemetry) by setting the \`OBSERVABLE_TELEMETRY_DISABLE\` environment variable to \`true\`.
8+
9+
## What is collected?
10+
11+
The following data is collected:
12+
13+
~~~ts run=false
14+
${(await readFile("./src/telemetryData.d.ts", "utf-8")).trim()}
15+
~~~
16+
17+
To inspect telemetry data, set the \`OBSERVABLE_TELEMETRY_DEBUG\` environment variable to \`true\`. This will print the telemetry data to stderr instead of sending it to Observable. See [\`telemetry.ts\`](https://github.com/observablehq/framework/blob/main/src/telemetry.ts) for source code.
18+
19+
## What is not collected?
20+
21+
We never collect identifying or sensitive information, such as environment variables, file names or paths, or file contents.
22+
23+
## Disabling telemetry
24+
25+
Setting the \`OBSERVABLE_TELEMETRY_DISABLE\` environment variable to \`true\` disables telemetry collection entirely. For example:
26+
27+
~~~sh
28+
OBSERVABLE_TELEMETRY_DISABLE=true npm run build
29+
~~~
30+
31+
Setting the \`OBSERVABLE_TELEMETRY_DEBUG\` environment variable to \`true\` also disables telemetry collection, instead printing telemetry data to stderr. Use this to inspect what telemetry data would be collected.
32+
`);

src/telemetry.ts

Lines changed: 5 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,15 @@
11
import {exec} from "node:child_process";
2+
import type {UUID} from "node:crypto";
23
import {createHash, randomUUID} from "node:crypto";
34
import {readFile, writeFile} from "node:fs/promises";
45
import os from "node:os";
56
import {join} from "node:path/posix";
67
import {CliError} from "./error.js";
78
import type {Logger} from "./logger.js";
89
import {getObservableUiOrigin} from "./observableApiClient.js";
10+
import type {TelemetryData, TelemetryEnvironment, TelemetryIds, TelemetryTime} from "./telemetryData.js";
911
import {link, magenta} from "./tty.js";
1012

11-
type uuid = ReturnType<typeof randomUUID>;
12-
13-
type TelemetryIds = {
14-
session: uuid | null; // random, held in memory for the duration of the process
15-
device: uuid | null; // persists to ~/.observablehq
16-
project: string | null; // one-way hash of private salt + repository URL or cwd
17-
};
18-
19-
type TelemetryEnvironment = {
20-
version: string; // version from package.json
21-
userAgent: string; // npm_config_user_agent
22-
node: string; // node.js version
23-
systemPlatform: string; // linux, darwin, win32, ...
24-
systemRelease: string; // 20.04, 11.2.3, ...
25-
systemArchitecture: string; // x64, arm64, ...
26-
cpuCount: number; // number of cpu cores
27-
cpuModel: string | null; // cpu model name
28-
cpuSpeed: number | null; // cpu speed in MHz
29-
memoryInMb: number; // truncated to mb
30-
isCI: string | boolean; // inside CI heuristic, name or false
31-
isDocker: boolean; // inside Docker heuristic
32-
isWSL: boolean; // inside WSL heuristic
33-
};
34-
35-
type TelemetryTime = {
36-
now: number; // performance.now
37-
timeOrigin: number; // performance.timeOrigin
38-
timeZoneOffset: number; // minutes from UTC
39-
};
40-
41-
type TelemetryData = {
42-
event: "build" | "deploy" | "preview" | "signal" | "login";
43-
step?: "start" | "finish" | "error";
44-
[key: string]: unknown;
45-
};
46-
4713
type TelemetryEffects = {
4814
logger: Logger;
4915
process: NodeJS.Process;
@@ -79,7 +45,7 @@ export class Telemetry {
7945
private endpoint: URL;
8046
private timeZoneOffset = new Date().getTimezoneOffset();
8147
private readonly _pending = new Set<Promise<unknown>>();
82-
private _config: Promise<Record<string, uuid>> | undefined;
48+
private _config: Promise<Record<string, UUID>> | undefined;
8349
private _ids: Promise<TelemetryIds> | undefined;
8450
private _environment: Promise<TelemetryEnvironment> | undefined;
8551

@@ -142,7 +108,7 @@ export class Telemetry {
142108
process.on(name, signaled);
143109
}
144110

145-
private async getPersistentId(name: string, generator = randomUUID): Promise<uuid | null> {
111+
private async getPersistentId(name: string, generator = randomUUID): Promise<UUID | null> {
146112
const {readFile, writeFile} = this.effects;
147113
const file = join(os.homedir(), ".observablehq");
148114
if (!this._config) {
@@ -213,7 +179,7 @@ export class Telemetry {
213179
}
214180

215181
private async showBannerIfNeeded() {
216-
let called: uuid | undefined;
182+
let called: UUID | undefined;
217183
await this.getPersistentId("cli_telemetry_banner", () => (called = randomUUID()));
218184
if (called) {
219185
this.effects.logger.error(

src/telemetryData.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {UUID} from "node:crypto";
2+
3+
export type TelemetryIds = {
4+
session: UUID | null; // random, held in memory for the duration of the process
5+
device: UUID | null; // persists to ~/.observablehq
6+
project: string | null; // one-way hash of private salt + repository URL or cwd
7+
};
8+
9+
export type TelemetryEnvironment = {
10+
version: string; // version from package.json
11+
userAgent: string; // npm_config_user_agent
12+
node: string; // node.js version
13+
systemPlatform: string; // linux, darwin, win32, ...
14+
systemRelease: string; // 20.04, 11.2.3, ...
15+
systemArchitecture: string; // x64, arm64, ...
16+
cpuCount: number; // number of cpu cores
17+
cpuModel: string | null; // cpu model name
18+
cpuSpeed: number | null; // cpu speed in MHz
19+
memoryInMb: number; // truncated to mb
20+
isCI: string | boolean; // inside CI heuristic, name or false
21+
isDocker: boolean; // inside Docker heuristic
22+
isWSL: boolean; // inside WSL heuristic
23+
};
24+
25+
export type TelemetryTime = {
26+
now: number; // performance.now
27+
timeOrigin: number; // performance.timeOrigin
28+
timeZoneOffset: number; // minutes from UTC
29+
};
30+
31+
export type TelemetryData =
32+
| {event: "build"; step: "start"}
33+
| {event: "build"; step: "finish"; pageCount: number}
34+
| {event: "deploy"; step: "start"; force: boolean | null | "build" | "deploy"}
35+
| {event: "deploy"; step: "finish"}
36+
| {event: "deploy"; step: "error"}
37+
| {event: "deploy"; buildManifest: "found" | "missing" | "error"}
38+
| {event: "preview"; step: "start"}
39+
| {event: "preview"; step: "finish"}
40+
| {event: "preview"; step: "error"}
41+
| {event: "signal"; signal: NodeJS.Signals}
42+
| {event: "login"; step: "start"}
43+
| {event: "login"; step: "finish"}
44+
| {event: "login"; step: "error"; code: "expired" | "consumed" | "no-key" | `unknown-${string}`};

test/telemetry-test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,22 @@ describe("telemetry", () => {
2929

3030
it("sends data", async () => {
3131
Telemetry._instance = new Telemetry(noopEffects);
32-
Telemetry.record({event: "build", step: "start", test: true});
32+
Telemetry.record({event: "build", step: "start"});
3333
await Telemetry.instance.pending;
3434
agent.assertNoPendingInterceptors();
3535
});
3636

3737
it("shows a banner", async () => {
3838
const logger = new MockLogger();
3939
const telemetry = new Telemetry({...noopEffects, logger, readFile: () => Promise.reject()});
40-
telemetry.record({event: "build", step: "start", test: true});
40+
telemetry.record({event: "build", step: "start"});
4141
await telemetry.pending;
4242
logger.assertExactErrors([/Attention.*observablehq.com.*OBSERVABLE_TELEMETRY_DISABLE=true/s]);
4343
});
4444

4545
it("can be disabled", async () => {
4646
const telemetry = new Telemetry({...noopEffects, process: processMock({env: {OBSERVABLE_TELEMETRY_DISABLE: "1"}})});
47-
telemetry.record({event: "build", step: "start", test: true});
47+
telemetry.record({event: "build", step: "start"});
4848
await telemetry.pending;
4949
assert.equal(agent.pendingInterceptors().length, 1);
5050
});
@@ -56,7 +56,7 @@ describe("telemetry", () => {
5656
logger,
5757
process: processMock({env: {OBSERVABLE_TELEMETRY_DEBUG: "1"}})
5858
});
59-
telemetry.record({event: "build", step: "start", test: true});
59+
telemetry.record({event: "build", step: "start"});
6060
await telemetry.pending;
6161
assert.equal(logger.errorLines.length, 1);
6262
assert.equal(logger.errorLines[0][0], "[telemetry]");
@@ -71,7 +71,7 @@ describe("telemetry", () => {
7171
process: processMock({env: {OBSERVABLE_TELEMETRY_DEBUG: "1"}}),
7272
writeFile: () => Promise.reject()
7373
});
74-
telemetry.record({event: "build", step: "start", test: true});
74+
telemetry.record({event: "build", step: "start"});
7575
await telemetry.pending;
7676
assert.notEqual(logger.errorLines[0][1].ids.session, null);
7777
assert.equal(logger.errorLines[0][1].ids.device, null);
@@ -86,7 +86,7 @@ describe("telemetry", () => {
8686
logger,
8787
process: processMock({env: {OBSERVABLE_TELEMETRY_ORIGIN: "https://invalid."}})
8888
});
89-
telemetry.record({event: "build", step: "start", test: true});
89+
telemetry.record({event: "build", step: "start"});
9090
await telemetry.pending;
9191
assert.equal(logger.errorLines.length, 0);
9292
assert.equal(agent.pendingInterceptors().length, 1);

0 commit comments

Comments
 (0)