diff --git a/.gitignore b/.gitignore index 3520a38..cd8a550 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .nyc_output +.tap coverage node_modules -.tap diff --git a/index.js b/index.js index fb37913..9423253 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ const { Transform } = require("node:stream"); const { prettyFactory } = require("pino-pretty"); -const Sentry = require("@sentry/node"); +const { init, withScope, captureException } = require("@sentry/node"); const LEVEL_MAP = { 10: "trace", @@ -14,8 +14,27 @@ const LEVEL_MAP = { 60: "fatal", }; +const pinoIgnore = [ + // default pino keys + "time", + "pid", + "hostname", + // remove keys from pino-http + "req", + "res", + "responseTime", +].join(","); + +const pinoErrorProps = [ + "event", + "status", + "headers", + "request", + "sentryEventId", +].join(","); + /** - * Implements Probot's default logging formatting and error captionaing using Sentry. + * Implements Probot's default logging formatting and error captioning using Sentry. * * @param {import("./").Options} options * @returns Transform @@ -28,7 +47,7 @@ function getTransformStream(options = {}) { const sentryEnabled = !!options.sentryDsn; if (sentryEnabled) { - Sentry.init({ + init({ dsn: options.sentryDsn, // See https://github.com/getsentry/sentry-javascript/issues/1964#issuecomment-688482615 // 6 is enough to serialize the deepest property across all GitHub Event payloads @@ -37,19 +56,8 @@ function getTransformStream(options = {}) { } const pretty = prettyFactory({ - ignore: [ - // default pino keys - "time", - "pid", - "hostname", - // remove keys from pino-http - "req", - "res", - "responseTime", - ].join(","), - errorProps: ["event", "status", "headers", "request", "sentryEventId"].join( - ",", - ), + ignore: pinoIgnore, + errorProps: pinoErrorProps, }); return new Transform({ @@ -76,18 +84,26 @@ function getTransformStream(options = {}) { return; } - Sentry.withScope(function (scope) { + withScope((scope) => { const sentryLevelName = data.level === 50 ? "error" : "fatal"; scope.setLevel(sentryLevelName); - for (const extra of ["event", "headers", "request", "status"]) { - if (!data[extra]) continue; - - scope.setExtra(extra, data[extra]); + if (data.event) { + scope.setExtra("event", data.event); + } + if (data.headers) { + scope.setExtra("headers", data.headers); + } + if (data.request) { + scope.setExtra("request", data.request); + } + if (data.status) { + scope.setExtra("status", data.status); } // set user id and username to installation ID and account login - if (data.event && data.event.payload) { + const payload = data.event?.payload || data.err?.event?.payload; + if (payload) { const { // When GitHub App is installed organization wide installation: { id, account: { login: account } = {} } = {}, @@ -96,7 +112,7 @@ function getTransformStream(options = {}) { organization: { login: organization } = {}, // When the repository belongs to a user repository: { owner: { login: owner } = {} } = {}, - } = data.event.payload; + } = payload; scope.setUser({ id, @@ -104,7 +120,7 @@ function getTransformStream(options = {}) { }); } - const sentryEventId = Sentry.captureException(toSentryError(data)); + const sentryEventId = captureException(toSentryError(data)); // reduce logging data and add reference to sentry event instead if (data.event) { diff --git a/package-lock.json b/package-lock.json index dd289fc..750b04d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,7 @@ "pino-probot": "bin/cli.js" }, "devDependencies": { - "@types/pino": "^7.0.5", - "pino": "^6.6.0", + "pino": "^9.3.2", "prettier": "^3.4.2", "tap": "^21.0.1" }, @@ -1873,15 +1872,6 @@ "@types/pg": "*" } }, - "node_modules/@types/pino": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.5.tgz", - "integrity": "sha512-wKoab31pknvILkxAF8ss+v9iNyhw5Iu/0jLtRkUD74cNfOOLJNnqfFKAv0r7wVaTQxRZtWrMpGfShwwBjOcgcg==", - "dev": true, - "dependencies": { - "pino": "*" - } - }, "node_modules/@types/shimmer": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", @@ -2720,12 +2710,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", - "dev": true - }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -2783,21 +2767,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4155,18 +4124,26 @@ } }, "node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", "dev": true, + "license": "MIT", "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { @@ -4202,20 +4179,12 @@ "pino-pretty": "bin.js" } }, - "node_modules/pino-pretty/node_modules/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", - "dev": true + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "dev": true, + "license": "MIT" }, "node_modules/pirates": { "version": "4.0.6", @@ -4360,10 +4329,21 @@ } }, "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "dev": true + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" }, "node_modules/promise-inflight": { "version": "1.0.1", @@ -4482,6 +4462,16 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4597,6 +4587,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -4781,13 +4781,12 @@ } }, "node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dev": true, + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" } }, "node_modules/spdx-correct": { @@ -5279,6 +5278,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 4c243d5..2d002d3 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,7 @@ "license": "MIT", "repository": "github:probot/pino", "devDependencies": { - "@types/pino": "^7.0.5", - "pino": "^6.6.0", + "pino": "^9.3.2", "prettier": "^3.4.2", "tap": "^21.0.1" }, diff --git a/test/cli.test.js b/test/cli.test.js index 6e9a4af..0a0a348 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -2,7 +2,6 @@ const { join: pathJoin } = require("node:path"); const { spawn } = require("node:child_process"); -const { createServer } = require("node:http"); const { test } = require("tap"); const cliPath = require.resolve(pathJoin(__dirname, "..", "bin", "cli.js")); @@ -12,8 +11,6 @@ const logLine = '{"level":30,"time":1445858940000,"name":"probot","msg":"hello future","pid":42,"hostname":"foo"}\n'; const errorLine = '{"level":50,"time":1597399283686,"pid":35269,"hostname":"Gregors-MacBook-Pro.local","name":"probot","status":500,"event":{"event":"installation_repositories.added","id":"123","installation":456},"headers":{"x-github-request-id":"789"},"request":{"headers":{"accept":"application/vnd.github.v3+json","authorization":"[Filtered]","user-agent":"probot/10.0.0"},"method":"GET","url":"https://api.github.com/repos/octocat/hello-world/"},"stack":"Error: Oops\\n at Object.<anonymous> (/Users/gregor/Projects/probot/pino/example.js:37:15)\\n at Module._compile (internal/modules/cjs/loader.js:1137:30)\\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)\\n at Module.load (internal/modules/cjs/loader.js:985:32)\\n at Function.Module._load (internal/modules/cjs/loader.js:878:14)\\n at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)\\n at internal/main/run_main_module.js:17:47","type":"Error","msg":"Oops"}\n'; -const fatalErrorLine = - '{"level":60,"time":1597426544906,"pid":43024,"hostname":"Gregors-MacBook-Pro.local","name":"probot","stack":"Error: Oh no!\\n at Object.<anonymous> (/Users/gregor/Projects/probot/pino/example.js:59:12)\\n at Module._compile (internal/modules/cjs/loader.js:1137:30)\\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)\\n at Module.load (internal/modules/cjs/loader.js:985:32)\\n at Function.Module._load (internal/modules/cjs/loader.js:878:14)\\n at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)\\n at internal/main/run_main_module.js:17:47","type":"Error","msg":"Oh no!"}\n'; const stripAnsiColorRE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; @@ -24,6 +21,8 @@ const env = { }; test("cli", (t) => { + t.plan(4); + t.test( "formats using pino-pretty and Probot's preferences by default", (t) => { @@ -92,132 +91,4 @@ test("cli", (t) => { child.stdin.write(logLine); t.teardown(() => child.kill()); }); - - t.test("SENTRY_DSN", (t) => { - t.plan(5); - - const server = createServer((request, response) => { - // we can access HTTP headers - let body = ""; - request.on("data", (chunk) => { - body += chunk.toString(); - }); - request.on("end", () => { - const data = body.split("\n").map((line) => JSON.parse(line)); - const error = data[2].exception.values[0]; - - t.equal(error.type, "Error"); - t.equal(error.value, "Oops"); - t.strictSame(data[2].extra, { - event: { - event: "installation_repositories.added", - id: "123", - installation: 456, - }, - headers: { "x-github-request-id": "789" }, - request: { - headers: { - accept: "application/vnd.github.v3+json", - authorization: "[Filtered]", - "user-agent": "probot/10.0.0", - }, - method: "GET", - url: "https://api.github.com/repos/octocat/hello-world/", - }, - status: 500, - }); - server.close(() => t.end()); - }); - - response.writeHead(200); - response.write("ok"); - response.end(); - }); - - server.listen(0); - - const child = spawn(nodeBinaryPath, [cliPath], { - env: { - ...env, - SENTRY_DSN: `http://user@localhost:${server.address().port}/123`, - }, - }); - child.on("error", t.threw); - child.stdout.on("data", (data) => { - const errorStringLines = data - .toString() - .replace(stripAnsiColorRE, "") - .split(/\n/); - t.equal(errorStringLines[0].trim(), "ERROR (probot): Oops"); - - // skip the error stack, normalize Sentry Event ID, compare error details only - t.equal( - errorStringLines - .slice(9) - .join("\n") - .trim() - .replace(/sentryEventId: \w+$/, "sentryEventId: 123"), - `event: { - id: "123" - } - status: 500 - headers: { - x-github-request-id: "789" - } - request: { - method: "GET" - url: "https://api.github.com/repos/octocat/hello-world/" - } - sentryEventId: 123`, - ); - }); - child.stdin.write(errorLine); - - t.teardown(() => child.kill()); - }); - - t.test("SENTRY_DSN with fatal error", (t) => { - t.plan(3); - - const server = createServer((request, response) => { - let body = ""; - request.on("data", (chunk) => { - body += chunk.toString(); - }); - request.on("end", () => { - const data = body.split("\n").map((line) => JSON.parse(line)); - const error = data[2].exception.values[0]; - - t.equal(error.type, "Error"); - t.equal(error.value, "Oh no!"); - - server.close(() => t.end()); - }); - - response.writeHead(200); - response.write("ok"); - response.end(); - }); - - server.listen(0); - - const child = spawn(nodeBinaryPath, [cliPath], { - env: { - ...env, - SENTRY_DSN: `http://user@localhost:${server.address().port}/123`, - }, - }); - child.on("error", t.threw); - child.stdout.on("data", (data) => { - t.match( - data.toString().replace(stripAnsiColorRE, ""), - /^FATAL \(probot\): Oh no!\n/, - ); - }); - child.stdin.write(fatalErrorLine); - - t.teardown(() => child.kill()); - }); - - t.end(); }); diff --git a/test/index.test.js b/test/index.test.js index 0c541a3..9fda0a0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,18 +1,16 @@ "use strict"; -const Sentry = require("@sentry/node"); - const { Writable: WritableStream } = require("stream"); const { test } = require("tap"); -const pino = require("pino"); +const { pino } = require("pino"); const { getTransformStream } = require(".."); test("API", (t) => { let env = Object.assign({}, process.env); t.afterEach(() => { - process.env = Object.assign({}, env); + process.env = { ...env }; }); t.test("getTransformStream export", (t) => { @@ -25,114 +23,6 @@ test("API", (t) => { t.end(); }); - t.test("Sentry integration enabled", (t) => { - const transform = getTransformStream({ - sentryDsn: "http://username@example.com/1234", - }); - const log = pino({}, transform); - - function event(payload) { - const error = new Error("Hello from the test"); - error.level = 50; - error.event = { - payload: Object.assign( - { - installation: { - id: "456", - }, - }, - payload, - ), - }; - return error; - } - - t.test("without user", (t) => { - t.plan(1); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.strictSame(event.user, { id: "456" }); - }); - - log.fatal(event({})); - }); - }); - - t.test("with organization", (t) => { - t.plan(1); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.match(event.user, { username: "org" }); - }); - - log.fatal(event({ organization: { login: "org" } })); - }); - }); - - t.test("with installation account", (t) => { - t.plan(1); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.match(event.user, { username: "account" }); - }); - - log.fatal(event({ installation: { account: { login: "account" } } })); - }); - }); - - t.test("with repository owner", (t) => { - t.plan(1); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.match(event.user, { username: "owner" }); - }); - - log.fatal(event({ repository: { owner: { login: "owner" } } })); - }); - }); - - t.test("with repository owner and without installation", (t) => { - t.plan(1); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.match(event.user, { username: "owner" }); - }); - - log.fatal( - event({ - installation: undefined, - repository: { owner: { login: "owner" } }, - }), - ); - }); - }); - - t.test("with logFormat: json", (t) => { - t.plan(1); - - const transform = getTransformStream({ - sentryDsn: "http://username@example.com/1234", - logFormat: "json", - }); - const log = pino({}, transform); - - Sentry.withScope(function (scope) { - scope.addEventProcessor(function (event, hint) { - t.match(event.user, { username: "owner" }); - }); - - log.fatal(event({ repository: { owner: { login: "owner" } } })); - }); - }); - - t.end(); - }); - t.test( "A single \\n is added to the end log lines when LOG_FORMAT is set to 'json' (https://github.com/probot/probot/issues/1334)", (t) => { diff --git a/test/sentry.cli.test.js b/test/sentry.cli.test.js new file mode 100644 index 0000000..4eab1b8 --- /dev/null +++ b/test/sentry.cli.test.js @@ -0,0 +1,163 @@ +"use strict"; + +const { join: pathJoin } = require("node:path"); +const { spawn } = require("node:child_process"); +const { createServer } = require("node:http"); +const { test } = require("tap"); +const { once } = require("node:events"); + +const SENTRY_DSN = "http://username@example.com/1234"; + +const cliPath = require.resolve(pathJoin(__dirname, "..", "bin", "cli.js")); +const nodeBinaryPath = process.argv[0]; + +const errorLine = + '{"level":50,"time":1597399283686,"pid":35269,"hostname":"Gregors-MacBook-Pro.local","name":"probot","status":500,"event":{"event":"installation_repositories.added","id":"123","installation":456},"headers":{"x-github-request-id":"789"},"request":{"headers":{"accept":"application/vnd.github.v3+json","authorization":"[Filtered]","user-agent":"probot/10.0.0"},"method":"GET","url":"https://api.github.com/repos/octocat/hello-world/"},"stack":"Error: Oops\\n at Object.<anonymous> (/Users/gregor/Projects/probot/pino/example.js:37:15)\\n at Module._compile (internal/modules/cjs/loader.js:1137:30)\\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)\\n at Module.load (internal/modules/cjs/loader.js:985:32)\\n at Function.Module._load (internal/modules/cjs/loader.js:878:14)\\n at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)\\n at internal/main/run_main_module.js:17:47","type":"Error","msg":"Oops"}\n'; +const fatalErrorLine = + '{"level":60,"time":1597426544906,"pid":43024,"hostname":"Gregors-MacBook-Pro.local","name":"probot","stack":"Error: Oh no!\\n at Object.<anonymous> (/Users/gregor/Projects/probot/pino/example.js:59:12)\\n at Module._compile (internal/modules/cjs/loader.js:1137:30)\\n at Object.Module._extensions..js (internal/modules/cjs/loader.js:1157:10)\\n at Module.load (internal/modules/cjs/loader.js:985:32)\\n at Function.Module._load (internal/modules/cjs/loader.js:878:14)\\n at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)\\n at internal/main/run_main_module.js:17:47","type":"Error","msg":"Oh no!"}\n'; + +const stripAnsiColorRE = + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; + +const env = { + // disable colors + TERM: "dumb", +}; + +test("SENTRY_DSN", (t) => { + t.plan(2); + + t.test("SENTRY_DSN with ERROR error", async (t) => { + t.plan(2); + + const server = createServer((request, response) => { + // we can access HTTP headers + let body = ""; + request.on("data", (chunk) => { + body += chunk.toString(); + }); + request.on("end", () => { + const data = JSON.parse(body.split("\n")[2]); + const error = data.exception.values[0]; + + t.equal(error.type, "Error"); + t.equal(error.value, "Oops"); + t.strictSame(data.extra, { + event: { + event: "installation_repositories.added", + id: "123", + installation: 456, + }, + headers: { "x-github-request-id": "789" }, + request: { + headers: { + accept: "application/vnd.github.v3+json", + authorization: "[Filtered]", + "user-agent": "probot/10.0.0", + }, + method: "GET", + url: "https://api.github.com/repos/octocat/hello-world/", + }, + status: 500, + }); + }); + + response.writeHead(200); + response.write("ok"); + response.end(); + }); + + server.listen(0); + + await once(server, "listening"); + + const child = spawn(nodeBinaryPath, [cliPath], { + env: { + ...env, + SENTRY_DSN, + }, + }); + child.on("error", t.threw); + child.stdout.on("data", (data) => { + const errorStringLines = data + .toString() + .replace(stripAnsiColorRE, "") + .split(/\n/); + t.equal(errorStringLines[0].trim(), "ERROR (probot): Oops"); + + // skip the error stack, normalize Sentry Event ID, compare error details only + t.equal( + errorStringLines + .slice(9) + .join("\n") + .trim() + .replace(/sentryEventId: \w+$/, "sentryEventId: 123"), + `event: { + id: "123" + } + status: 500 + headers: { + x-github-request-id: "789" + } + request: { + method: "GET" + url: "https://api.github.com/repos/octocat/hello-world/" + } + sentryEventId: 123`, + ); + }); + child.stdin.write(errorLine); + + t.teardown(() => { + child.kill(); + server.closeAllConnections(); + server.close(); + }); + }); + + t.test("SENTRY_DSN with FATAL error", async (t) => { + t.plan(1); + + const server = createServer((request, response) => { + let body = ""; + request.on("data", (chunk) => { + body += chunk.toString(); + }); + request.on("end", () => { + const data = JSON.parse(body.split("\n")[2]); + const error = data.exception.values[0]; + + t.equal(error.type, "Error"); + t.equal(error.value, "Oh no!"); + }); + + response.writeHead(200); + response.write("ok"); + response.end(); + }); + + server.listen(0); + await once(server, "listening"); + + const child = spawn(nodeBinaryPath, [cliPath], { + env: { + ...env, + SENTRY_DSN, + }, + }); + child.on("error", t.threw); + child.stdout.on("data", (data) => { + t.match( + data.toString().replace(stripAnsiColorRE, ""), + /^FATAL \(probot\): Oh no!\n/, + ); + }); + child.stdin.write(fatalErrorLine); + + t.teardown(() => { + child.kill(); + server.closeAllConnections(); + server.close(); + }); + }); +}); diff --git a/test/sentry.runtime.test.js b/test/sentry.runtime.test.js new file mode 100644 index 0000000..f3f77ad --- /dev/null +++ b/test/sentry.runtime.test.js @@ -0,0 +1,124 @@ +"use strict"; + +const { withScope } = require("@sentry/node"); + +const { test } = require("tap"); +const { pino } = require("pino"); +const { getTransformStream } = require(".."); + +test("API", (t) => { + t.plan(1); + + let env = Object.assign({}, process.env); + + t.afterEach(() => { + process.env = { ...env }; + }); + + t.test("Sentry integration enabled", (t) => { + t.plan(6); + const transform = getTransformStream({ + sentryDsn: "http://username@example.com/1234", + }); + const log = pino({}, transform); + + function event(payload) { + const error = new Error("Hello from the test"); + error.level = 50; + error.event = { + payload: Object.assign( + { + installation: { + id: "456", + }, + }, + payload, + ), + }; + return error; + } + + t.test("without user", (t) => { + t.plan(1); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.strictSame(event.user, { id: "456" }); + }); + + log.fatal(event({})); + }); + }); + + t.test("with organization", (t) => { + t.plan(1); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.match(event.user, { username: "org" }); + }); + + log.fatal(event({ organization: { login: "org" } })); + }); + }); + + t.test("with installation account", (t) => { + t.plan(1); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.match(event.user, { username: "account" }); + }); + + log.fatal(event({ installation: { account: { login: "account" } } })); + }); + }); + + t.test("with repository owner", (t) => { + t.plan(1); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.match(event.user, { username: "owner" }); + }); + + log.fatal(event({ repository: { owner: { login: "owner" } } })); + }); + }); + + t.test("with repository owner and without installation", (t) => { + t.plan(1); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.match(event.user, { username: "owner" }); + }); + + log.fatal( + event({ + installation: undefined, + repository: { owner: { login: "owner" } }, + }), + ); + }); + }); + + t.test("with logFormat: json", (t) => { + t.plan(1); + + const transform = getTransformStream({ + sentryDsn: "http://username@example.com/1234", + logFormat: "json", + }); + const log = pino({}, transform); + + withScope(function (scope) { + scope.addEventProcessor(function (event, hint) { + t.match(event.user, { username: "owner" }); + }); + + log.fatal(event({ repository: { owner: { login: "owner" } } })); + }); + }); + }); +});