Skip to content

Commit 0c7b42d

Browse files
authored
Update telemetry notice (#8234)
* chore: enable telemetry notice * chore: update telemetry date, notify * chore(telemetry): refactor telemetry notices * chore: add changeset * chore: improve debugging * chore: update debug * fix: logical error * chore(lint): remove unused input * chore: improve telemetry debug * chore: improve telemetry tests * chore: allow isCI to be stubbed * chore: stub process.env * chore: stub process.env * chore: add env to class * chore: act like we're not on CI * test: didn't commit the env stub properly * chore: tweak wording
1 parent 1db4e92 commit 0c7b42d

6 files changed

Lines changed: 133 additions & 34 deletions

File tree

.changeset/breezy-books-notice.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@astrojs/telemetry': patch
3+
'astro': patch
4+
---
5+
6+
Update telemetry notice

packages/astro/src/cli/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
109109
await update(subcommand, { flags });
110110
return;
111111
}
112+
case 'sync': {
113+
const { sync } = await import('./sync/index.js');
114+
const exitCode = await sync({ flags });
115+
return process.exit(exitCode);
116+
}
112117
}
113118

114119
// In verbose/debug mode, we log the debug logs asap before any potential errors could appear
@@ -122,6 +127,9 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
122127
process.env.NODE_ENV = cmd === 'dev' ? 'development' : 'production';
123128
}
124129

130+
const { notify } = await import('./telemetry/index.js');
131+
await notify();
132+
125133
// These commands uses the logging and user config. All commands are assumed to have been handled
126134
// by the end of this switch statement.
127135
switch (cmd) {
@@ -161,11 +169,6 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
161169
return process.exit(checkServer ? 1 : 0);
162170
}
163171
}
164-
case 'sync': {
165-
const { sync } = await import('./sync/index.js');
166-
const exitCode = await sync({ flags });
167-
return process.exit(exitCode);
168-
}
169172
}
170173

171174
// No command handler matched! This is unexpected.

packages/astro/src/cli/telemetry/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ interface TelemetryOptions {
77
flags: yargs.Arguments;
88
}
99

10+
export async function notify() {
11+
await telemetry.notify(() => {
12+
console.log(msg.telemetryNotice() + '\n');
13+
return true;
14+
})
15+
}
16+
1017
export async function update(subcommand: string, { flags }: TelemetryOptions) {
1118
const isValid = ['enable', 'disable', 'reset'].includes(subcommand);
1219

packages/astro/src/core/messages.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import boxen from 'boxen';
21
import {
32
bgCyan,
43
bgGreen,
@@ -107,34 +106,29 @@ export function serverStart({
107106
}
108107

109108
export function telemetryNotice() {
110-
const headline = yellow(`Astro now collects ${bold('anonymous')} usage data.`);
111-
const why = `This ${bold('optional program')} will help shape our roadmap.`;
112-
const more = `For more info, visit ${underline('https://astro.build/telemetry')}`;
113-
const box = boxen([headline, why, '', more].join('\n'), {
114-
margin: 0,
115-
padding: 1,
116-
borderStyle: 'round',
117-
borderColor: 'yellow',
118-
});
119-
return box;
109+
const headline = `${cyan('◆')} Astro collects completely anonymous usage data.`;
110+
const why = dim(' This optional program helps shape our roadmap.')
111+
const disable = dim(' Run `npm run astro telemetry disable` to opt-out.');
112+
const details = ` Details: ${underline('https://astro.build/telemetry')}`;
113+
return [headline, why, disable, details].map(v => ' ' + v).join('\n');
120114
}
121115

122116
export function telemetryEnabled() {
123-
return `\n ${green('◉')} Anonymous telemetry is ${bgGreen(
117+
return `${green('◉')} Anonymous telemetry is now ${bgGreen(
124118
black(' enabled ')
125-
)}. Thank you for improving Astro!\n`;
119+
)}\n ${dim('Thank you for improving Astro!')}\n`;
126120
}
127121

128122
export function telemetryDisabled() {
129-
return `\n ${yellow('◯')} Anonymous telemetry is ${bgYellow(
123+
return `${yellow('◯')} Anonymous telemetry is now ${bgYellow(
130124
black(' disabled ')
131-
)}. We won't share any usage data.\n`;
125+
)}\n ${dim('We won\'t ever record your usage data.')}\n`;
132126
}
133127

134128
export function telemetryReset() {
135-
return `\n ${cyan('◆')} Anonymous telemetry has been ${bgCyan(
129+
return `${cyan('◆')} Anonymous telemetry has been ${bgCyan(
136130
black(' reset ')
137-
)}. You may be prompted again.\n`;
131+
)}\n ${dim('You may be prompted again.')}\n`;
138132
}
139133

140134
export function fsStrictWarning() {

packages/telemetry/src/index.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { getSystemInfo, type SystemInfo } from './system-info.js';
1010
export type AstroTelemetryOptions = { astroVersion: string; viteVersion: string };
1111
export type TelemetryEvent = { eventName: string; payload: Record<string, any> };
1212

13+
// In the event of significant policy changes, update this!
14+
const VALID_TELEMETRY_NOTICE_DATE = '2023-08-25';
15+
1316
type EventMeta = SystemInfo;
1417
interface EventContext extends ProjectInfo {
1518
anonymousId: string;
@@ -20,6 +23,8 @@ export class AstroTelemetry {
2023
private _anonymousProjectInfo: ProjectInfo | undefined;
2124
private config = new GlobalConfig({ name: 'astro' });
2225
private debug = debug('astro:telemetry');
26+
private isCI = isCI;
27+
private env = process.env;
2328

2429
private get astroVersion() {
2530
return this.opts.astroVersion;
@@ -28,10 +33,10 @@ export class AstroTelemetry {
2833
return this.opts.viteVersion;
2934
}
3035
private get ASTRO_TELEMETRY_DISABLED() {
31-
return process.env.ASTRO_TELEMETRY_DISABLED;
36+
return this.env.ASTRO_TELEMETRY_DISABLED;
3237
}
3338
private get TELEMETRY_DISABLED() {
34-
return process.env.TELEMETRY_DISABLED;
39+
return this.env.TELEMETRY_DISABLED;
3540
}
3641

3742
constructor(private opts: AstroTelemetryOptions) {
@@ -47,7 +52,7 @@ export class AstroTelemetry {
4752
*/
4853
private getConfigWithFallback<T>(key: string, getValue: () => T): T {
4954
const currentValue = this.config.get(key);
50-
if (currentValue) {
55+
if (currentValue !== undefined) {
5156
return currentValue;
5257
}
5358
const newValue = getValue();
@@ -75,7 +80,7 @@ export class AstroTelemetry {
7580

7681
private get anonymousProjectInfo(): ProjectInfo {
7782
// NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
78-
this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(isCI);
83+
this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(this.isCI);
7984
return this._anonymousProjectInfo;
8085
}
8186

@@ -94,19 +99,29 @@ export class AstroTelemetry {
9499
return this.config.clear();
95100
}
96101

97-
async notify(callback: () => Promise<boolean>) {
98-
if (this.isDisabled || isCI) {
102+
isValidNotice() {
103+
if (!this.notifyDate) return false;
104+
const current = Number(this.notifyDate);
105+
const valid = new Date(VALID_TELEMETRY_NOTICE_DATE).valueOf();
106+
107+
return current > valid;
108+
}
109+
110+
async notify(callback: () => boolean | Promise<boolean>) {
111+
if (this.isDisabled || this.isCI) {
112+
this.debug(`[notify] telemetry has been disabled`);
99113
return;
100114
}
101115
// The end-user has already been notified about our telemetry integration!
102116
// Don't bother them about it again.
103-
// In the event of significant changes, we should invalidate old dates.
104-
if (this.notifyDate) {
117+
if (this.isValidNotice()) {
118+
this.debug(`[notify] last notified on ${this.notifyDate}`)
105119
return;
106120
}
107121
const enabled = await callback();
108-
this.config.set(KEY.TELEMETRY_NOTIFY_DATE, Date.now().toString());
122+
this.config.set(KEY.TELEMETRY_NOTIFY_DATE, new Date().valueOf().toString());
109123
this.config.set(KEY.TELEMETRY_ENABLED, enabled);
124+
this.debug(`[notify] telemetry has been ${enabled ? 'enabled' : 'disabled'}`)
110125
}
111126

112127
async record(event: TelemetryEvent | TelemetryEvent[] = []) {
@@ -117,7 +132,7 @@ export class AstroTelemetry {
117132

118133
// Skip recording telemetry if the feature is disabled
119134
if (this.isDisabled) {
120-
this.debug('telemetry disabled');
135+
this.debug('[record] telemetry has been disabled');
121136
return Promise.resolve();
122137
}
123138

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,83 @@
11
import { expect } from 'chai';
22
import { AstroTelemetry } from '../dist/index.js';
33

4-
describe('AstroTelemetry', () => {
4+
function setup() {
5+
const config = new Map();
6+
const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
7+
const logs = [];
8+
// Stub isCI to false so we can test user-facing behavior
9+
telemetry.isCI = false;
10+
// Stub process.env to properly test in Astro's own CI
11+
telemetry.env = {};
12+
// Override config so we can inspect it
13+
telemetry.config = config;
14+
// Override debug so we can inspect it
15+
telemetry.debug.enabled = true;
16+
telemetry.debug.log = (...args) => logs.push(args);
17+
18+
return { telemetry, config, logs }
19+
}
20+
describe('AstroTelemetry', () => {
21+
let oldCI;
22+
before(() => {
23+
oldCI = process.env.CI;
24+
// Stub process.env.CI to `false`
25+
process.env.CI = 'false';
26+
})
27+
after(() => {
28+
process.env.CI = oldCI;
29+
})
530
it('initializes when expected arguments are given', () => {
6-
const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
31+
const { telemetry } = setup();
732
expect(telemetry).to.be.instanceOf(AstroTelemetry);
833
});
34+
it('does not record event if disabled', async () => {
35+
const { telemetry, config, logs } = setup();
36+
telemetry.setEnabled(false);
37+
const [key] = Array.from(config.keys());
38+
expect(key).not.to.be.undefined;
39+
expect(config.get(key)).to.be.false;
40+
expect(telemetry.enabled).to.be.false;
41+
expect(telemetry.isDisabled).to.be.true;
42+
const result = await telemetry.record(['TEST']);
43+
expect(result).to.be.undefined;
44+
const [log] = logs;
45+
expect(log).not.to.be.undefined;
46+
expect(logs.join('')).to.match(/disabled/);
47+
});
48+
it('records event if enabled', async () => {
49+
const { telemetry, config, logs } = setup();
50+
telemetry.setEnabled(true);
51+
const [key] = Array.from(config.keys());
52+
expect(key).not.to.be.undefined;
53+
expect(config.get(key)).to.be.true;
54+
expect(telemetry.enabled).to.be.true;
55+
expect(telemetry.isDisabled).to.be.false;
56+
await telemetry.record(['TEST']);
57+
expect(logs.length).to.equal(2);
58+
});
59+
it('respects disable from notify', async () => {
60+
const { telemetry, config, logs } = setup();
61+
await telemetry.notify(() => false);
62+
const [key] = Array.from(config.keys());
63+
expect(key).not.to.be.undefined;
64+
expect(config.get(key)).to.be.false;
65+
expect(telemetry.enabled).to.be.false;
66+
expect(telemetry.isDisabled).to.be.true;
67+
const [log] = logs;
68+
expect(log).not.to.be.undefined;
69+
expect(logs.join('')).to.match(/disabled/);
70+
});
71+
it('respects enable from notify', async () => {
72+
const { telemetry, config, logs } = setup();
73+
await telemetry.notify(() => true);
74+
const [key] = Array.from(config.keys());
75+
expect(key).not.to.be.undefined;
76+
expect(config.get(key)).to.be.true;
77+
expect(telemetry.enabled).to.be.true;
78+
expect(telemetry.isDisabled).to.be.false;
79+
const [log] = logs;
80+
expect(log).not.to.be.undefined;
81+
expect(logs.join('')).to.match(/enabled/);
82+
});
983
});

0 commit comments

Comments
 (0)