Skip to content
This repository has been archived by the owner on Mar 15, 2024. It is now read-only.

Commit

Permalink
feat: Report unhandled exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinbyrne committed Apr 27, 2023
1 parent 41fe450 commit b1fb7e6
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
1 change: 1 addition & 0 deletions component/build-prod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ for (const [name, component, env, resolution] of [
},
],
[null, "error", "node", {}],
[null, "crash-reporter", "node", {}],
[null, "server", "node", { validate: "ajv", instrumentation: "default" }],
[null, "client", "node", { validate: "stub" }],
[null, "init", "node", {}],
Expand Down
1 change: 1 addition & 0 deletions components/.ordering
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ uuid
self
version
error
crash-reporter
log-inner
log
glob
Expand Down
1 change: 1 addition & 0 deletions components/crash-reporter/node/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node
103 changes: 103 additions & 0 deletions components/crash-reporter/node/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { request } from "node:https";

const {
Boolean,
Buffer,
Date,
JSON: { stringify: stringifyJSON },
Number,
process: {
env: { APPMAP_TELEMETRY_DISABLED },
},
Promise,
} = globalThis;

// This key is meant to be publically shared. However, I'm adding a simple
// obfuscation to mitigate key scraping bots on GitHub. The key is split on
// hypens and base64 encoded without padding.
// key.split('-').map((x) => x.toString('base64').replace(/=*/, ''))
const INSTRUMENTATION_KEY = [
"NTBjMWE1YzI",
"NDliNA",
"NDkxMw",
"YjdjYw",
"ODZhNzhkNDA3NDVm",
]
.map((x) => Buffer.from(x, "base64").toString("utf8"))
.join("-");

const METHOD_REGEX = /at\s(?!\w+:\/\/)(.*?)[\s:]/u;
const INGESTION_ENDPOINT = "centralus-2.in.applicationinsights.azure.com";
const LOCATION_REGEX = /at\s+(\w+:\/\/|.*\()?(?:\w+:\/\/)?(.*):(\d+):\d+/u;

export const parseExceptionStack = (exception) =>
exception.stack
?.split("\n")
.filter((line) => line.match(/^\s+at\s/u))
.map((line, index) => {
const method = line.match(METHOD_REGEX)?.[1] ?? "";
const [, , fileName, lineNumber] = line.match(LOCATION_REGEX) ?? [];
if (!fileName || !lineNumber) {
return null;
}

return {
level: index,
method,
fileName,
line: Number(lineNumber),
};
})
.filter(Boolean) ?? [];

/* c8 ignore start */
export const reportException = (exception) => {
if (APPMAP_TELEMETRY_DISABLED) {
return null;
}

const data = {
name: "Microsoft.ApplicationInsights.Exception",
time: new Date().toISOString(),
iKey: INSTRUMENTATION_KEY,
tags: {
"ai.cloud.roleInstance": "@appland/appmap-agent-js",
},
data: {
baseType: "ExceptionData",
baseData: {
ver: 2,
handledAt: "UserCode",
exceptions: [
{
id: 1,
typeName: exception.name,
message: exception.message,
hasFullStack: Boolean(exception.stack),
parsedStack: parseExceptionStack(exception),
},
],
},
},
};

const options = {
hostname: INGESTION_ENDPOINT,
path: "/v2/track",
method: "POST",
headers: {
"Content-Type": "application/json",
},
};

return new Promise((resolve) => {
const req = request(options, resolve);

// Don't throw if the request fails - we don't want to crash the app
req.on("error", resolve);

req.write(stringifyJSON(data));
req.end();
});
};
/* c8 ignore stop */
84 changes: 84 additions & 0 deletions components/crash-reporter/node/index.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { assertDeepEqual } from "../../__fixture__.mjs";
import { parseExceptionStack } from "./index.mjs";

{
const error = {
stack: `InternalAppmapError: expected at least two argv
at assert (file:///Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs:191:11)
at extendConfigurationNode (file:///Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs:5588:3)
at record (file:///Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs:5609:19)
at file:///Users/user/dev/appland/appmap-agent-js/lib/node/recorder.mjs:11:1
at ModuleJob.run (node:internal/modules/esm/module_job:183:25)
at <anonymous>:1:1`,
message: "expected at least two argv",
name: "InternalAppmapError",
};

const stack = parseExceptionStack(error);
assertDeepEqual(stack, [
{
level: 0,
method: "assert",
fileName:
"/Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs",
line: 191,
},
{
level: 1,
method: "extendConfigurationNode",
fileName:
"/Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs",
line: 5588,
},
{
level: 2,
method: "record",
fileName:
"/Users/user/dev/appland/appmap-agent-js/dist/bundles/recorder-process.mjs",
line: 5609,
},
{
level: 3,
method: "",
fileName: "/Users/user/dev/appland/appmap-agent-js/lib/node/recorder.mjs",
line: 11,
},
{
level: 4,
method: "ModuleJob.run",
fileName: "node:internal/modules/esm/module_job",
line: 183,
},
{
level: 5,
method: "<anonymous>",
fileName: "<anonymous>",
line: 1,
},
]);
}

{
// Unknown stack trace formats are ignored
const error = {
stack: `Error: something went wrong
at Class.method ()`,
message: "something went wrong",
name: "Error",
};

const stack = parseExceptionStack(error);
assertDeepEqual(stack, []);
}

{
// Empty stacks return an empty array
const error = {
stack: null,
message: "something went wrong",
name: "Error",
};

const stack = parseExceptionStack(error);
assertDeepEqual(stack, []);
}
4 changes: 4 additions & 0 deletions lib/node/error.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { default as process, stderr } from "node:process";
import "./global.mjs";

const { reportError } = await import("../../dist/bundles/error.mjs");
const { reportException } = await import(
"../../dist/bundles/crash-reporter.mjs"
);

process.on("uncaughtExceptionMonitor", (error) => {
reportException(error);
stderr.write(reportError(error));
});

0 comments on commit b1fb7e6

Please sign in to comment.