improve telemetry (#8600)

This commit is contained in:
Fred K. Schott 2023-09-20 09:50:04 -07:00 committed by GitHub
parent 0119a271ae
commit ed54d46449
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 426 deletions

View file

@ -0,0 +1,6 @@
---
'@astrojs/telemetry': patch
'astro': patch
---
Improve config info telemetry

View file

@ -9,7 +9,12 @@ import ora from 'ora';
import preferredPM from 'preferred-pm';
import prompts from 'prompts';
import type yargs from 'yargs-parser';
import { loadTSConfig, resolveConfigPath, resolveRoot } from '../../core/config/index.js';
import {
loadTSConfig,
resolveConfig,
resolveConfigPath,
resolveRoot,
} from '../../core/config/index.js';
import {
defaultTSConfig,
presets,
@ -23,7 +28,7 @@ import { appendForwardSlash } from '../../core/path.js';
import { apply as applyPolyfill } from '../../core/polyfill.js';
import { parseNpmName } from '../../core/util.js';
import { eventCliSession, telemetry } from '../../events/index.js';
import { createLoggerFromFlags } from '../flags.js';
import { createLoggerFromFlags, flagsToAstroInlineConfig } from '../flags.js';
import { generate, parse, t, visit } from './babel.js';
import { ensureImport } from './imports.js';
import { wrapDefaultExport } from './wrapper.js';
@ -87,7 +92,9 @@ async function getRegistry(): Promise<string> {
}
export async function add(names: string[], { flags }: AddOptions) {
telemetry.record(eventCliSession('add'));
const inlineConfig = flagsToAstroInlineConfig(flags);
const { userConfig } = await resolveConfig(inlineConfig, 'add');
telemetry.record(eventCliSession('add', userConfig));
applyPolyfill();
if (flags.help || names.length === 0) {
printHelp({

View file

@ -140,7 +140,6 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.build.excludeMiddleware),
})
.optional()
.default({}),
server: z.preprocess(
// preprocess
@ -158,7 +157,6 @@ export const AstroConfigSchema = z.object({
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
headers: z.custom<OutgoingHttpHeaders>().optional(),
})
.optional()
.default({})
),
redirects: z
@ -274,27 +272,11 @@ export const AstroConfigSchema = z.object({
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
})
.passthrough()
.refine(
(d) => {
const validKeys = Object.keys(ASTRO_CONFIG_DEFAULTS.experimental);
const invalidKeys = Object.keys(d).filter((key) => !validKeys.includes(key));
if (invalidKeys.length > 0) return false;
return true;
},
(d) => {
const validKeys = Object.keys(ASTRO_CONFIG_DEFAULTS.experimental);
const invalidKeys = Object.keys(d).filter((key) => !validKeys.includes(key));
return {
message: `Invalid experimental key: \`${invalidKeys.join(
', '
)}\`. \nMake sure the spelling is correct, and that your Astro version supports this experiment.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for more information.`,
};
}
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
)
.optional()
.default({}),
legacy: z.object({}).optional().default({}),
legacy: z.object({}).default({}),
});
export type AstroConfigType = z.infer<typeof AstroConfigSchema>;

View file

@ -1,25 +1,8 @@
import type { AstroUserConfig } from '../@types/astro.js';
import type { AstroIntegration, AstroUserConfig } from '../@types/astro.js';
import { AstroConfigSchema } from '../core/config/schema.js';
const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED';
interface ConfigInfo {
markdownPlugins: string[];
adapter: string | null;
integrations: string[];
trailingSlash: undefined | 'always' | 'never' | 'ignore';
build:
| undefined
| {
format: undefined | 'file' | 'directory';
};
markdown:
| undefined
| {
drafts: undefined | boolean;
syntaxHighlight: undefined | 'shiki' | 'prism' | false;
};
}
interface EventPayload {
cliCommand: string;
config?: ConfigInfo;
@ -28,87 +11,126 @@ interface EventPayload {
optionalIntegrations?: number;
}
const multiLevelKeys = new Set([
'build',
'markdown',
'markdown.shikiConfig',
'server',
'vite',
'vite.resolve',
'vite.css',
'vite.json',
'vite.server',
'vite.server.fs',
'vite.build',
'vite.preview',
'vite.optimizeDeps',
'vite.ssr',
'vite.worker',
]);
function configKeys(obj: Record<string, any> | undefined, parentKey: string): string[] {
if (!obj) {
return [];
type ConfigInfoValue = string | boolean | string[] | undefined;
type ConfigInfoRecord = Record<string, ConfigInfoValue>;
type ConfigInfoBase = {
[alias in keyof AstroUserConfig]: ConfigInfoValue | ConfigInfoRecord;
};
export interface ConfigInfo extends ConfigInfoBase {
build: ConfigInfoRecord;
image: ConfigInfoRecord;
markdown: ConfigInfoRecord;
experimental: ConfigInfoRecord;
legacy: ConfigInfoRecord;
vite: ConfigInfoRecord | undefined;
}
function measureIsDefined(val: unknown) {
// if val is undefined, measure undefined as a value
if (val === undefined) {
return undefined;
}
// otherwise, convert the value to a boolean
return Boolean(val);
}
return Object.entries(obj)
.map(([key, value]) => {
if (typeof value === 'object' && !Array.isArray(value)) {
const localKey = parentKey ? parentKey + '.' + key : key;
if (multiLevelKeys.has(localKey)) {
let keys = configKeys(value, localKey).map((subkey) => key + '.' + subkey);
keys.unshift(key);
return keys;
}
}
type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
return key;
})
.flat(1);
/**
* Measure supports string literal values. Passing a generic `string` type
* results in an error, to make sure generic user input is never measured directly.
*/
function measureStringLiteral<T extends string>(
val: StringLiteral<T> | boolean | undefined
): string | boolean | undefined {
return val;
}
function measureIntegration(val: AstroIntegration | false | null | undefined): string | undefined {
if (!val || !val.name) {
return undefined;
}
return val.name;
}
function sanitizeConfigInfo(obj: object | undefined, validKeys: string[]): ConfigInfoRecord {
if (!obj || validKeys.length === 0) {
return {};
}
return validKeys.reduce(
(result, key) => {
result[key] = measureIsDefined((obj as Record<string, unknown>)[key]);
return result;
},
{} as Record<string, boolean | undefined>
);
}
/**
* This function creates an anonymous ConfigInfo object from the user's config.
* All values are sanitized to preserve anonymity. Simple "exist" boolean checks
* are used by default, with a few additional sanitized values added manually.
* Helper functions should always be used to ensure correct sanitization.
*/
function createAnonymousConfigInfo(userConfig: AstroUserConfig) {
// Sanitize and measure the generic config object
// NOTE(fks): Using _def is the correct, documented way to get the `shape`
// from a Zod object that includes a wrapping default(), optional(), etc.
// Even though `_def` appears private, it is type-checked for us so that
// any changes between versions will be detected.
const configInfo: ConfigInfo = {
...sanitizeConfigInfo(userConfig, Object.keys(AstroConfigSchema.shape)),
build: sanitizeConfigInfo(
userConfig.build,
Object.keys(AstroConfigSchema.shape.build._def.innerType.shape)
),
image: sanitizeConfigInfo(
userConfig.image,
Object.keys(AstroConfigSchema.shape.image._def.innerType.shape)
),
markdown: sanitizeConfigInfo(
userConfig.markdown,
Object.keys(AstroConfigSchema.shape.markdown._def.innerType.shape)
),
experimental: sanitizeConfigInfo(
userConfig.experimental,
Object.keys(AstroConfigSchema.shape.experimental._def.innerType.shape)
),
legacy: sanitizeConfigInfo(
userConfig.legacy,
Object.keys(AstroConfigSchema.shape.legacy._def.innerType.shape)
),
vite: userConfig.vite
? sanitizeConfigInfo(userConfig.vite, Object.keys(userConfig.vite))
: undefined,
};
// Measure string literal/enum configuration values
configInfo.build.format = measureStringLiteral(userConfig.build?.format);
configInfo.markdown.syntaxHighlight = measureStringLiteral(userConfig.markdown?.syntaxHighlight);
configInfo.output = measureStringLiteral(userConfig.output);
configInfo.scopedStyleStrategy = measureStringLiteral(userConfig.scopedStyleStrategy);
configInfo.trailingSlash = measureStringLiteral(userConfig.trailingSlash);
// Measure integration & adapter usage
configInfo.adapter = measureIntegration(userConfig.adapter);
configInfo.integrations = userConfig.integrations
?.flat(100)
.map(measureIntegration)
.filter(Boolean) as string[];
// Return the sanitized ConfigInfo object
return configInfo;
}
export function eventCliSession(
cliCommand: string,
userConfig?: AstroUserConfig,
userConfig: AstroUserConfig,
flags?: Record<string, any>
): { eventName: string; payload: EventPayload }[] {
// Filter out falsy integrations
const configValues = userConfig
? {
markdownPlugins: [
...(userConfig?.markdown?.remarkPlugins?.map((p) =>
typeof p === 'string' ? p : typeof p
) ?? []),
...(userConfig?.markdown?.rehypePlugins?.map((p) =>
typeof p === 'string' ? p : typeof p
) ?? []),
] as string[],
adapter: userConfig?.adapter?.name ?? null,
integrations: (userConfig?.integrations ?? [])
.filter(Boolean)
.flat()
.map((i: any) => i?.name),
trailingSlash: userConfig?.trailingSlash,
build: userConfig?.build
? {
format: userConfig?.build?.format,
}
: undefined,
markdown: userConfig?.markdown
? {
drafts: userConfig.markdown?.drafts,
syntaxHighlight: userConfig.markdown?.syntaxHighlight,
}
: undefined,
}
: undefined;
// Filter out yargs default `_` flag which is the cli command
const cliFlags = flags ? Object.keys(flags).filter((name) => name != '_') : undefined;
const payload: EventPayload = {
cliCommand,
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
config: configValues,
config: createAnonymousConfigInfo(userConfig),
flags: cliFlags,
};
return [{ eventName: EVENT_SESSION, payload }];

View file

@ -5,44 +5,8 @@ import * as events from '../dist/events/index.js';
describe('Events', () => {
describe('eventCliSession()', () => {
it('All top-level keys added', () => {
const config = {
root: 1,
srcDir: 2,
publicDir: 3,
outDir: 4,
site: 5,
base: 6,
trailingSlash: 7,
experimental: 8,
};
const expected = Object.keys(config);
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).to.deep.equal(expected);
});
it('configKeys includes format', () => {
const config = {
srcDir: 1,
build: {
format: 'file',
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).to.deep.equal(['srcDir', 'build', 'build.format']);
});
it('config.build.format', () => {
it('string literal "build.format" is included', () => {
const config = {
srcDir: 1,
build: {
@ -58,59 +22,8 @@ describe('Events', () => {
expect(payload.config.build.format).to.equal('file');
});
it('configKeys includes server props', () => {
const config = {
srcDir: 1,
server: {
host: 'example.com',
port: 8033,
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).to.deep.equal(['srcDir', 'server', 'server.host', 'server.port']);
});
it('configKeys is deep', () => {
const config = {
publicDir: 1,
markdown: {
drafts: true,
shikiConfig: {
lang: 1,
theme: 2,
wrap: 3,
},
syntaxHighlight: 'shiki',
remarkPlugins: [],
rehypePlugins: [],
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).to.deep.equal([
'publicDir',
'markdown',
'markdown.drafts',
'markdown.shikiConfig',
'markdown.shikiConfig.lang',
'markdown.shikiConfig.theme',
'markdown.shikiConfig.wrap',
'markdown.syntaxHighlight',
'markdown.remarkPlugins',
'markdown.rehypePlugins',
]);
});
it('syntaxHighlight', () => {
it('string literal "markdown.syntaxHighlight" is included', () => {
const config = {
markdown: {
syntaxHighlight: 'shiki',
@ -145,233 +58,16 @@ describe('Events', () => {
},
config
);
expect(payload.configKeys).is.deep.equal([
'root',
'vite',
'vite.css',
'vite.css.modules',
'vite.base',
'vite.mode',
'vite.define',
'vite.publicDir',
expect(Object.keys(payload.config.vite)).is.deep.equal([
'css',
'base',
'mode',
'define',
'publicDir',
]);
});
it('vite.resolve keys are captured', async () => {
const config = {
vite: {
resolve: {
alias: {
a: 'b',
},
dedupe: ['one', 'two'],
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.resolve',
'vite.resolve.alias',
'vite.resolve.dedupe',
]);
});
it('vite.css keys are captured', async () => {
const config = {
vite: {
resolve: {
dedupe: ['one', 'two'],
},
css: {
modules: [],
postcss: {},
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.resolve',
'vite.resolve.dedupe',
'vite.css',
'vite.css.modules',
'vite.css.postcss',
]);
});
it('vite.server keys are captured', async () => {
const config = {
vite: {
server: {
host: 'example.com',
open: true,
fs: {
strict: true,
allow: ['a', 'b'],
},
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.server',
'vite.server.host',
'vite.server.open',
'vite.server.fs',
'vite.server.fs.strict',
'vite.server.fs.allow',
]);
});
it('vite.build keys are captured', async () => {
const config = {
vite: {
build: {
target: 'one',
outDir: 'some/dir',
cssTarget: {
one: 'two',
},
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.build',
'vite.build.target',
'vite.build.outDir',
'vite.build.cssTarget',
]);
});
it('vite.preview keys are captured', async () => {
const config = {
vite: {
preview: {
host: 'example.com',
port: 8080,
another: {
a: 'b',
},
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.preview',
'vite.preview.host',
'vite.preview.port',
'vite.preview.another',
]);
});
it('vite.optimizeDeps keys are captured', async () => {
const config = {
vite: {
optimizeDeps: {
entries: ['one', 'two'],
exclude: ['secret', 'name'],
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.optimizeDeps',
'vite.optimizeDeps.entries',
'vite.optimizeDeps.exclude',
]);
});
it('vite.ssr keys are captured', async () => {
const config = {
vite: {
ssr: {
external: ['a'],
target: { one: 'two' },
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.ssr',
'vite.ssr.external',
'vite.ssr.target',
]);
});
it('vite.worker keys are captured', async () => {
const config = {
vite: {
worker: {
format: { a: 'b' },
plugins: ['a', 'b'],
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
},
config
);
expect(payload.configKeys).is.deep.equal([
'vite',
'vite.worker',
'vite.worker.format',
'vite.worker.plugins',
]);
});
it('falsy integrations', () => {
it('falsy integrations are handled', () => {
const config = {
srcDir: 1,
integrations: [null, undefined, false],
@ -385,7 +81,7 @@ describe('Events', () => {
expect(payload.config.integrations.length).to.equal(0);
});
it('finds names for integration arrays', () => {
it('only integration names are included', () => {
const config = {
integrations: [{ name: 'foo' }, [{ name: 'bar' }, { name: 'baz' }]],
};
@ -393,6 +89,14 @@ describe('Events', () => {
expect(payload.config.integrations).to.deep.equal(['foo', 'bar', 'baz']);
});
it('only adapter name is included', () => {
const config = {
adapter: {name: 'ADAPTER_NAME'},
};
const [{ payload }] = events.eventCliSession({ cliCommand: 'dev' }, config);
expect(payload.config.adapter).to.equal('ADAPTER_NAME');
});
it('includes cli flags in payload', () => {
const config = {};
const flags = {

View file

@ -1,9 +1,17 @@
# Astro Telemetry
This package is used to collect anonymous telemetry data within the Astro CLI. It is enabled by default. Telemetry data does not contain any personal identifying information and can be disabled via:
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
```
See the [CLI documentation](https://docs.astro.build/en/reference/cli-reference/#astro-telemetry) for more options on configuration telemetry.
```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.