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 { openInBrowser } from './open.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 { createSafeError } from '../core/util.js';
@ -138,14 +138,16 @@ export async function cli(args: string[]) {
}
case 'dev': {
try {
const config = await loadConfig({ cwd: root, flags, cmd });
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record(
event.eventCliSession(
{ 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
} catch (err) {
return throwAndExit(err);
@ -154,41 +156,44 @@ export async function cli(args: string[]) {
case 'build': {
try {
const config = await loadConfig({ cwd: root, flags, cmd });
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record(
event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'build' },
config
userConfig,
flags
)
);
return await build(config, { logging, telemetry });
return await build(astroConfig, { logging, telemetry });
} catch (err) {
return throwAndExit(err);
}
}
case 'check': {
const config = await loadConfig({ cwd: root, flags, cmd });
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record(
event.eventCliSession(
{ astroVersion: process.env.PACKAGE_VERSION ?? '', cliCommand: 'check' },
config
userConfig,
flags,
)
);
const ret = await check(config);
const ret = await check(astroConfig);
return process.exit(ret);
}
case 'preview': {
try {
const config = await loadConfig({ cwd: root, flags, cmd });
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
telemetry.record(
event.eventCliSession(
{ 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
} catch (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> {
const root = configOptions.cwd ? path.resolve(configOptions.cwd) : process.cwd();
const flags = resolveFlags(configOptions.flags || {});

View file

@ -6,25 +6,34 @@ const require = createRequire(import.meta.url);
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 {
astroVersion: string;
cliCommand: string;
}
interface ConfigInfo {
hasViteConfig: boolean;
hasBase: boolean;
viteKeys: string[];
markdownPlugins: string[];
adapter: string | null;
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 {
nodeVersion: string;
viteVersion: string;
config?: ConfigInfo;
configKeys?: string[];
flags?: string[];
}
function getViteVersion() {
@ -35,29 +44,24 @@ function getViteVersion() {
return undefined;
}
function getExperimentalFeatures(astroConfig?: Record<string, any>): string[] | undefined {
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',
const multiLevelKeys = new Set([
'build',
'preview',
'optimizeDeps',
'ssr',
'worker',
'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 viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string): string[] {
function configKeys(obj: Record<string, any> | undefined, parentKey: string): string[] {
if (!obj) {
return [];
}
@ -66,8 +70,8 @@ function viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string)
.map(([key, value]) => {
if (typeof value === 'object' && !Array.isArray(value)) {
const localKey = parentKey ? parentKey + '.' + key : key;
if (secondLevelViteKeys.has(localKey)) {
let keys = viteConfigKeys(value, localKey).map((subkey) => key + '.' + subkey);
if (multiLevelKeys.has(localKey)) {
let keys = configKeys(value, localKey).map((subkey) => key + '.' + subkey);
keys.unshift(key);
return keys;
}
@ -80,29 +84,39 @@ function viteConfigKeys(obj: Record<string, any> | undefined, parentKey: string)
export function eventCliSession(
event: EventCliSession,
astroConfig?: Record<string, any>
userConfig?: AstroUserConfig,
flags?: Record<string, any>,
): { 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 = {
cliCommand: event.cliCommand,
// Versions
astroVersion: event.astroVersion,
viteVersion: getViteVersion(),
nodeVersion: process.version.replace(/^v?/, ''),
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
// Config Values
config: astroConfig
? {
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,
config: configValues,
flags: cliFlags,
};
return [{ eventName: EVENT_SESSION, payload }];
}

View file

@ -1,241 +1,423 @@
import { expect } from 'chai';
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', () => {
it('top-level keys are captured', async () => {
const config = await mockConfig({
vite: {
css: { modules: [] },
base: 'a',
mode: 'b',
define: {
a: 'b',
describe('top-level', () => {
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',
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 () => {
const config = await mockConfig({
vite: {
resolve: {
alias: {
a: 'b',
},
dedupe: ['one', 'two'],
describe('config.build', () => {
it('configKeys includes format', () => {
const config = {
srcDir: 1,
build: {
format: 'file',
}
};
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(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal(['resolve', 'resolve.alias', 'resolve.dedupe']);
});
it('vite.css keys are captured', async () => {
const config = await mockConfig({
vite: {
resolve: {
dedupe: ['one', 'two'],
it('config.build.format', () => {
const config = {
srcDir: 1,
build: {
format: 'file',
}
};
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0'
},
css: {
modules: [],
postcss: {},
},
},
config
);
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 () => {
const config = await mockConfig({
vite: {
describe('config.server', () => {
it('configKeys includes server props', () => {
const config = {
srcDir: 1,
server: {
host: 'example.com',
open: true,
fs: {
strict: true,
allow: ['a', 'b'],
},
port: 8033
}
};
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 () => {
const config = await mockConfig({
vite: {
build: {
target: 'one',
outDir: 'some/dir',
cssTarget: {
one: 'two',
describe('config.markdown', () => {
it('configKeys is deep', () => {
const config = {
publicDir: 1,
markdown: {
drafts: true,
mode: 'mdx',
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(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'build',
'build.target',
'build.outDir',
'build.cssTarget',
]);
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', () => {
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 () => {
const config = await mockConfig({
vite: {
preview: {
host: 'example.com',
port: 8080,
another: {
describe('config.vite', () => {
it('top-level keys are captured', async () => {
const config = {
root: 'some/thing',
vite: {
css: { modules: [] },
base: 'a',
mode: 'b',
define: {
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 () => {
const config = await mockConfig({
vite: {
optimizeDeps: {
entries: ['one', 'two'],
exclude: ['secret', 'name'],
describe('flags', () => {
it('includes cli flags in payload', () => {
const config = {};
const flags = {
root: 'root',
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',
},
},
});
const [{ payload }] = events.eventCliSession(
{
cliCommand: 'dev',
astroVersion: '0.0.0',
},
config
);
expect(payload.config.viteKeys).is.deep.equal([
'optimizeDeps',
'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']);
});
config,
flags
);
expect(payload.flags).to.deep.equal([
'root',
'site',
'host',
'port',
'config',
'experimentalSsr',
'experimentalIntegrations',
'drafts'
]);
})
})
});