Add new configKeys prop for telemetry (#3299)

* Add new configKeys prop for telemetry

This property lets us known which AstroConfig keys are being used, for anonymous telemetry.

* Adds a changeset

* Restructure how the telemetry event is shaped
This commit is contained in:
Matthew Phillips 2022-05-06 09:22:27 -04:00 committed by GitHub
parent f159c18bf5
commit 8021998bb6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 521 additions and 262 deletions

View file

@ -0,0 +1,6 @@
---
'astro': patch
'@astrojs/telemetry': patch
---
Update to telemetry to include AstroConfig keys used

View file

@ -17,7 +17,7 @@ import preview from '../core/preview/index.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 { loadConfig } from '../core/config.js'; import { openConfig } from '../core/config.js';
import { printHelp, formatErrorMessage, formatConfigErrorMessage } from '../core/messages.js'; import { printHelp, formatErrorMessage, formatConfigErrorMessage } from '../core/messages.js';
import { createSafeError } from '../core/util.js'; import { createSafeError } from '../core/util.js';
@ -138,14 +138,16 @@ export async function cli(args: string[]) {
} }
case 'dev': { case 'dev': {
try { try {
const config = await loadConfig({ cwd: root, flags, cmd }); const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record( telemetry.record(
event.eventCliSession( event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'dev' }, { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'dev' },
config userConfig,
flags
) )
); );
await devServer(config, { logging, telemetry }); await devServer(astroConfig, { logging, telemetry });
return await new Promise(() => {}); // lives forever return await new Promise(() => {}); // lives forever
} catch (err) { } catch (err) {
return throwAndExit(err); return throwAndExit(err);
@ -154,41 +156,44 @@ export async function cli(args: string[]) {
case 'build': { case 'build': {
try { try {
const config = await loadConfig({ cwd: root, flags, cmd }); const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record( telemetry.record(
event.eventCliSession( event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'build' }, { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'build' },
config userConfig,
flags
) )
); );
return await build(config, { logging, telemetry }); return await build(astroConfig, { logging, telemetry });
} catch (err) { } catch (err) {
return throwAndExit(err); return throwAndExit(err);
} }
} }
case 'check': { case 'check': {
const config = await loadConfig({ cwd: root, flags, cmd }); const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record( telemetry.record(
event.eventCliSession( event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'check' }, { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'check' },
config userConfig,
flags,
) )
); );
const ret = await check(config); const ret = await check(astroConfig);
return process.exit(ret); return process.exit(ret);
} }
case 'preview': { case 'preview': {
try { try {
const config = await loadConfig({ cwd: root, flags, cmd }); const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record( telemetry.record(
event.eventCliSession( event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'preview' }, { astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'preview' },
config userConfig,
flags
) )
); );
const server = await preview(config, { 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);

View file

@ -397,7 +397,59 @@ export async function resolveConfigURL(
} }
} }
/** Attempt to load an `astro.config.mjs` file */ interface OpenConfigResult {
userConfig: AstroUserConfig;
astroConfig: AstroConfig;
flags: CLIFlags;
root: string;
}
/** Load a configuration file, returning both the userConfig and astroConfig */
export async function openConfig(configOptions: LoadConfigOptions): Promise<OpenConfigResult> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
const flags = resolveFlags(configOptions.flags || {});
let userConfig: AstroUserConfig = {};
let userConfigPath: string | undefined;
if (flags?.config) {
userConfigPath = /^\.*\//.test(flags.config) ? flags.config : `./${flags.config}`;
userConfigPath = fileURLToPath(
new URL(userConfigPath, appendForwardSlash(pathToFileURL(root).toString()))
);
}
// Automatically load config file using Proload
// If `userConfigPath` is `undefined`, Proload will search for `astro.config.[cm]?[jt]s`
let config;
try {
config = await load('astro', {
mustExist: !!userConfigPath,
cwd: root,
filePath: userConfigPath,
});
} catch (err) {
if (err instanceof ProloadError && flags.config) {
throw new Error(`Unable to resolve --config "${flags.config}"! Does the file exist?`);
}
throw err;
}
if (config) {
userConfig = config.value;
}
const astroConfig = await resolveConfig(userConfig, root, flags, configOptions.cmd);
return {
astroConfig,
userConfig,
flags,
root
};
}
/**
* Attempt to load an `astro.config.mjs` file
* @deprecated
*/
export async function loadConfig(configOptions: LoadConfigOptions): Promise<AstroConfig> { export async function loadConfig(configOptions: LoadConfigOptions): Promise<AstroConfig> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd(); const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
const flags = resolveFlags(configOptions.flags || {}); const flags = resolveFlags(configOptions.flags || {});

View file

@ -6,25 +6,34 @@ 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; astroVersion: string;
cliCommand: string; cliCommand: string;
} }
interface ConfigInfo { interface ConfigInfo {
hasViteConfig: boolean;
hasBase: boolean;
viteKeys: string[];
markdownPlugins: string[]; markdownPlugins: string[];
adapter: string | null; adapter: string | null;
integrations: string[]; integrations: string[];
experimentalFeatures: string[]; trailingSlash: undefined | 'always' | 'never' | 'ignore';
build: undefined | {
format: undefined | 'file' | 'directory'
};
markdown: undefined | {
mode: undefined | 'md' | 'mdx';
syntaxHighlight: undefined | 'shiki' | 'prism' | false;
};
} }
interface EventCliSessionInternal extends EventCliSession { interface EventCliSessionInternal extends EventCliSession {
nodeVersion: string; nodeVersion: string;
viteVersion: string; viteVersion: string;
config?: ConfigInfo; config?: ConfigInfo;
configKeys?: string[];
flags?: string[];
} }
function getViteVersion() { function getViteVersion() {
@ -35,29 +44,24 @@ function getViteVersion() {
return undefined; return undefined;
} }
function getExperimentalFeatures(astroConfig?: Record<string, any>): string[] | undefined { const multiLevelKeys = new Set([
if (!astroConfig) return undefined;
return Object.entries(astroConfig.experimental || []).reduce((acc, [key, value]) => {
if (value) {
acc.push(key);
}
return acc;
}, [] as string[]);
}
const secondLevelViteKeys = new Set([
'resolve',
'css',
'json',
'server',
'server.fs',
'build', 'build',
'preview', 'markdown',
'optimizeDeps', 'markdown.shikiConfig',
'ssr', 'server',
'worker', 'vite',
'vite.resolve',
'vite.css',
'vite.json',
'vite.server',
'vite.server.fs',
'vite.build',
'vite.preview',
'vite.optimizeDeps',
'vite.ssr',
'vite.worker',
]); ]);
function viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string): string[] { function configKeys(obj: Record<string, any> | undefined, parentKey: string): string[] {
if (!obj) { if (!obj) {
return []; return [];
} }
@ -66,8 +70,8 @@ function viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string)
.map(([key, value]) => { .map(([key, value]) => {
if (typeof value === 'object' && !Array.isArray(value)) { if (typeof value === 'object' && !Array.isArray(value)) {
const localKey = parentKey ? parentKey + '.' + key : key; const localKey = parentKey ? parentKey + '.' + key : key;
if (secondLevelViteKeys.has(localKey)) { if (multiLevelKeys.has(localKey)) {
let keys = viteConfigKeys(value, localKey).map((subkey) => key + '.' + subkey); let keys = configKeys(value, localKey).map((subkey) => key + '.' + subkey);
keys.unshift(key); keys.unshift(key);
return keys; return keys;
} }
@ -80,29 +84,39 @@ function viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string)
export function eventCliSession( export function eventCliSession(
event: EventCliSession, event: EventCliSession,
astroConfig?: Record<string, any> userConfig?: AstroUserConfig,
flags?: Record<string, any>,
): { eventName: string; payload: EventCliSessionInternal }[] { ): { eventName: string; payload: EventCliSessionInternal }[] {
const configValues = userConfig ? {
markdownPlugins: [
userConfig?.markdown?.remarkPlugins ?? [],
userConfig?.markdown?.rehypePlugins ?? [],
].flat(1),
adapter: userConfig?.adapter?.name ?? null,
integrations: userConfig?.integrations?.map((i: any) => i.name) ?? [],
trailingSlash: userConfig?.trailingSlash,
build: userConfig?.build ? {
format: userConfig?.build?.format
} : undefined,
markdown: userConfig?.markdown ? {
mode: userConfig?.markdown?.mode,
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: EventCliSessionInternal = { const payload: EventCliSessionInternal = {
cliCommand: event.cliCommand, cliCommand: event.cliCommand,
// Versions // Versions
astroVersion: event.astroVersion, astroVersion: event.astroVersion,
viteVersion: getViteVersion(), viteVersion: getViteVersion(),
nodeVersion: process.version.replace(/^v?/, ''), nodeVersion: process.version.replace(/^v?/, ''),
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
// Config Values // Config Values
config: astroConfig config: configValues,
? { flags: cliFlags,
hasViteConfig: Object.keys(astroConfig?.vite).length > 0,
markdownPlugins: [
astroConfig?.markdown?.remarkPlugins ?? [],
astroConfig?.markdown?.rehypePlugins ?? [],
].flat(1),
hasBase: astroConfig?.base !== '/',
viteKeys: viteConfigKeys(astroConfig?.vite, ''),
adapter: astroConfig?.adapter?.name ?? null,
integrations: astroConfig?.integrations?.map((i: any) => i.name) ?? [],
experimentalFeatures: getExperimentalFeatures(astroConfig) ?? [],
}
: undefined,
}; };
return [{ eventName: EVENT_SESSION, payload }]; return [{ eventName: EVENT_SESSION, payload }];
} }

View file

@ -1,241 +1,423 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as events from '../dist/events/index.js'; import * as events from '../dist/events/index.js';
import { resolveConfig } from '../../astro/dist/core/config.js';
async function mockConfig(userConfig) {
return await resolveConfig(userConfig, import.meta.url, {}, 'dev');
}
describe('Session event', () => { describe('Session event', () => {
it('top-level keys are captured', async () => { describe('top-level', () => {
const config = await mockConfig({ it('All top-level keys added', () => {
vite: { const config = {
css: { modules: [] }, root: 1,
base: 'a', srcDir: 2,
mode: 'b', publicDir: 3,
define: { outDir: 4,
a: 'b', site: 5,
base: 6,
trailingSlash: 7,
experimental: 8,
};
const expected = Object.keys(config);
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
}, },
publicDir: 'some/dir', config
}, );
expect(payload.configKeys).to.deep.equal(expected);
}); });
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'css',
'css.modules',
'base',
'mode',
'define',
'publicDir',
]);
}); });
it('vite.resolve keys are captured', async () => { describe('config.build', () => {
const config = await mockConfig({ it('configKeys includes format', () => {
vite: { const config = {
resolve: { srcDir: 1,
alias: { build: {
a: 'b', format: 'file',
}, }
dedupe: ['one', 'two'], };
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0'
}, },
}, config
);
expect(payload.configKeys).to.deep.equal(['srcDir', 'build', 'build.format']);
}); });
const [{ payload }] = events.eventCliSession( it('config.build.format', () => {
{ const config = {
cliCommand: 'dev', srcDir: 1,
astroVersion: '0.0.0', build: {
}, format: 'file',
config }
); };
expect(payload.config.viteKeys).is.deep.equal(['resolve', 'resolve.alias', 'resolve.dedupe']); const [{ payload }] = events.eventCliSession(
}); {
cliCommand: 'dev',
it('vite.css keys are captured', async () => { astroVersion: '0.0.0'
const config = await mockConfig({
vite: {
resolve: {
dedupe: ['one', 'two'],
}, },
css: { config
modules: [], );
postcss: {}, expect(payload.config.build.format).to.equal('file');
},
},
}); });
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'resolve',
'resolve.dedupe',
'css',
'css.modules',
'css.postcss',
]);
}); });
it('vite.server keys are captured', async () => { describe('config.server', () => {
const config = await mockConfig({ it('configKeys includes server props', () => {
vite: { const config = {
srcDir: 1,
server: { server: {
host: 'example.com', host: 'example.com',
open: true, port: 8033
fs: { }
strict: true, };
allow: ['a', 'b'], const [{ payload }] = events.eventCliSession(
}, {
cliCommand: 'dev',
astroVersion: '0.0.0'
}, },
}, config
);
expect(payload.configKeys).to.deep.equal(['srcDir', 'server', 'server.host', 'server.port']);
}); });
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'server',
'server.host',
'server.open',
'server.fs',
'server.fs.strict',
'server.fs.allow',
]);
}); });
it('vite.build keys are captured', async () => { describe('config.markdown', () => {
const config = await mockConfig({ it('configKeys is deep', () => {
vite: { const config = {
build: { publicDir: 1,
target: 'one', markdown: {
outDir: 'some/dir', drafts: true,
cssTarget: { mode: 'mdx',
one: 'two', shikiConfig: {
lang: 1,
theme: 2,
wrap: 3
}, },
syntaxHighlight: 'shiki',
remarkPlugins: [],
rehypePlugins: [],
}
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0'
}, },
}, config
);
expect(payload.configKeys).to.deep.equal(['publicDir', 'markdown',
'markdown.drafts', 'markdown.mode', 'markdown.shikiConfig',
'markdown.shikiConfig.lang', 'markdown.shikiConfig.theme', 'markdown.shikiConfig.wrap',
'markdown.syntaxHighlight', 'markdown.remarkPlugins', 'markdown.rehypePlugins']);
}); });
const [{ payload }] = events.eventCliSession( it('mode', () => {
{ const config = {
cliCommand: 'dev', markdown: {
astroVersion: '0.0.0', mode: 'mdx',
}, }
config };
); const [{ payload }] = events.eventCliSession(
expect(payload.config.viteKeys).is.deep.equal([ {
'build', cliCommand: 'dev',
'build.target', astroVersion: '0.0.0'
'build.outDir', },
'build.cssTarget', config
]); );
expect(payload.config.markdown.mode).to.equal('mdx');
});
it('syntaxHighlight', () => {
const config = {
markdown: {
syntaxHighlight: 'shiki',
}
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0'
},
config
);
expect(payload.config.markdown.syntaxHighlight).to.equal('shiki');
});
}); });
it('vite.preview keys are captured', async () => { describe('config.vite', () => {
const config = await mockConfig({ it('top-level keys are captured', async () => {
vite: { const config = {
preview: { root: 'some/thing',
host: 'example.com', vite: {
port: 8080, css: { modules: [] },
another: { base: 'a',
mode: 'b',
define: {
a: 'b', a: 'b',
}, },
publicDir: 'some/dir',
}, },
}, };
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.configKeys).is.deep.equal([
'root',
'vite',
'vite.css',
'vite.css.modules',
'vite.base',
'vite.mode',
'vite.define',
'vite.publicDir',
]);
});
it('vite.resolve keys are captured', async () => {
const config = {
vite: {
resolve: {
alias: {
a: 'b',
},
dedupe: ['one', 'two'],
},
},
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
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',
astroVersion: '0.0.0',
},
config
);
expect(payload.configKeys).is.deep.equal(['vite', 'vite.worker', 'vite.worker.format', 'vite.worker.plugins']);
}); });
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'preview',
'preview.host',
'preview.port',
'preview.another',
]);
}); });
it('vite.optimizeDeps keys are captured', async () => { describe('flags', () => {
const config = await mockConfig({ it('includes cli flags in payload', () => {
vite: { const config = {};
optimizeDeps: { const flags = {
entries: ['one', 'two'], root: 'root',
exclude: ['secret', 'name'], site: 'http://example.com',
host: true,
port: 8080,
config: 'path/to/config.mjs',
experimentalSsr: true,
experimentalIntegrations: true,
drafts: true,
}
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
}, },
}, config,
}); flags
);
const [{ payload }] = events.eventCliSession( expect(payload.flags).to.deep.equal([
{ 'root',
cliCommand: 'dev', 'site',
astroVersion: '0.0.0', 'host',
}, 'port',
config 'config',
); 'experimentalSsr',
expect(payload.config.viteKeys).is.deep.equal([ 'experimentalIntegrations',
'optimizeDeps', 'drafts'
'optimizeDeps.entries', ]);
'optimizeDeps.exclude', })
]); })
});
it('vite.ssr keys are captured', async () => {
const config = await mockConfig({
vite: {
ssr: {
external: ['a'],
target: { one: 'two' },
},
},
});
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal(['ssr', 'ssr.external', 'ssr.target']);
});
it('vite.worker keys are captured', async () => {
const config = await mockConfig({
vite: {
worker: {
format: { a: 'b' },
plugins: ['a', 'b'],
},
},
});
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal(['worker', 'worker.format', 'worker.plugins']);
});
}); });