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
This commit is contained in:
parent
1db4e92c12
commit
0c7b42dc67
6 changed files with 133 additions and 34 deletions
6
.changeset/breezy-books-notice.md
Normal file
6
.changeset/breezy-books-notice.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'@astrojs/telemetry': patch
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update telemetry notice
|
|
@ -109,6 +109,11 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
|
||||||
await update(subcommand, { flags });
|
await update(subcommand, { flags });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case 'sync': {
|
||||||
|
const { sync } = await import('./sync/index.js');
|
||||||
|
const exitCode = await sync({ flags });
|
||||||
|
return process.exit(exitCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In verbose/debug mode, we log the debug logs asap before any potential errors could appear
|
// 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) {
|
||||||
process.env.NODE_ENV = cmd === 'dev' ? 'development' : 'production';
|
process.env.NODE_ENV = cmd === 'dev' ? 'development' : 'production';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { notify } = await import('./telemetry/index.js');
|
||||||
|
await notify();
|
||||||
|
|
||||||
// These commands uses the logging and user config. All commands are assumed to have been handled
|
// These commands uses the logging and user config. All commands are assumed to have been handled
|
||||||
// by the end of this switch statement.
|
// by the end of this switch statement.
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
|
@ -161,11 +169,6 @@ async function runCommand(cmd: string, flags: yargs.Arguments) {
|
||||||
return process.exit(checkServer ? 1 : 0);
|
return process.exit(checkServer ? 1 : 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'sync': {
|
|
||||||
const { sync } = await import('./sync/index.js');
|
|
||||||
const exitCode = await sync({ flags });
|
|
||||||
return process.exit(exitCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No command handler matched! This is unexpected.
|
// No command handler matched! This is unexpected.
|
||||||
|
|
|
@ -7,6 +7,13 @@ interface TelemetryOptions {
|
||||||
flags: yargs.Arguments;
|
flags: yargs.Arguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function notify() {
|
||||||
|
await telemetry.notify(() => {
|
||||||
|
console.log(msg.telemetryNotice() + '\n');
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function update(subcommand: string, { flags }: TelemetryOptions) {
|
export async function update(subcommand: string, { flags }: TelemetryOptions) {
|
||||||
const isValid = ['enable', 'disable', 'reset'].includes(subcommand);
|
const isValid = ['enable', 'disable', 'reset'].includes(subcommand);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import boxen from 'boxen';
|
|
||||||
import {
|
import {
|
||||||
bgCyan,
|
bgCyan,
|
||||||
bgGreen,
|
bgGreen,
|
||||||
|
@ -107,34 +106,29 @@ export function serverStart({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function telemetryNotice() {
|
export function telemetryNotice() {
|
||||||
const headline = yellow(`Astro now collects ${bold('anonymous')} usage data.`);
|
const headline = `${cyan('◆')} Astro collects completely anonymous usage data.`;
|
||||||
const why = `This ${bold('optional program')} will help shape our roadmap.`;
|
const why = dim(' This optional program helps shape our roadmap.')
|
||||||
const more = `For more info, visit ${underline('https://astro.build/telemetry')}`;
|
const disable = dim(' Run `npm run astro telemetry disable` to opt-out.');
|
||||||
const box = boxen([headline, why, '', more].join('\n'), {
|
const details = ` Details: ${underline('https://astro.build/telemetry')}`;
|
||||||
margin: 0,
|
return [headline, why, disable, details].map(v => ' ' + v).join('\n');
|
||||||
padding: 1,
|
|
||||||
borderStyle: 'round',
|
|
||||||
borderColor: 'yellow',
|
|
||||||
});
|
|
||||||
return box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function telemetryEnabled() {
|
export function telemetryEnabled() {
|
||||||
return `\n ${green('◉')} Anonymous telemetry is ${bgGreen(
|
return `${green('◉')} Anonymous telemetry is now ${bgGreen(
|
||||||
black(' enabled ')
|
black(' enabled ')
|
||||||
)}. Thank you for improving Astro!\n`;
|
)}\n ${dim('Thank you for improving Astro!')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function telemetryDisabled() {
|
export function telemetryDisabled() {
|
||||||
return `\n ${yellow('◯')} Anonymous telemetry is ${bgYellow(
|
return `${yellow('◯')} Anonymous telemetry is now ${bgYellow(
|
||||||
black(' disabled ')
|
black(' disabled ')
|
||||||
)}. We won't share any usage data.\n`;
|
)}\n ${dim('We won\'t ever record your usage data.')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function telemetryReset() {
|
export function telemetryReset() {
|
||||||
return `\n ${cyan('◆')} Anonymous telemetry has been ${bgCyan(
|
return `${cyan('◆')} Anonymous telemetry has been ${bgCyan(
|
||||||
black(' reset ')
|
black(' reset ')
|
||||||
)}. You may be prompted again.\n`;
|
)}\n ${dim('You may be prompted again.')}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fsStrictWarning() {
|
export function fsStrictWarning() {
|
||||||
|
|
|
@ -10,6 +10,9 @@ import { getSystemInfo, type SystemInfo } from './system-info.js';
|
||||||
export type AstroTelemetryOptions = { astroVersion: string; viteVersion: string };
|
export type AstroTelemetryOptions = { astroVersion: string; viteVersion: string };
|
||||||
export type TelemetryEvent = { eventName: string; payload: Record<string, any> };
|
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;
|
type EventMeta = SystemInfo;
|
||||||
interface EventContext extends ProjectInfo {
|
interface EventContext extends ProjectInfo {
|
||||||
anonymousId: string;
|
anonymousId: string;
|
||||||
|
@ -20,6 +23,8 @@ export class AstroTelemetry {
|
||||||
private _anonymousProjectInfo: ProjectInfo | undefined;
|
private _anonymousProjectInfo: ProjectInfo | undefined;
|
||||||
private config = new GlobalConfig({ name: 'astro' });
|
private config = new GlobalConfig({ name: 'astro' });
|
||||||
private debug = debug('astro:telemetry');
|
private debug = debug('astro:telemetry');
|
||||||
|
private isCI = isCI;
|
||||||
|
private env = process.env;
|
||||||
|
|
||||||
private get astroVersion() {
|
private get astroVersion() {
|
||||||
return this.opts.astroVersion;
|
return this.opts.astroVersion;
|
||||||
|
@ -28,10 +33,10 @@ export class AstroTelemetry {
|
||||||
return this.opts.viteVersion;
|
return this.opts.viteVersion;
|
||||||
}
|
}
|
||||||
private get ASTRO_TELEMETRY_DISABLED() {
|
private get ASTRO_TELEMETRY_DISABLED() {
|
||||||
return process.env.ASTRO_TELEMETRY_DISABLED;
|
return this.env.ASTRO_TELEMETRY_DISABLED;
|
||||||
}
|
}
|
||||||
private get TELEMETRY_DISABLED() {
|
private get TELEMETRY_DISABLED() {
|
||||||
return process.env.TELEMETRY_DISABLED;
|
return this.env.TELEMETRY_DISABLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private opts: AstroTelemetryOptions) {
|
constructor(private opts: AstroTelemetryOptions) {
|
||||||
|
@ -47,7 +52,7 @@ export class AstroTelemetry {
|
||||||
*/
|
*/
|
||||||
private getConfigWithFallback<T>(key: string, getValue: () => T): T {
|
private getConfigWithFallback<T>(key: string, getValue: () => T): T {
|
||||||
const currentValue = this.config.get(key);
|
const currentValue = this.config.get(key);
|
||||||
if (currentValue) {
|
if (currentValue !== undefined) {
|
||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
}
|
||||||
const newValue = getValue();
|
const newValue = getValue();
|
||||||
|
@ -75,7 +80,7 @@ export class AstroTelemetry {
|
||||||
|
|
||||||
private get anonymousProjectInfo(): ProjectInfo {
|
private get anonymousProjectInfo(): ProjectInfo {
|
||||||
// NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
|
// NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
|
||||||
this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(isCI);
|
this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(this.isCI);
|
||||||
return this._anonymousProjectInfo;
|
return this._anonymousProjectInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,19 +99,29 @@ export class AstroTelemetry {
|
||||||
return this.config.clear();
|
return this.config.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async notify(callback: () => Promise<boolean>) {
|
isValidNotice() {
|
||||||
if (this.isDisabled || isCI) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
// The end-user has already been notified about our telemetry integration!
|
// The end-user has already been notified about our telemetry integration!
|
||||||
// Don't bother them about it again.
|
// Don't bother them about it again.
|
||||||
// In the event of significant changes, we should invalidate old dates.
|
if (this.isValidNotice()) {
|
||||||
if (this.notifyDate) {
|
this.debug(`[notify] last notified on ${this.notifyDate}`)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const enabled = await callback();
|
const enabled = await callback();
|
||||||
this.config.set(KEY.TELEMETRY_NOTIFY_DATE, Date.now().toString());
|
this.config.set(KEY.TELEMETRY_NOTIFY_DATE, new Date().valueOf().toString());
|
||||||
this.config.set(KEY.TELEMETRY_ENABLED, enabled);
|
this.config.set(KEY.TELEMETRY_ENABLED, enabled);
|
||||||
|
this.debug(`[notify] telemetry has been ${enabled ? 'enabled' : 'disabled'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async record(event: TelemetryEvent | TelemetryEvent[] = []) {
|
async record(event: TelemetryEvent | TelemetryEvent[] = []) {
|
||||||
|
@ -117,7 +132,7 @@ export class AstroTelemetry {
|
||||||
|
|
||||||
// Skip recording telemetry if the feature is disabled
|
// Skip recording telemetry if the feature is disabled
|
||||||
if (this.isDisabled) {
|
if (this.isDisabled) {
|
||||||
this.debug('telemetry disabled');
|
this.debug('[record] telemetry has been disabled');
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,83 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { AstroTelemetry } from '../dist/index.js';
|
import { AstroTelemetry } from '../dist/index.js';
|
||||||
|
|
||||||
describe('AstroTelemetry', () => {
|
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', () => {
|
it('initializes when expected arguments are given', () => {
|
||||||
const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
|
const { telemetry } = setup();
|
||||||
expect(telemetry).to.be.instanceOf(AstroTelemetry);
|
expect(telemetry).to.be.instanceOf(AstroTelemetry);
|
||||||
});
|
});
|
||||||
|
it('does not record event if disabled', async () => {
|
||||||
|
const { telemetry, config, logs } = setup();
|
||||||
|
telemetry.setEnabled(false);
|
||||||
|
const [key] = Array.from(config.keys());
|
||||||
|
expect(key).not.to.be.undefined;
|
||||||
|
expect(config.get(key)).to.be.false;
|
||||||
|
expect(telemetry.enabled).to.be.false;
|
||||||
|
expect(telemetry.isDisabled).to.be.true;
|
||||||
|
const result = await telemetry.record(['TEST']);
|
||||||
|
expect(result).to.be.undefined;
|
||||||
|
const [log] = logs;
|
||||||
|
expect(log).not.to.be.undefined;
|
||||||
|
expect(logs.join('')).to.match(/disabled/);
|
||||||
|
});
|
||||||
|
it('records event if enabled', async () => {
|
||||||
|
const { telemetry, config, logs } = setup();
|
||||||
|
telemetry.setEnabled(true);
|
||||||
|
const [key] = Array.from(config.keys());
|
||||||
|
expect(key).not.to.be.undefined;
|
||||||
|
expect(config.get(key)).to.be.true;
|
||||||
|
expect(telemetry.enabled).to.be.true;
|
||||||
|
expect(telemetry.isDisabled).to.be.false;
|
||||||
|
await telemetry.record(['TEST']);
|
||||||
|
expect(logs.length).to.equal(2);
|
||||||
|
});
|
||||||
|
it('respects disable from notify', async () => {
|
||||||
|
const { telemetry, config, logs } = setup();
|
||||||
|
await telemetry.notify(() => false);
|
||||||
|
const [key] = Array.from(config.keys());
|
||||||
|
expect(key).not.to.be.undefined;
|
||||||
|
expect(config.get(key)).to.be.false;
|
||||||
|
expect(telemetry.enabled).to.be.false;
|
||||||
|
expect(telemetry.isDisabled).to.be.true;
|
||||||
|
const [log] = logs;
|
||||||
|
expect(log).not.to.be.undefined;
|
||||||
|
expect(logs.join('')).to.match(/disabled/);
|
||||||
|
});
|
||||||
|
it('respects enable from notify', async () => {
|
||||||
|
const { telemetry, config, logs } = setup();
|
||||||
|
await telemetry.notify(() => true);
|
||||||
|
const [key] = Array.from(config.keys());
|
||||||
|
expect(key).not.to.be.undefined;
|
||||||
|
expect(config.get(key)).to.be.true;
|
||||||
|
expect(telemetry.enabled).to.be.true;
|
||||||
|
expect(telemetry.isDisabled).to.be.false;
|
||||||
|
const [log] = logs;
|
||||||
|
expect(log).not.to.be.undefined;
|
||||||
|
expect(logs.join('')).to.match(/enabled/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue