Skip to content

Commit dd176ca

Browse files
authored
add error event to telemetry (#3750)
1 parent 1eab496 commit dd176ca

File tree

16 files changed

+270
-85
lines changed

16 files changed

+270
-85
lines changed

.changeset/silly-phones-watch.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'astro': patch
3+
'@astrojs/telemetry': patch
4+
---
5+
6+
Add basic error reporting to astro telemetry

packages/astro/src/cli/index.ts

+25-21
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
/* eslint-disable no-console */
22

33
import { LogOptions } from '../core/logger/core.js';
4-
5-
import { AstroTelemetry } from '@astrojs/telemetry';
64
import * as colors from 'kleur/colors';
75
import yargs from 'yargs-parser';
86
import { z } from 'zod';
7+
import { telemetry } from '../events/index.js';
98
import * as event from '../events/index.js';
10-
119
import add from '../core/add/index.js';
1210
import build from '../core/build/index.js';
1311
import { openConfig } from '../core/config.js';
1412
import devServer from '../core/dev/index.js';
1513
import { enableVerboseLogging, nodeLogDestination } from '../core/logger/node.js';
1614
import { formatConfigErrorMessage, formatErrorMessage, printHelp } from '../core/messages.js';
1715
import preview from '../core/preview/index.js';
18-
import { createSafeError } from '../core/util.js';
16+
import { createSafeError, ASTRO_VERSION } from '../core/util.js';
1917
import { check } from './check.js';
2018
import { openInBrowser } from './open.js';
2119
import * as telemetryHandler from './telemetry.js';
20+
import { collectErrorMetadata } from '../core/errors.js';
21+
import { eventError, eventConfigError } from '../events/index.js';
2222

2323
type Arguments = yargs.Arguments;
2424
type CLICommand =
@@ -61,9 +61,6 @@ function printAstroHelp() {
6161
});
6262
}
6363

64-
// PACKAGE_VERSION is injected when we build and publish the astro package.
65-
const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
66-
6764
/** Display --version flag */
6865
async function printVersion() {
6966
console.log();
@@ -111,7 +108,6 @@ export async function cli(args: string[]) {
111108
} else if (flags.silent) {
112109
logging.level = 'silent';
113110
}
114-
const telemetry = new AstroTelemetry({ version: ASTRO_VERSION });
115111

116112
// Special CLI Commands: "add", "docs", "telemetry"
117113
// These commands run before the user's config is parsed, and may have other special
@@ -120,19 +116,19 @@ export async function cli(args: string[]) {
120116
switch (cmd) {
121117
case 'add': {
122118
try {
123-
telemetry.record(event.eventCliSession({ cliCommand: cmd }));
119+
telemetry.record(event.eventCliSession(cmd));
124120
const packages = flags._.slice(3) as string[];
125121
return await add(packages, { cwd: root, flags, logging, telemetry });
126122
} catch (err) {
127-
return throwAndExit(err);
123+
return throwAndExit(cmd, err);
128124
}
129125
}
130126
case 'docs': {
131127
try {
132-
telemetry.record(event.eventCliSession({ cliCommand: cmd }));
128+
telemetry.record(event.eventCliSession(cmd));
133129
return await openInBrowser('https://docs.astro.build/');
134130
} catch (err) {
135-
return throwAndExit(err);
131+
return throwAndExit(cmd, err);
136132
}
137133
}
138134
case 'telemetry': {
@@ -142,13 +138,13 @@ export async function cli(args: string[]) {
142138
const subcommand = flags._[3]?.toString();
143139
return await telemetryHandler.update(subcommand, { flags, telemetry });
144140
} catch (err) {
145-
return throwAndExit(err);
141+
return throwAndExit(cmd, err);
146142
}
147143
}
148144
}
149145

150146
const { astroConfig, userConfig } = await openConfig({ cwd: root, flags, cmd });
151-
telemetry.record(event.eventCliSession({ cliCommand: cmd }, userConfig, flags));
147+
telemetry.record(event.eventCliSession(cmd, userConfig, flags));
152148

153149
// Common CLI Commands:
154150
// These commands run normally. All commands are assumed to have been handled
@@ -159,15 +155,15 @@ export async function cli(args: string[]) {
159155
await devServer(astroConfig, { logging, telemetry });
160156
return await new Promise(() => {}); // lives forever
161157
} catch (err) {
162-
return throwAndExit(err);
158+
return throwAndExit(cmd, err);
163159
}
164160
}
165161

166162
case 'build': {
167163
try {
168164
return await build(astroConfig, { logging, telemetry });
169165
} catch (err) {
170-
return throwAndExit(err);
166+
return throwAndExit(cmd, err);
171167
}
172168
}
173169

@@ -181,21 +177,29 @@ export async function cli(args: string[]) {
181177
const server = await preview(astroConfig, { logging, telemetry });
182178
return await server.closed(); // keep alive until the server is closed
183179
} catch (err) {
184-
return throwAndExit(err);
180+
return throwAndExit(cmd, err);
185181
}
186182
}
187183
}
188184

189185
// No command handler matched! This is unexpected.
190-
throwAndExit(new Error(`Error running ${cmd} -- no command found.`));
186+
throwAndExit(cmd, new Error(`Error running ${cmd} -- no command found.`));
191187
}
192188

193189
/** Display error and exit */
194-
function throwAndExit(err: unknown) {
190+
function throwAndExit(cmd: string, err: unknown) {
191+
let telemetryPromise: Promise<any>;
195192
if (err instanceof z.ZodError) {
196193
console.error(formatConfigErrorMessage(err));
194+
telemetryPromise = telemetry.record(eventConfigError({ cmd, err, isFatal: true }));
197195
} else {
198-
console.error(formatErrorMessage(createSafeError(err)));
196+
const errorWithMetadata = collectErrorMetadata(createSafeError(err));
197+
console.error(formatErrorMessage(errorWithMetadata));
198+
telemetryPromise = telemetry.record(eventError({ cmd, err: errorWithMetadata, isFatal: true }));
199199
}
200-
process.exit(1);
200+
// Wait for the telemetry event to send, then exit. Ignore an error.
201+
telemetryPromise.catch(() => undefined).then(() => process.exit(1));
202+
// Don't wait too long. Timeout the request faster than usual because the user is waiting.
203+
// TODO: Investigate using an AbortController once we drop Node v14 support.
204+
setTimeout(() => process.exit(1), 300);
201205
}

packages/astro/src/core/errors.ts

+9
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import type { ViteDevServer } from 'vite';
55
import type { SSRError } from '../@types/astro';
66
import { codeFrame, createSafeError } from './util.js';
77

8+
export enum AstroErrorCodes {
9+
// 1xxx: Astro Runtime Errors
10+
UnknownError = 1000,
11+
ConfigError = 1001,
12+
// 2xxx: Astro Compiler Errors
13+
UnknownCompilerError = 2000,
14+
UnknownCompilerCSSError = 2001,
15+
}
816
export interface ErrorWithMetadata {
917
[name: string]: any;
1018
message: string;
1119
stack: string;
20+
code?: number;
1221
hint?: string;
1322
id?: string;
1423
frame?: string;

packages/astro/src/core/messages.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type { AddressInfo } from 'net';
1818
import os from 'os';
1919
import { ZodError } from 'zod';
2020
import type { AstroConfig } from '../@types/astro';
21-
import { cleanErrorStack, collectErrorMetadata } from './errors.js';
21+
import { cleanErrorStack, collectErrorMetadata, ErrorWithMetadata } from './errors.js';
2222
import { emoji, getLocalAddress, padMultilineString } from './util.js';
2323

2424
const PREFIX_PADDING = 6;
@@ -219,8 +219,7 @@ export function formatConfigErrorMessage(err: ZodError) {
219219
)}`;
220220
}
221221

222-
export function formatErrorMessage(_err: Error, args: string[] = []): string {
223-
const err = collectErrorMetadata(_err);
222+
export function formatErrorMessage(err: ErrorWithMetadata, args: string[] = []): string {
224223
args.push(`${bgRed(black(` error `))}${red(bold(padMultilineString(err.message)))}`);
225224
if (err.hint) {
226225
args.push(` ${bold('Hint:')}`);

packages/astro/src/core/util.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type { ErrorPayload } from 'vite';
88
import type { AstroConfig } from '../@types/astro';
99
import { removeTrailingForwardSlash } from './path.js';
1010

11+
// process.env.PACKAGE_VERSION is injected when we build and publish the astro package.
12+
export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';
13+
1114
/** Returns true if argument is an object of any prototype/class (but not null). */
1215
export function isObject(value: unknown): value is Record<string, any> {
1316
return typeof value === 'object' && value != null;

packages/astro/src/events/error.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ZodError } from 'zod';
2+
import { AstroErrorCodes, ErrorWithMetadata } from '../core/errors.js';
3+
4+
const EVENT_ERROR = 'ASTRO_CLI_ERROR';
5+
6+
interface ErrorEventPayload {
7+
code: number | undefined;
8+
isFatal: boolean;
9+
plugin?: string | undefined;
10+
cliCommand: string;
11+
anonymousMessageHint?: string | undefined;
12+
}
13+
14+
interface ConfigErrorEventPayload extends ErrorEventPayload {
15+
isConfig: true;
16+
configErrorPaths: string[];
17+
}
18+
19+
/**
20+
* This regex will grab a small snippet at the start of an error message.
21+
* This was designed to stop capturing at the first sign of some non-message
22+
* content like a filename, filepath, or any other code-specific value.
23+
* We also trim this value even further to just a few words.
24+
*
25+
* Our goal is to remove this entirely before v1.0.0 is released, as we work
26+
* to add a proper error code system (see AstroErrorCodes for examples).
27+
*
28+
* TODO(fks): Remove around v1.0.0 release.
29+
*/
30+
const ANONYMIZE_MESSAGE_REGEX = /^(\w| )+/;
31+
function anonymizeErrorMessage(msg: string): string | undefined {
32+
const matchedMessage = msg.match(ANONYMIZE_MESSAGE_REGEX);
33+
if (!matchedMessage || !matchedMessage[0]) {
34+
return undefined;
35+
}
36+
return matchedMessage[0].trim().substring(0, 20);
37+
}
38+
39+
export function eventConfigError({
40+
err,
41+
cmd,
42+
isFatal,
43+
}: {
44+
err: ZodError;
45+
cmd: string;
46+
isFatal: boolean;
47+
}): { eventName: string; payload: ConfigErrorEventPayload }[] {
48+
const payload: ConfigErrorEventPayload = {
49+
code: AstroErrorCodes.ConfigError,
50+
isFatal,
51+
isConfig: true,
52+
cliCommand: cmd,
53+
configErrorPaths: err.issues.map((issue) => issue.path.join('.')),
54+
};
55+
return [{ eventName: EVENT_ERROR, payload }];
56+
}
57+
58+
export function eventError({
59+
cmd,
60+
err,
61+
isFatal,
62+
}: {
63+
err: ErrorWithMetadata;
64+
cmd: string;
65+
isFatal: boolean;
66+
}): { eventName: string; payload: ErrorEventPayload }[] {
67+
const payload: ErrorEventPayload = {
68+
code: err.code || AstroErrorCodes.UnknownError,
69+
plugin: err.plugin,
70+
cliCommand: cmd,
71+
isFatal: isFatal,
72+
anonymousMessageHint: anonymizeErrorMessage(err.message),
73+
};
74+
return [{ eventName: EVENT_ERROR, payload }];
75+
}

packages/astro/src/events/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1+
import { AstroTelemetry } from '@astrojs/telemetry';
2+
import { ASTRO_VERSION } from '../core/util.js';
3+
import { createRequire } from 'module';
4+
const require = createRequire(import.meta.url);
5+
6+
function getViteVersion() {
7+
try {
8+
const { version } = require('vite/package.json');
9+
return version;
10+
} catch (e) {}
11+
return undefined;
12+
}
13+
14+
export const telemetry = new AstroTelemetry({ astroVersion: ASTRO_VERSION, viteVersion: getViteVersion() });
15+
16+
export * from './error.js';
117
export * from './session.js';
18+

packages/astro/src/events/session.ts

+6-23
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ const require = createRequire(import.meta.url);
44

55
const EVENT_SESSION = 'ASTRO_CLI_SESSION_STARTED';
66

7-
interface EventCliSession {
8-
cliCommand: string;
9-
}
10-
117
interface ConfigInfo {
128
markdownPlugins: string[];
139
adapter: string | null;
@@ -26,23 +22,14 @@ interface ConfigInfo {
2622
};
2723
}
2824

29-
interface EventCliSessionInternal extends EventCliSession {
30-
nodeVersion: string;
31-
viteVersion: string;
25+
interface EventPayload {
26+
cliCommand: string;
3227
config?: ConfigInfo;
3328
configKeys?: string[];
3429
flags?: string[];
3530
optionalIntegrations?: number;
3631
}
3732

38-
function getViteVersion() {
39-
try {
40-
const { version } = require('vite/package.json');
41-
return version;
42-
} catch (e) {}
43-
return undefined;
44-
}
45-
4633
const multiLevelKeys = new Set([
4734
'build',
4835
'markdown',
@@ -82,10 +69,10 @@ function configKeys(obj: Record<string, any> | undefined, parentKey: string): st
8269
}
8370

8471
export function eventCliSession(
85-
event: EventCliSession,
72+
cliCommand: string,
8673
userConfig?: AstroUserConfig,
8774
flags?: Record<string, any>
88-
): { eventName: string; payload: EventCliSessionInternal }[] {
75+
): { eventName: string; payload: EventPayload }[] {
8976
// Filter out falsy integrations
9077
const configValues = userConfig
9178
? {
@@ -117,13 +104,9 @@ export function eventCliSession(
117104
// Filter out yargs default `_` flag which is the cli command
118105
const cliFlags = flags ? Object.keys(flags).filter((name) => name != '_') : undefined;
119106

120-
const payload: EventCliSessionInternal = {
121-
cliCommand: event.cliCommand,
122-
// Versions
123-
viteVersion: getViteVersion(),
124-
nodeVersion: process.version.replace(/^v?/, ''),
107+
const payload: EventPayload = {
108+
cliCommand,
125109
configKeys: userConfig ? configKeys(userConfig, '') : undefined,
126-
// Config Values
127110
config: configValues,
128111
flags: cliFlags,
129112
};

packages/astro/src/vite-plugin-astro-server/index.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { SSROptions } from '../core/render/dev/index';
66
import { Readable } from 'stream';
77
import stripAnsi from 'strip-ansi';
88
import { call as callEndpoint } from '../core/endpoint/dev/index.js';
9-
import { fixViteErrorMessage } from '../core/errors.js';
9+
import { collectErrorMetadata, fixViteErrorMessage } from '../core/errors.js';
1010
import { error, info, LogOptions, warn } from '../core/logger/core.js';
1111
import * as msg from '../core/messages.js';
1212
import { appendForwardSlash } from '../core/path.js';
@@ -320,7 +320,8 @@ async function handleRequest(
320320
}
321321
} catch (_err) {
322322
const err = fixViteErrorMessage(createSafeError(_err), viteServer);
323-
error(logging, null, msg.formatErrorMessage(err));
323+
const errorWithMetadata = collectErrorMetadata(_err);
324+
error(logging, null, msg.formatErrorMessage(errorWithMetadata));
324325
handle500Response(viteServer, origin, req, res, err);
325326
}
326327
}

0 commit comments

Comments
 (0)