update telemetry to support more anonymized project id (#3713)
* update telemetry to support more anonymized project id * Create strange-laws-kick.md * respond to nate feedback
This commit is contained in:
parent
4d6d8644e6
commit
ebd7e7ad81
20 changed files with 312 additions and 352 deletions
6
.changeset/strange-laws-kick.md
Normal file
6
.changeset/strange-laws-kick.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"astro": patch
|
||||||
|
"@astrojs/telemetry": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Update telemetry to support a more anonymized project id. `anonymousProjectId` is now hashed based on anonymous git data instead of your git remote URL.
|
|
@ -3,7 +3,7 @@
|
||||||
import { LogOptions } from '../core/logger/core.js';
|
import { LogOptions } from '../core/logger/core.js';
|
||||||
|
|
||||||
import { AstroTelemetry } from '@astrojs/telemetry';
|
import { AstroTelemetry } from '@astrojs/telemetry';
|
||||||
import * as event from '@astrojs/telemetry/events';
|
import * as event from '../events/index.js';
|
||||||
import * as colors from 'kleur/colors';
|
import * as colors from 'kleur/colors';
|
||||||
import yargs from 'yargs-parser';
|
import yargs from 'yargs-parser';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -19,6 +19,7 @@ import { createSafeError } from '../core/util.js';
|
||||||
import { check } from './check.js';
|
import { check } from './check.js';
|
||||||
import { openInBrowser } from './open.js';
|
import { openInBrowser } from './open.js';
|
||||||
import * as telemetryHandler from './telemetry.js';
|
import * as telemetryHandler from './telemetry.js';
|
||||||
|
import { AstroUserConfig } from '../@types/astro.js';
|
||||||
|
|
||||||
type Arguments = yargs.Arguments;
|
type Arguments = yargs.Arguments;
|
||||||
type CLICommand =
|
type CLICommand =
|
||||||
|
@ -61,12 +62,13 @@ function printAstroHelp() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PACKAGE_VERSION is injected when we build and publish the astro package.
|
||||||
|
const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
|
||||||
|
|
||||||
/** Display --version flag */
|
/** Display --version flag */
|
||||||
async function printVersion() {
|
async function printVersion() {
|
||||||
// PACKAGE_VERSION is injected at build time
|
|
||||||
const version = process.env.PACKAGE_VERSION ?? '';
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(` ${colors.bgGreen(colors.black(` astro `))} ${colors.green(`v${version}`)}`);
|
console.log(` ${colors.bgGreen(colors.black(` astro `))} ${colors.green(`v${ASTRO_VERSION}`)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Determine which command the user requested */
|
/** Determine which command the user requested */
|
||||||
|
@ -110,43 +112,51 @@ export async function cli(args: string[]) {
|
||||||
} else if (flags.silent) {
|
} else if (flags.silent) {
|
||||||
logging.level = 'silent';
|
logging.level = 'silent';
|
||||||
}
|
}
|
||||||
const telemetry = new AstroTelemetry({ version: process.env.PACKAGE_VERSION ?? '' });
|
const telemetry = new AstroTelemetry({ version: ASTRO_VERSION });
|
||||||
|
|
||||||
if (cmd === 'telemetry') {
|
|
||||||
try {
|
|
||||||
const subcommand = flags._[3]?.toString();
|
|
||||||
return await telemetryHandler.update(subcommand, { flags, telemetry });
|
|
||||||
} catch (err) {
|
|
||||||
return throwAndExit(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Special CLI Commands: "add", "docs", "telemetry"
|
||||||
|
// These commands run before the user's config is parsed, and may have other special
|
||||||
|
// conditions that should be handled here, before the others.
|
||||||
|
//
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case 'add': {
|
case 'add': {
|
||||||
try {
|
try {
|
||||||
|
telemetry.record(event.eventCliSession({ cliCommand: cmd }));
|
||||||
const packages = flags._.slice(3) as string[];
|
const packages = flags._.slice(3) as string[];
|
||||||
telemetry.record(
|
|
||||||
event.eventCliSession({
|
|
||||||
astroVersion: process.env.PACKAGE_VERSION ?? '',
|
|
||||||
cliCommand: 'add',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return await add(packages, { cwd: root, flags, logging, telemetry });
|
return await add(packages, { cwd: root, flags, logging, telemetry });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return throwAndExit(err);
|
return throwAndExit(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'docs': {
|
||||||
|
try {
|
||||||
|
telemetry.record(event.eventCliSession({ cliCommand: cmd }));
|
||||||
|
return await openInBrowser('https://docs.astro.build/');
|
||||||
|
} catch (err) {
|
||||||
|
return throwAndExit(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'telemetry': {
|
||||||
|
try {
|
||||||
|
// Do not track session start, since the user may be trying to enable,
|
||||||
|
// disable, or modify telemetry settings.
|
||||||
|
const subcommand = flags._[3]?.toString();
|
||||||
|
return await telemetryHandler.update(subcommand, { flags, telemetry });
|
||||||
|
} catch (err) {
|
||||||
|
return throwAndExit(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
|
||||||
|
telemetry.record(event.eventCliSession({ cliCommand: cmd }, userConfig, flags));
|
||||||
|
|
||||||
|
// Common CLI Commands:
|
||||||
|
// These commands run normally. All commands are assumed to have been handled
|
||||||
|
// by the end of this switch statement.
|
||||||
|
switch (cmd) {
|
||||||
case 'dev': {
|
case 'dev': {
|
||||||
try {
|
try {
|
||||||
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
|
|
||||||
|
|
||||||
telemetry.record(
|
|
||||||
event.eventCliSession(
|
|
||||||
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'dev' },
|
|
||||||
userConfig,
|
|
||||||
flags
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await devServer(astroConfig, { logging, telemetry });
|
await devServer(astroConfig, { logging, telemetry });
|
||||||
return await new Promise(() => {}); // lives forever
|
return await new Promise(() => {}); // lives forever
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -156,14 +166,6 @@ export async function cli(args: string[]) {
|
||||||
|
|
||||||
case 'build': {
|
case 'build': {
|
||||||
try {
|
try {
|
||||||
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
|
|
||||||
telemetry.record(
|
|
||||||
event.eventCliSession(
|
|
||||||
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'build' },
|
|
||||||
userConfig,
|
|
||||||
flags
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return await build(astroConfig, { logging, telemetry });
|
return await build(astroConfig, { logging, telemetry });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return throwAndExit(err);
|
return throwAndExit(err);
|
||||||
|
@ -171,53 +173,22 @@ export async function cli(args: string[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'check': {
|
case 'check': {
|
||||||
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
|
|
||||||
telemetry.record(
|
|
||||||
event.eventCliSession(
|
|
||||||
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'check' },
|
|
||||||
userConfig,
|
|
||||||
flags
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const ret = await check(astroConfig);
|
const ret = await check(astroConfig);
|
||||||
return process.exit(ret);
|
return process.exit(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'preview': {
|
case 'preview': {
|
||||||
try {
|
try {
|
||||||
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
|
|
||||||
telemetry.record(
|
|
||||||
event.eventCliSession(
|
|
||||||
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'preview' },
|
|
||||||
userConfig,
|
|
||||||
flags
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const server = await preview(astroConfig, { logging, telemetry });
|
const server = await preview(astroConfig, { logging, telemetry });
|
||||||
return await server.closed(); // keep alive until the server is closed
|
return await server.closed(); // keep alive until the server is closed
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return throwAndExit(err);
|
return throwAndExit(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'docs': {
|
|
||||||
try {
|
|
||||||
await telemetry.record(
|
|
||||||
event.eventCliSession({
|
|
||||||
astroVersion: process.env.PACKAGE_VERSION ?? '',
|
|
||||||
cliCommand: 'docs',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return await openInBrowser('https://docs.astro.build/');
|
|
||||||
} catch (err) {
|
|
||||||
return throwAndExit(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
throw new Error(`Error running ${cmd}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No command handler matched! This is unexpected.
|
||||||
|
throwAndExit(new Error(`Error running ${cmd} -- no command found.`));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Display error and exit */
|
/** Display error and exit */
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import type { AstroTelemetry } from '@astrojs/telemetry';
|
import type { AstroTelemetry } from '@astrojs/telemetry';
|
||||||
import type yargs from 'yargs-parser';
|
import type yargs from 'yargs-parser';
|
||||||
|
|
||||||
import * as msg from '../core/messages.js';
|
import * as msg from '../core/messages.js';
|
||||||
|
|
||||||
export interface TelemetryOptions {
|
export interface TelemetryOptions {
|
||||||
flags: yargs.Arguments;
|
flags: yargs.Arguments;
|
||||||
telemetry: AstroTelemetry;
|
telemetry: AstroTelemetry;
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export * from './build.js';
|
|
||||||
export * from './session.js';
|
export * from './session.js';
|
|
@ -1,14 +1,10 @@
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
|
import type { AstroUserConfig } from '../@types/astro';
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED';
|
const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED';
|
||||||
|
|
||||||
// :( We can't import the type because of TurboRepo circular dep limitation
|
|
||||||
type AstroUserConfig = Record<string, any>;
|
|
||||||
|
|
||||||
interface EventCliSession {
|
interface EventCliSession {
|
||||||
astroVersion: string;
|
|
||||||
cliCommand: string;
|
cliCommand: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +21,7 @@ interface ConfigInfo {
|
||||||
markdown:
|
markdown:
|
||||||
| undefined
|
| undefined
|
||||||
| {
|
| {
|
||||||
mode: undefined | 'md' | 'mdx';
|
drafts: undefined | boolean;
|
||||||
syntaxHighlight: undefined | 'shiki' | 'prism' | false;
|
syntaxHighlight: undefined | 'shiki' | 'prism' | false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -91,15 +87,18 @@ export function eventCliSession(
|
||||||
flags?: Record<string, any>
|
flags?: Record<string, any>
|
||||||
): { eventName: string; payload: EventCliSessionInternal }[] {
|
): { eventName: string; payload: EventCliSessionInternal }[] {
|
||||||
// Filter out falsy integrations
|
// Filter out falsy integrations
|
||||||
const integrations = userConfig?.integrations?.filter?.(Boolean) ?? [];
|
|
||||||
const configValues = userConfig
|
const configValues = userConfig
|
||||||
? {
|
? {
|
||||||
markdownPlugins: [
|
markdownPlugins: [
|
||||||
userConfig?.markdown?.remarkPlugins ?? [],
|
...(userConfig?.markdown?.remarkPlugins?.map((p) =>
|
||||||
userConfig?.markdown?.rehypePlugins ?? [],
|
typeof p === 'string' ? p : typeof p
|
||||||
].flat(1),
|
) ?? []),
|
||||||
|
...(userConfig?.markdown?.rehypePlugins?.map((p) =>
|
||||||
|
typeof p === 'string' ? p : typeof p
|
||||||
|
) ?? []),
|
||||||
|
] as string[],
|
||||||
adapter: userConfig?.adapter?.name ?? null,
|
adapter: userConfig?.adapter?.name ?? null,
|
||||||
integrations: integrations?.map?.((i: any) => i?.name) ?? [],
|
integrations: (userConfig?.integrations ?? []).filter(Boolean).map((i: any) => i?.name),
|
||||||
trailingSlash: userConfig?.trailingSlash,
|
trailingSlash: userConfig?.trailingSlash,
|
||||||
build: userConfig?.build
|
build: userConfig?.build
|
||||||
? {
|
? {
|
||||||
|
@ -108,7 +107,7 @@ export function eventCliSession(
|
||||||
: undefined,
|
: undefined,
|
||||||
markdown: userConfig?.markdown
|
markdown: userConfig?.markdown
|
||||||
? {
|
? {
|
||||||
mode: userConfig?.markdown?.mode,
|
drafts: userConfig.markdown?.drafts,
|
||||||
syntaxHighlight: userConfig.markdown?.syntaxHighlight,
|
syntaxHighlight: userConfig.markdown?.syntaxHighlight,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -121,15 +120,12 @@ export function eventCliSession(
|
||||||
const payload: EventCliSessionInternal = {
|
const payload: EventCliSessionInternal = {
|
||||||
cliCommand: event.cliCommand,
|
cliCommand: event.cliCommand,
|
||||||
// Versions
|
// Versions
|
||||||
astroVersion: event.astroVersion,
|
|
||||||
viteVersion: getViteVersion(),
|
viteVersion: getViteVersion(),
|
||||||
nodeVersion: process.version.replace(/^v?/, ''),
|
nodeVersion: process.version.replace(/^v?/, ''),
|
||||||
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
|
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
|
||||||
// Config Values
|
// Config Values
|
||||||
config: configValues,
|
config: configValues,
|
||||||
flags: cliFlags,
|
flags: cliFlags,
|
||||||
// Optional integrations
|
|
||||||
optionalIntegrations: userConfig?.integrations?.length - integrations?.length,
|
|
||||||
};
|
};
|
||||||
return [{ eventName: EVENT_SESSION, payload }];
|
return [{ eventName: EVENT_SESSION, payload }];
|
||||||
}
|
}
|
|
@ -18,7 +18,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -37,7 +36,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -54,7 +52,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -74,7 +71,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -88,7 +84,6 @@ describe('Session event', () => {
|
||||||
publicDir: 1,
|
publicDir: 1,
|
||||||
markdown: {
|
markdown: {
|
||||||
drafts: true,
|
drafts: true,
|
||||||
mode: 'mdx',
|
|
||||||
shikiConfig: {
|
shikiConfig: {
|
||||||
lang: 1,
|
lang: 1,
|
||||||
theme: 2,
|
theme: 2,
|
||||||
|
@ -102,7 +97,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -110,7 +104,6 @@ describe('Session event', () => {
|
||||||
'publicDir',
|
'publicDir',
|
||||||
'markdown',
|
'markdown',
|
||||||
'markdown.drafts',
|
'markdown.drafts',
|
||||||
'markdown.mode',
|
|
||||||
'markdown.shikiConfig',
|
'markdown.shikiConfig',
|
||||||
'markdown.shikiConfig.lang',
|
'markdown.shikiConfig.lang',
|
||||||
'markdown.shikiConfig.theme',
|
'markdown.shikiConfig.theme',
|
||||||
|
@ -121,22 +114,6 @@ describe('Session event', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mode', () => {
|
|
||||||
const config = {
|
|
||||||
markdown: {
|
|
||||||
mode: 'mdx',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const [{ payload }] = events.eventCliSession(
|
|
||||||
{
|
|
||||||
cliCommand: 'dev',
|
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
|
||||||
config
|
|
||||||
);
|
|
||||||
expect(payload.config.markdown.mode).to.equal('mdx');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('syntaxHighlight', () => {
|
it('syntaxHighlight', () => {
|
||||||
const config = {
|
const config = {
|
||||||
markdown: {
|
markdown: {
|
||||||
|
@ -146,7 +123,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -172,7 +148,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -203,7 +178,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -231,7 +205,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -262,7 +235,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -293,7 +265,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -322,7 +293,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -348,7 +318,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -373,7 +342,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -398,7 +366,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
@ -411,38 +378,18 @@ describe('Session event', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('config.integrations + optionalIntegrations', () => {
|
it('falsy integrations', () => {
|
||||||
it('optional/conditional integrations', () => {
|
const config = {
|
||||||
const config = {
|
srcDir: 1,
|
||||||
srcDir: 1,
|
integrations: [null, undefined, false],
|
||||||
integrations: [null, undefined, { name: 'example-integration' }],
|
};
|
||||||
};
|
const [{ payload }] = events.eventCliSession(
|
||||||
const [{ payload }] = events.eventCliSession(
|
{
|
||||||
{
|
cliCommand: 'dev',
|
||||||
cliCommand: 'dev',
|
},
|
||||||
astroVersion: '0.0.0',
|
config
|
||||||
},
|
);
|
||||||
config
|
expect(payload.config.integrations.length).to.equal(0);
|
||||||
);
|
|
||||||
expect(payload.config.integrations).deep.equal(['example-integration']);
|
|
||||||
expect(payload.optionalIntegrations).to.equal(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('falsy integrations', () => {
|
|
||||||
const config = {
|
|
||||||
srcDir: 1,
|
|
||||||
integrations: [null, undefined, false],
|
|
||||||
};
|
|
||||||
const [{ payload }] = events.eventCliSession(
|
|
||||||
{
|
|
||||||
cliCommand: 'dev',
|
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
|
||||||
config
|
|
||||||
);
|
|
||||||
expect(payload.config.integrations.length).to.equal(0);
|
|
||||||
expect(payload.optionalIntegrations).to.equal(3);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('flags', () => {
|
describe('flags', () => {
|
||||||
|
@ -461,7 +408,6 @@ describe('Session event', () => {
|
||||||
const [{ payload }] = events.eventCliSession(
|
const [{ payload }] = events.eventCliSession(
|
||||||
{
|
{
|
||||||
cliCommand: 'dev',
|
cliCommand: 'dev',
|
||||||
astroVersion: '0.0.0',
|
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
flags
|
flags
|
1
packages/telemetry/events.d.ts
vendored
1
packages/telemetry/events.d.ts
vendored
|
@ -1 +0,0 @@
|
||||||
export * from './dist/types/events';
|
|
|
@ -14,7 +14,6 @@
|
||||||
"homepage": "https://astro.build",
|
"homepage": "https://astro.build",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
"./events": "./dist/events/index.js",
|
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { isCI, name as ciName } from 'ci-info';
|
|
||||||
import isDocker from 'is-docker';
|
|
||||||
import isWSL from 'is-wsl';
|
|
||||||
import os from 'node:os';
|
|
||||||
|
|
||||||
type AnonymousMeta = {
|
|
||||||
systemPlatform: NodeJS.Platform;
|
|
||||||
systemRelease: string;
|
|
||||||
systemArchitecture: string;
|
|
||||||
cpuCount: number;
|
|
||||||
cpuModel: string | null;
|
|
||||||
cpuSpeed: number | null;
|
|
||||||
memoryInMb: number;
|
|
||||||
isDocker: boolean;
|
|
||||||
isWSL: boolean;
|
|
||||||
isCI: boolean;
|
|
||||||
ciName: string | null;
|
|
||||||
astroVersion: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
let meta: AnonymousMeta | undefined;
|
|
||||||
|
|
||||||
export function getAnonymousMeta(astroVersion: string): AnonymousMeta {
|
|
||||||
if (meta) {
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cpus = os.cpus() || [];
|
|
||||||
meta = {
|
|
||||||
// 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(),
|
|
||||||
isWSL,
|
|
||||||
isCI,
|
|
||||||
ciName,
|
|
||||||
astroVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
return meta!;
|
|
||||||
}
|
|
8
packages/telemetry/src/config-keys.ts
Normal file
8
packages/telemetry/src/config-keys.ts
Normal file
|
@ -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`;
|
|
@ -7,7 +7,6 @@ import process from 'node:process';
|
||||||
|
|
||||||
export interface ConfigOptions {
|
export interface ConfigOptions {
|
||||||
name: string;
|
name: string;
|
||||||
defaults: Map<string, any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapted from https://github.com/sindresorhus/env-paths
|
// Adapted from https://github.com/sindresorhus/env-paths
|
||||||
|
@ -32,7 +31,7 @@ function getConfigDir(name: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Config {
|
export class GlobalConfig {
|
||||||
private dir: string;
|
private dir: string;
|
||||||
private file: string;
|
private file: string;
|
||||||
|
|
||||||
|
@ -49,9 +48,6 @@ export class Config {
|
||||||
this._store = JSON.parse(fs.readFileSync(this.file).toString());
|
this._store = JSON.parse(fs.readFileSync(this.file).toString());
|
||||||
} else {
|
} else {
|
||||||
const store = {};
|
const store = {};
|
||||||
for (const [key, value] of this.project.defaults) {
|
|
||||||
dset(store, key, value);
|
|
||||||
}
|
|
||||||
this._store = store;
|
this._store = store;
|
||||||
this.write();
|
this.write();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
// See https://github.com/vercel/next.js/blob/canary/packages/next/telemetry/events/build.ts
|
|
||||||
export {};
|
|
|
@ -1,44 +1,27 @@
|
||||||
import type { BinaryLike } from 'node:crypto';
|
|
||||||
import { createHash, randomBytes } from 'node:crypto';
|
|
||||||
|
|
||||||
import { isCI } from 'ci-info';
|
import { isCI } from 'ci-info';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
// @ts-ignore
|
import { randomBytes } from 'node:crypto';
|
||||||
import gitUp from 'git-up';
|
import * as KEY from './config-keys.js';
|
||||||
|
import { GlobalConfig } from './config.js';
|
||||||
import { getAnonymousMeta } from './anonymous-meta.js';
|
|
||||||
import { Config } from './config.js';
|
|
||||||
import * as KEY from './keys.js';
|
|
||||||
import { post } from './post.js';
|
import { post } from './post.js';
|
||||||
import { getRawProjectId } from './project-id.js';
|
import { getProjectInfo, ProjectInfo } from './project-info.js';
|
||||||
|
import { getSystemInfo, SystemInfo } from './system-info.js';
|
||||||
export interface AstroTelemetryOptions {
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export type AstroTelemetryOptions = { version: string };
|
||||||
export type TelemetryEvent = { eventName: string; payload: Record<string, any> };
|
export type TelemetryEvent = { eventName: string; payload: Record<string, any> };
|
||||||
|
|
||||||
interface EventContext {
|
interface EventContext {
|
||||||
anonymousId: string;
|
anonymousId: string;
|
||||||
projectId: string;
|
anonymousProjectId: string;
|
||||||
projectMetadata: any;
|
anonymousSessionId: string;
|
||||||
sessionId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EventMeta extends SystemInfo {
|
||||||
|
isGit: boolean;
|
||||||
|
}
|
||||||
export class AstroTelemetry {
|
export class AstroTelemetry {
|
||||||
private rawProjectId = getRawProjectId();
|
private _anonymousSessionId: string | undefined;
|
||||||
private sessionId = randomBytes(32).toString('hex');
|
private _anonymousProjectInfo: ProjectInfo | undefined;
|
||||||
private config = new Config({
|
private config = new GlobalConfig({ name: 'astro' });
|
||||||
name: 'astro',
|
|
||||||
// Use getter to defer generation of defaults unless needed
|
|
||||||
get defaults() {
|
|
||||||
return new Map<string, any>([
|
|
||||||
[KEY.TELEMETRY_ENABLED, true],
|
|
||||||
[KEY.TELEMETRY_SALT, randomBytes(16).toString('hex')],
|
|
||||||
[KEY.TELEMETRY_ID, randomBytes(32).toString('hex')],
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
private debug = debug('astro:telemetry');
|
private debug = debug('astro:telemetry');
|
||||||
|
|
||||||
private get astroVersion() {
|
private get astroVersion() {
|
||||||
|
@ -53,65 +36,47 @@ export class AstroTelemetry {
|
||||||
|
|
||||||
constructor(private opts: AstroTelemetryOptions) {
|
constructor(private opts: AstroTelemetryOptions) {
|
||||||
// TODO: When the process exits, flush any queued promises
|
// TODO: When the process exits, flush any queued promises
|
||||||
// This line caused a "cannot exist astro" error, needs to be revisited.
|
// This caused a "cannot exist astro" error when it ran, so it was removed.
|
||||||
// process.on('SIGINT', () => this.flush());
|
// process.on('SIGINT', () => this.flush());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util to get value from config or set it if missing
|
/**
|
||||||
private getWithFallback<T>(key: string, value: T): T {
|
* Get value from either the global config or the provided fallback.
|
||||||
const val = this.config.get(key);
|
* If value is not set, the fallback is saved to the global config,
|
||||||
if (val) {
|
* persisted for later sessions.
|
||||||
return val;
|
*/
|
||||||
|
private getConfigWithFallback<T>(key: string, getValue: () => T): T {
|
||||||
|
const currentValue = this.config.get(key);
|
||||||
|
if (currentValue) {
|
||||||
|
return currentValue;
|
||||||
}
|
}
|
||||||
this.config.set(key, value);
|
const newValue = getValue();
|
||||||
return value;
|
this.config.set(key, newValue);
|
||||||
|
return newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get salt(): string {
|
|
||||||
return this.getWithFallback(KEY.TELEMETRY_SALT, randomBytes(16).toString('hex'));
|
|
||||||
}
|
|
||||||
private get enabled(): boolean {
|
private get enabled(): boolean {
|
||||||
return this.getWithFallback(KEY.TELEMETRY_ENABLED, true);
|
return this.getConfigWithFallback(KEY.TELEMETRY_ENABLED, () => true);
|
||||||
}
|
|
||||||
private get anonymousId(): string {
|
|
||||||
return this.getWithFallback(KEY.TELEMETRY_ID, randomBytes(32).toString('hex'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get notifyDate(): string {
|
private get notifyDate(): string {
|
||||||
return this.getWithFallback(KEY.TELEMETRY_NOTIFY_DATE, '');
|
return this.getConfigWithFallback(KEY.TELEMETRY_NOTIFY_DATE, () => '');
|
||||||
}
|
}
|
||||||
|
|
||||||
private hash(payload: BinaryLike): string {
|
private get anonymousId(): string {
|
||||||
const hash = createHash('sha256');
|
return this.getConfigWithFallback(KEY.TELEMETRY_ID, () => randomBytes(32).toString('hex'));
|
||||||
hash.update(payload);
|
|
||||||
return hash.digest('hex');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a ONE-WAY hash so there is no way for Astro to decode the value later.
|
private get anonymousSessionId(): string {
|
||||||
private oneWayHash(payload: BinaryLike): string {
|
// NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
|
||||||
const hash = createHash('sha256');
|
this._anonymousSessionId = this._anonymousSessionId || randomBytes(32).toString('hex');
|
||||||
// Always prepend the payload value with salt! This ensures the hash is one-way.
|
return this._anonymousSessionId;
|
||||||
hash.update(this.salt);
|
|
||||||
hash.update(payload);
|
|
||||||
return hash.digest('hex');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instead of sending `rawProjectId`, we only ever reference a hashed value *derived*
|
private get anonymousProjectInfo(): ProjectInfo {
|
||||||
// from `rawProjectId`. This ensures that `projectId` is ALWAYS anonymous and can't
|
// NOTE(fks): this value isn't global, so it can't use getConfigWithFallback().
|
||||||
// be reversed from the hashed value.
|
this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(isCI);
|
||||||
private get projectId(): string {
|
return this._anonymousProjectInfo;
|
||||||
return this.oneWayHash(this.rawProjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get projectMetadata(): undefined | { owner: string; name: string } {
|
|
||||||
const projectId = this.rawProjectId;
|
|
||||||
if (projectId === process.cwd()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { pathname, resource } = gitUp(projectId);
|
|
||||||
const parts = pathname.split('/').slice(1);
|
|
||||||
const owner = `${resource}${parts[0]}`;
|
|
||||||
const name = parts[1].replace('.git', '');
|
|
||||||
return { owner: this.hash(owner), name: this.hash(name) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get isDisabled(): boolean {
|
private get isDisabled(): boolean {
|
||||||
|
@ -129,13 +94,6 @@ export class AstroTelemetry {
|
||||||
return this.config.clear();
|
return this.config.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private queue: Promise<any>[] = [];
|
|
||||||
|
|
||||||
// Wait for any in-flight promises to resolve
|
|
||||||
private async flush() {
|
|
||||||
await Promise.all(this.queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
async notify(callback: () => Promise<boolean>) {
|
async notify(callback: () => Promise<boolean>) {
|
||||||
if (this.isDisabled || isCI) {
|
if (this.isDisabled || isCI) {
|
||||||
return;
|
return;
|
||||||
|
@ -172,22 +130,24 @@ export class AstroTelemetry {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const meta: EventMeta = {
|
||||||
|
...getSystemInfo(this.astroVersion),
|
||||||
|
isGit: this.anonymousProjectInfo.isGit,
|
||||||
|
};
|
||||||
|
|
||||||
const context: EventContext = {
|
const context: EventContext = {
|
||||||
anonymousId: this.anonymousId,
|
anonymousId: this.anonymousId,
|
||||||
projectId: this.projectId,
|
anonymousProjectId: this.anonymousProjectInfo.anonymousProjectId,
|
||||||
projectMetadata: this.projectMetadata,
|
anonymousSessionId: this.anonymousSessionId,
|
||||||
sessionId: this.sessionId,
|
|
||||||
};
|
};
|
||||||
const meta = getAnonymousMeta(this.astroVersion);
|
|
||||||
|
|
||||||
const req = post({
|
return post({
|
||||||
context,
|
context,
|
||||||
meta,
|
meta,
|
||||||
events,
|
events,
|
||||||
}).then(() => {
|
}).catch((err) => {
|
||||||
this.queue = this.queue.filter((r) => r !== req);
|
// Log the error to the debugger, but otherwise do nothing.
|
||||||
|
this.debug(`Error sending event: ${err.message}`);
|
||||||
});
|
});
|
||||||
this.queue.push(req);
|
|
||||||
return req;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
// This is the key that stores whether or not telemetry is enabled or disabled.
|
|
||||||
export const TELEMETRY_ENABLED = 'telemetry.enabled';
|
|
||||||
|
|
||||||
// This is the key that specifies when the user was informed about anonymous
|
|
||||||
// telemetry collection.
|
|
||||||
export const TELEMETRY_NOTIFY_DATE = 'telemetry.notifiedAt';
|
|
||||||
|
|
||||||
// This is a quasi-persistent identifier used to dedupe recurring events. It's
|
|
||||||
// generated from random data and completely anonymous.
|
|
||||||
export const TELEMETRY_ID = `telemetry.anonymousId`;
|
|
||||||
|
|
||||||
// This is the cryptographic salt that is included within every hashed value.
|
|
||||||
// This salt value is never sent to us, ensuring privacy and the one-way nature
|
|
||||||
// of the hash (prevents dictionary lookups of pre-computed hashes).
|
|
||||||
// See the `oneWayHash` function.
|
|
||||||
export const TELEMETRY_SALT = `telemetry.salt`;
|
|
|
@ -1,13 +1,11 @@
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
const ASTRO_TELEMETRY_ENDPOINT = `https://telemetry.astro.build/api/v1/record`;
|
|
||||||
const noop = () => {};
|
|
||||||
|
|
||||||
export function post(body: Record<string, any>) {
|
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, {
|
return fetch(ASTRO_TELEMETRY_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
})
|
});
|
||||||
.catch(noop)
|
|
||||||
.then(noop, noop);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { execSync } from 'child_process';
|
|
||||||
|
|
||||||
// Why does Astro need a project ID? Why is it looking at my git remote?
|
|
||||||
// ---
|
|
||||||
// Astro's telemetry is and always will be completely anonymous.
|
|
||||||
// Differentiating unique projects helps us track feature usage accurately.
|
|
||||||
//
|
|
||||||
// We **never** read your actual git remote! The value is hashed one-way
|
|
||||||
// with random salt data, making it impossible for us to reverse or try to
|
|
||||||
// guess the remote by re-computing hashes.
|
|
||||||
|
|
||||||
function getProjectIdFromGit() {
|
|
||||||
try {
|
|
||||||
const originBuffer = execSync(`git config --local --get remote.origin.url`, {
|
|
||||||
timeout: 1000,
|
|
||||||
stdio: `pipe`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return String(originBuffer).trim();
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRawProjectId(): string {
|
|
||||||
return getProjectIdFromGit() ?? process.env.REPOSITORY_URL ?? process.cwd();
|
|
||||||
}
|
|
87
packages/telemetry/src/project-info.ts
Normal file
87
packages/telemetry/src/project-info.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import type { BinaryLike } from 'node:crypto';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
/* true if your project is connected to a git repository. false otherwise. */
|
||||||
|
isGit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [0, 'pipe', 0]});
|
||||||
|
return String(originBuffer).trim();
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectInfo(isCI: boolean): ProjectInfo {
|
||||||
|
const projectIdFromGit = getProjectIdFromGit();
|
||||||
|
if (projectIdFromGit) {
|
||||||
|
return {
|
||||||
|
isGit: true,
|
||||||
|
anonymousProjectId: createAnonymousValue(projectIdFromGit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isGit: false,
|
||||||
|
anonymousProjectId: isCI ? '' : process.cwd(),
|
||||||
|
};
|
||||||
|
}
|
72
packages/telemetry/src/system-info.ts
Normal file
72
packages/telemetry/src/system-info.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { isCI, name as ciName } from 'ci-info';
|
||||||
|
import isDocker from 'is-docker';
|
||||||
|
import isWSL from 'is-wsl';
|
||||||
|
import os from 'node:os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
cpuCount: number;
|
||||||
|
cpuModel: string | null;
|
||||||
|
cpuSpeed: number | null;
|
||||||
|
memoryInMb: number;
|
||||||
|
isDocker: boolean;
|
||||||
|
isWSL: boolean;
|
||||||
|
isCI: boolean;
|
||||||
|
ciName: string | null;
|
||||||
|
astroVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta: SystemInfo | undefined;
|
||||||
|
|
||||||
|
export function getSystemInfo(astroVersion: string): SystemInfo {
|
||||||
|
if (meta) {
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpus = os.cpus() || [];
|
||||||
|
meta = {
|
||||||
|
// 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(),
|
||||||
|
isWSL,
|
||||||
|
isCI,
|
||||||
|
ciName,
|
||||||
|
astroVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
return meta!;
|
||||||
|
}
|
9
packages/telemetry/test/config.test.js
Normal file
9
packages/telemetry/test/config.test.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {GlobalConfig} from '../dist/config.js';
|
||||||
|
|
||||||
|
describe('GlobalConfig', () => {
|
||||||
|
it('initializes when expected arguments are given', () => {
|
||||||
|
const config = new GlobalConfig({ name: 'TEST_NAME' });
|
||||||
|
expect(config).to.be.instanceOf(GlobalConfig);
|
||||||
|
});
|
||||||
|
});
|
9
packages/telemetry/test/index.test.js
Normal file
9
packages/telemetry/test/index.test.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import {AstroTelemetry} from '../dist/index.js';
|
||||||
|
|
||||||
|
describe('AstroTelemetry', () => {
|
||||||
|
it('initializes when expected arguments are given', () => {
|
||||||
|
const telemetry = new AstroTelemetry({ version: '0.0.0-test.1' });
|
||||||
|
expect(telemetry).to.be.instanceOf(AstroTelemetry);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue