From 66aa32e0c828859d82eb944f81c07e9b7eafd227 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 08:49:56 +0200 Subject: [PATCH 01/37] setup eslint style --- eslint.config.mjs | 2 + package-lock.json | 143 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 2043070..a323adf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,10 +1,12 @@ // @ts-check import eslint from '@eslint/js'; +import stylistic from '@stylistic/eslint-plugin'; import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, + stylistic.configs["recommended-flat"], ...tseslint.configs.recommended, { languageOptions: { diff --git a/package-lock.json b/package-lock.json index b319106..d94f299 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@eslint/js": "^9.2.0", + "@stylistic/eslint-plugin": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", @@ -603,6 +604,132 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.0.0.tgz", + "integrity": "sha512-M3j82FavnsI5Wm8+C2MHqUBijKEVfXmKci3YVaIpKaB9SSY5lJn1+Yh5zk8AqV+qH3qOSJJeFCTyIiQpghxBiA==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.0.0", + "@stylistic/eslint-plugin-jsx": "2.0.0", + "@stylistic/eslint-plugin-plus": "2.0.0", + "@stylistic/eslint-plugin-ts": "2.0.0", + "@types/eslint": "^8.56.10" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.0.0.tgz", + "integrity": "sha512-yQZKvygpWZ5rlIyzdQzmJbT4RwK2bfYn+QCey+WUy6XdgLQ0RHXSFT1ezBZyxWKbNRfoVbbNfs1BqyEDIkXX3A==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-js/node_modules/espree": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.0.0.tgz", + "integrity": "sha512-XwsqawXfCvFdU8iu217xfiVIDXq3xF6zRpmGTrgmnXP/FxBcY9shDz9sZYGwNzEgiBeS14nL1vRuO9NWkYutoA==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.0.0", + "@types/eslint": "^8.56.10", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.0.0.tgz", + "integrity": "sha512-iif+oS6SaAIG9yN3zynNWT2YqP/vq1Os5sgIKocinkjZvbTBdNygq7OoxO+V2PTwx/KwF0z6IXU5OdxwdEizkg==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.0.0.tgz", + "integrity": "sha512-Rsk9XLR+Edv7TNh20Z+pfFA+T6pjPidbK1JYk6UIxc5iVW5Po6E/NOnO0UKD++uWT3XIDsC0LkHUxfKPoXf9TQ==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.0.0", + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, "node_modules/@tapjs/after": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/@tapjs/after/-/after-1.1.20.tgz", @@ -1273,6 +1400,22 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", diff --git a/package.json b/package.json index b1bea88..26cdc75 100644 --- a/package.json +++ b/package.json @@ -33,18 +33,16 @@ "scripts": { "//eslint": "performs static analysis over project files and examples", "eslint": "eslint --fix lib/*.ts test/*.js examples/**/*.js", - "//prettier": "performs code formatting over project files and examples", "prettier": "prettier --write lib/*.ts test/*.js examples/**/*.js", - "//prepare": "prepare project for distribution", "prepare": "tsc", - "//test": "runs tests on main project", "test": "tap --node-arg=-r --node-arg=ts-node/register --disable-coverage test/*_test.js" }, "devDependencies": { "@eslint/js": "^9.2.0", + "@stylistic/eslint-plugin": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", From 3051584bd4878f0efe7a23b7313bc3c7daf12f57 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 08:55:46 +0200 Subject: [PATCH 02/37] rules for quotes --- eslint.config.mjs | 4 ++++ examples/express-sample/app.js | 4 ++-- examples/using-domains/app.js | 6 +++--- lib/raygun.batch.ts | 2 +- lib/raygun.offline.ts | 8 ++++---- lib/raygun.sync.worker.ts | 4 ++-- lib/raygun.ts | 12 ++++++------ test/raygun_uncaught_exception_test.js | 2 +- 8 files changed, 23 insertions(+), 19 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index a323adf..46d3c5b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,10 @@ export default tseslint.config( ], // Required to import JS modules "@typescript-eslint/no-var-requires": "off", + // Always require semicolons + "@stylistic/semi": ["error", "always"], + // Stick to double quotes + "@stylistic/quotes": ["error", "double"], } } ); \ No newline at end of file diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 3b7f0e2..2882b0d 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -2,7 +2,7 @@ var config = require("config"); if (config.Raygun.Key === "YOUR_API_KEY") { console.error( - `[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file`, + "[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file", ); process.exit(1); } @@ -35,7 +35,7 @@ app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); // uncomment after placing your favicon in /public -//app.use(favicon(__dirname + '/public/favicon.ico')); +// app.use(favicon(__dirname + '/public/favicon.ico')); app.use(logger("dev")); app.use(bodyParser.json()); app.use( diff --git a/examples/using-domains/app.js b/examples/using-domains/app.js index 42e1fd7..c78e2f5 100644 --- a/examples/using-domains/app.js +++ b/examples/using-domains/app.js @@ -2,7 +2,7 @@ var config = require("config"); if (config.Raygun.Key === "YOUR_API_KEY") { console.error( - `[Raygun4Node-Domains-Sample] You need to set your Raygun API key in the config file`, + "[Raygun4Node-Domains-Sample] You need to set your Raygun API key in the config file", ); process.exit(1); } @@ -24,7 +24,7 @@ appDomain.on("error", function (err) { .then((message) => { // Exit the process once the error has been sent console.log( - `[Raygun4Node-Domains-Sample] Error sent to Raygun, exiting process`, + "[Raygun4Node-Domains-Sample] Error sent to Raygun, exiting process", ); process.exit(1); }) @@ -40,7 +40,7 @@ appDomain.on("error", function (err) { appDomain.run(function () { var fs = require("fs"); - console.log(`[Raygun4Node-Domains-Sample] Running example app`); + console.log("[Raygun4Node-Domains-Sample] Running example app"); // Try and read a file that doesn't exist fs.readFile("badfile.json", "utf8", function (err, file) { diff --git a/lib/raygun.batch.ts b/lib/raygun.batch.ts index 0fabff2..a7f1ec4 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -71,7 +71,7 @@ export class RaygunBatchTransport { stopProcessing() { if (this.timerId) { - debug(`[raygun.batch.ts] Batch transport - stopping`); + debug("[raygun.batch.ts] Batch transport - stopping"); clearInterval(this.timerId); this.timerId = null; diff --git a/lib/raygun.offline.ts b/lib/raygun.offline.ts index 5f39bb3..ecaee5b 100644 --- a/lib/raygun.offline.ts +++ b/lib/raygun.offline.ts @@ -64,12 +64,12 @@ export class OfflineStorage implements IOfflineStorage { fs.readdir(this.cachePath, (err, files) => { if (err) { - console.error(`[Raygun4Node] Error reading cache folder`, err); + console.error("[Raygun4Node] Error reading cache folder", err); return callback(err); } if (files.length > this.cacheLimit) { - console.error(`[Raygun4Node] Error cache reached limit`); + console.error("[Raygun4Node] Error cache reached limit"); return callback(null); } @@ -81,7 +81,7 @@ export class OfflineStorage implements IOfflineStorage { return callback(null); } - console.error(`[Raygun4Node] Error writing to cache folder`, err); + console.error("[Raygun4Node] Error writing to cache folder", err); return callback(err); }); }); @@ -96,7 +96,7 @@ export class OfflineStorage implements IOfflineStorage { send(callback: (error: Error | null, items?: string[]) => void) { this.retrieve((err, items) => { if (err) { - console.error(`[Raygun4Node] Error reading cache folder`, err); + console.error("[Raygun4Node] Error reading cache folder", err); return callback(err); } diff --git a/lib/raygun.sync.worker.ts b/lib/raygun.sync.worker.ts index fde4954..96d0b44 100644 --- a/lib/raygun.sync.worker.ts +++ b/lib/raygun.sync.worker.ts @@ -11,9 +11,9 @@ transport .send(options) .then((response) => { console.log( - `[Raygun4Node] Successfully reported uncaught exception to Raygun`, + "[Raygun4Node] Successfully reported uncaught exception to Raygun", ); }) .catch((error) => { - console.error(`[Raygun4Node] Error sending with sync transport`, error); + console.error("[Raygun4Node] Error sending with sync transport", error); }); diff --git a/lib/raygun.ts b/lib/raygun.ts index 8de0dcd..959101a 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -99,7 +99,7 @@ class Raygun { this._reportColumnNumbers = options.reportColumnNumbers; this._innerErrorFieldName = options.innerErrorFieldName || "cause"; // VError function to retrieve inner error; - debug(`[raygun.ts] Client initialized`); + debug("[raygun.ts] Client initialized"); if (options.reportUncaughtExceptions) { this.reportUncaughtExceptions(); @@ -211,7 +211,7 @@ class Raygun { if (!sendOptionsResult.valid) { console.error( - `[Raygun4Node] Encountered an error sending an error to Raygun. No API key is configured, please ensure .init is called with api key. See docs for more info.`, + "[Raygun4Node] Encountered an error sending an error to Raygun. No API key is configured, please ensure .init is called with api key. See docs for more info.", ); return Promise.reject(sendOptionsResult.message); } @@ -293,7 +293,7 @@ class Raygun { (major === 13 && minor < 7) ) { console.log( - `[Raygun4Node] Warning: reportUncaughtExceptions requires at least Node v12.17.0 or v13.7.0. Uncaught exceptions will not be automatically reported.`, + "[Raygun4Node] Warning: reportUncaughtExceptions requires at least Node v12.17.0 or v13.7.0. Uncaught exceptions will not be automatically reported.", ); return; @@ -341,14 +341,14 @@ class Raygun { this.send(err, customData || {}, requestParams, [ "UnhandledException", ]).catch((err) => { - console.error(`[Raygun] Failed to send Express error`, err); + console.error("[Raygun] Failed to send Express error", err); }); next(err); } stop() { if (this._batchTransport) { - debug(`[raygun.ts] Batch transport stopped`); + debug("[raygun.ts] Batch transport stopped"); this._batchTransport.stopProcessing(); } } @@ -451,7 +451,7 @@ class Raygun { }) .catch((error) => { console.error( - `[Raygun4Node] Failed to send message from offline transport`, + "[Raygun4Node] Failed to send message from offline transport", error, ); }); diff --git a/test/raygun_uncaught_exception_test.js b/test/raygun_uncaught_exception_test.js index aa94686..36ef059 100644 --- a/test/raygun_uncaught_exception_test.js +++ b/test/raygun_uncaught_exception_test.js @@ -11,7 +11,7 @@ test("reporting uncaught exceptions", async function (t) { await util .promisify(childProcess.exec)( - `node -r ts-node/register ./raygun_uncaught_exception_app.js`, + "node -r ts-node/register ./raygun_uncaught_exception_app.js", { cwd: __dirname, stdio: "inherit", From 3e9633233060ad922f262217e50113dd0bfa5e8b Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 09:30:31 +0200 Subject: [PATCH 03/37] add eslint plugin ts --- eslint.config.mjs | 5 +++++ package-lock.json | 1 + package.json | 1 + 3 files changed, 7 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index 46d3c5b..6042702 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,11 +2,13 @@ import eslint from '@eslint/js'; import stylistic from '@stylistic/eslint-plugin'; +import stylisticTs from '@stylistic/eslint-plugin-ts'; import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, stylistic.configs["recommended-flat"], + stylisticTs.configs["all-flat"], ...tseslint.configs.recommended, { languageOptions: { @@ -35,6 +37,9 @@ export default tseslint.config( "@stylistic/semi": ["error", "always"], // Stick to double quotes "@stylistic/quotes": ["error", "double"], + '@stylistic/ts/indent': ['error', 2], + + // "@stylistic/ts/member-delimiter-style": ["error", "comma"], } } ); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d94f299..afea7d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "devDependencies": { "@eslint/js": "^9.2.0", "@stylistic/eslint-plugin": "^2.0.0", + "@stylistic/eslint-plugin-ts": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", diff --git a/package.json b/package.json index 26cdc75..a899dfa 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "devDependencies": { "@eslint/js": "^9.2.0", "@stylistic/eslint-plugin": "^2.0.0", + "@stylistic/eslint-plugin-ts": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", From b13402ba538b5e7cec9c77f6b6f1baf6b2d265c2 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 09:43:43 +0200 Subject: [PATCH 04/37] wip configs --- eslint.config.mjs | 19 ++- lib/raygun.batch.ts | 29 +++-- lib/raygun.messageBuilder.ts | 16 +-- lib/raygun.offline.ts | 2 + lib/raygun.transport.ts | 2 +- lib/raygun.ts | 45 ++++--- lib/types.ts | 186 ++++++++++++++--------------- test/raygun.messageBuilder_test.js | 6 +- test/raygun_express_test.js | 12 +- test/raygun_offline_test.js | 2 +- 10 files changed, 180 insertions(+), 139 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 6042702..1b70594 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,7 +39,24 @@ export default tseslint.config( "@stylistic/quotes": ["error", "double"], '@stylistic/ts/indent': ['error', 2], - // "@stylistic/ts/member-delimiter-style": ["error", "comma"], + "@stylistic/ts/member-delimiter-style": ["error", + { + "multiline": { + "delimiter": "comma", + "requireLast": true + }, + "singleline": { + "delimiter": "comma", + "requireLast": false + }, + "multilineDetection": "brackets" + } + ], + + "@stylistic/ts/object-curly-spacing": ["error", "always"], + "@stylistic/ts/comma-dangle": ["off", 0], + "@stylistic/ts/quote-props": ["off", 0], + "@stylistic/member-delimiter-style": ["off", 0], } } ); \ No newline at end of file diff --git a/lib/raygun.batch.ts b/lib/raygun.batch.ts index a7f1ec4..cedc0e0 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -11,19 +11,19 @@ import { const debug = require("debug")("raygun"); export type MessageAndCallback = { - serializedMessage: string; - callback: Callback | null; + serializedMessage: string, + callback: Callback | null, }; export type PreparedBatch = { - payload: string; - messageCount: number; - callbacks: Array | null>; + payload: string, + messageCount: number, + callbacks: Array | null>, }; export type BatchState = { - messages: MessageAndCallback[]; - messageSizeInBytes: number; + messages: MessageAndCallback[], + messageSizeInBytes: number, }; export const MAX_MESSAGES_IN_BATCH = 100; @@ -32,13 +32,16 @@ const MAX_BATCH_INNER_SIZE_BYTES = MAX_BATCH_SIZE_BYTES - 2; // for the starting export class RaygunBatchTransport { private timerId: NodeJS.Timeout | null = null; + private httpOptions: HTTPOptions; + private interval: number; + private batchId: number = 0; private batchState: BatchState = { messages: [], messageSizeInBytes: 0 }; - constructor(options: { interval: number; httpOptions: HTTPOptions }) { + constructor(options: { interval: number, httpOptions: HTTPOptions }) { this.interval = options.interval; this.httpOptions = options.httpOptions; } @@ -91,9 +94,9 @@ export class RaygunBatchTransport { throw Error(errorMessage); } - const messageIsTooLargeToAddToBatch = - this.batchState.messageSizeInBytes + messageLength > - MAX_BATCH_INNER_SIZE_BYTES; + const messageIsTooLargeToAddToBatch + = this.batchState.messageSizeInBytes + messageLength + > MAX_BATCH_INNER_SIZE_BYTES; if (messageIsTooLargeToAddToBatch) { this.processBatch(); @@ -116,13 +119,13 @@ export class RaygunBatchTransport { private processBatch() { const payload = this.batchState.messages - .map((m) => m.serializedMessage) + .map(m => m.serializedMessage) .join(","); const batch: PreparedBatch = { payload: `[${payload}]`, messageCount: this.batchState.messages.length, - callbacks: this.batchState.messages.map((m) => m.callback), + callbacks: this.batchState.messages.map(m => m.callback), }; this.sendBatch(batch); diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index c438e3c..7512153 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -83,8 +83,8 @@ function getStackTrace( }; if ( - !!options.reportColumnNumbers && - typeof callSite.getColumnNumber === "function" + !!options.reportColumnNumbers + && typeof callSite.getColumnNumber === "function" ) { frame.columnNumber = callSite.getColumnNumber(); } @@ -108,8 +108,8 @@ function buildError( let innerError: Error | undefined = undefined; if (options.innerErrorFieldName) { - innerError = - typeof error[options.innerErrorFieldName!] === "function" + innerError + = typeof error[options.innerErrorFieldName!] === "function" ? error[options.innerErrorFieldName!]() : error[options.innerErrorFieldName!]; } @@ -123,7 +123,9 @@ function buildError( export class RaygunMessageBuilder { _filters: string[]; + options: MessageBuilderOptions; + message: MessageBuilding; constructor(options: MessageBuilderOptions = {}) { @@ -151,9 +153,9 @@ export class RaygunMessageBuilder { // eslint-disable-next-line @typescript-eslint/no-explicit-any setErrorDetails(error: Error | string | any) { if ( - !(error instanceof Error) && - typeof error !== "string" && - this.options.useHumanStringForObject + !(error instanceof Error) + && typeof error !== "string" + && this.options.useHumanStringForObject ) { error = humanString(error); this.message.details.groupingKey = error diff --git a/lib/raygun.offline.ts b/lib/raygun.offline.ts index ecaee5b..95c5040 100644 --- a/lib/raygun.offline.ts +++ b/lib/raygun.offline.ts @@ -20,7 +20,9 @@ const debug = require("debug")("raygun"); export class OfflineStorage implements IOfflineStorage { cachePath: string = ""; + cacheLimit: number = 100; + transport: MessageTransport; constructor(transport: MessageTransport) { diff --git a/lib/raygun.transport.ts b/lib/raygun.transport.ts index 3cd82f9..8a0b288 100644 --- a/lib/raygun.transport.ts +++ b/lib/raygun.transport.ts @@ -42,7 +42,7 @@ export function send( path: path, method: "POST", headers: { - Host: API_HOST, + "Host": API_HOST, "Content-Type": "application/json", "Content-Length": data.length, "X-ApiKey": options.http.apiKey, diff --git a/lib/raygun.ts b/lib/raygun.ts index 959101a..6d5d589 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -36,8 +36,8 @@ import * as raygunSyncTransport from "./raygun.sync.transport"; import { v4 as uuidv4 } from "uuid"; type SendOptionsResult = - | { valid: true; message: Message; options: SendOptions } - | { valid: false; message: Message }; + | { valid: true, message: Message, options: SendOptions } + | { valid: false, message: Message }; const debug = require("debug")("raygun"); @@ -64,22 +64,39 @@ function emptyCallback() {} class Raygun { _apiKey: string | undefined; + _filters: string[] = []; + _user: RawUserData | undefined; + _version: string = ""; + _host: string | undefined; + _port: number | undefined; + _useSSL: boolean | undefined; + _onBeforeSend: Hook | undefined; + _offlineStorage: IOfflineStorage | undefined; + _isOffline: boolean | undefined; + _offlineStorageOptions: OfflineStorageOptions | undefined; + _groupingKey: Hook | undefined; + _tags: Tag[] | undefined; + _useHumanStringForObject: boolean | undefined; + _reportColumnNumbers: boolean | undefined; + _innerErrorFieldName: string | undefined; + _batch: boolean = false; + _batchTransport: RaygunBatchTransport | undefined; init(options: RaygunOptions) { @@ -92,8 +109,8 @@ class Raygun { this._isOffline = options.isOffline; this._groupingKey = options.groupingKey; this._tags = options.tags; - this._useHumanStringForObject = - options.useHumanStringForObject === undefined + this._useHumanStringForObject + = options.useHumanStringForObject === undefined ? true : options.useHumanStringForObject; this._reportColumnNumbers = options.reportColumnNumbers; @@ -123,8 +140,8 @@ class Raygun { this.sendWithCallback = this.sendWithCallback.bind(this); this.send = this.send.bind(this); - this._offlineStorage = - options.offlineStorage || new OfflineStorage(this.offlineTransport()); + this._offlineStorage + = options.offlineStorage || new OfflineStorage(this.offlineTransport()); this._offlineStorageOptions = options.offlineStorageOptions; if (this._isOffline) { @@ -285,12 +302,12 @@ class Raygun { private reportUncaughtExceptions() { const [major, minor] = process.versions.node .split(".") - .map((part) => parseInt(part, 10)); + .map(part => parseInt(part, 10)); if ( - major < 12 || - (major === 12 && minor < 17) || - (major === 13 && minor < 7) + major < 12 + || major === 12 && minor < 17 + || major === 13 && minor < 7 ) { console.log( "[Raygun4Node] Warning: reportUncaughtExceptions requires at least Node v12.17.0 or v13.7.0. Uncaught exceptions will not be automatically reported.", @@ -387,15 +404,15 @@ class Raygun { let message = builder.build(); if (this._groupingKey) { - message.details.groupingKey = - typeof this._groupingKey === "function" + message.details.groupingKey + = typeof this._groupingKey === "function" ? this._groupingKey(message, exception, customData, request, tags) : null; } if (this._onBeforeSend) { - message = - typeof this._onBeforeSend === "function" + message + = typeof this._onBeforeSend === "function" ? this._onBeforeSend(message, exception, customData, request, tags) : message; } diff --git a/lib/types.ts b/lib/types.ts index 7fc506a..d169980 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -6,76 +6,76 @@ import type { IncomingMessage } from "http"; export type IndexableError = Error & Record; export type MessageBuilderOptions = { - reportColumnNumbers?: boolean; - useHumanStringForObject?: boolean; - innerErrorFieldName?: string; - filters?: string[]; + reportColumnNumbers?: boolean, + useHumanStringForObject?: boolean, + innerErrorFieldName?: string, + filters?: string[], }; export type StackFrame = { - lineNumber: number; - columnNumber?: number; - className: string; - fileName: string; - methodName: string; + lineNumber: number, + columnNumber?: number, + className: string, + fileName: string, + methodName: string, }; export type Message = { - occurredOn: Date; - details: MessageDetails; + occurredOn: Date, + details: MessageDetails, }; export type MessageBuilding = { - occurredOn: Date; - details: Partial; + occurredOn: Date, + details: Partial, }; export type BuiltError = { - message: string; - stackTrace?: StackFrame[]; - className?: string; - innerError?: BuiltError; + message: string, + stackTrace?: StackFrame[], + className?: string, + innerError?: BuiltError, }; export type MessageDetails = { client: { - name: string; - version: string; - }; - groupingKey: string | null; - error: BuiltError; - version: string; - user: UserDetails; - request: RequestDetails; - tags: Tag[]; - userCustomData: CustomData; - machineName: string; - environment: Environment; - correlationId: string | null; + name: string, + version: string, + }, + groupingKey: string | null, + error: BuiltError, + version: string, + user: UserDetails, + request: RequestDetails, + tags: Tag[], + userCustomData: CustomData, + machineName: string, + environment: Environment, + correlationId: string | null, }; export type Environment = { - osVersion: string; - architecture: string; - totalPhysicalMemory: number; - availablePhysicalMemory: number; - utcOffset: number; - processorCount?: number; - cpu?: string; + osVersion: string, + architecture: string, + totalPhysicalMemory: number, + availablePhysicalMemory: number, + utcOffset: number, + processorCount?: number, + cpu?: string, }; export type Tag = string; export type SendOptions = { - message: string; - http: HTTPOptions; + message: string, + http: HTTPOptions, }; export type HTTPOptions = { - useSSL: boolean; - host: string | undefined; - port: number | undefined; - apiKey: string; + useSSL: boolean, + host: string | undefined, + port: number | undefined, + apiKey: string, }; // Allow any because users are free to set anything as CustomData @@ -86,53 +86,53 @@ export type RequestParams = ({ host: string } | { hostname: string }) & CommonRequestParams; type CommonRequestParams = { - path: string; - method: string; - ip: string; - query: object; - headers: object; - body: object; + path: string, + method: string, + ip: string, + query: object, + headers: object, + body: object, }; export type RequestDetails = { - hostName: string; - url: string; - httpMethod: string; - ipAddress: string; - queryString: object; - headers: object; - form: object; + hostName: string, + url: string, + httpMethod: string, + ipAddress: string, + queryString: object, + headers: object, + form: object, }; export type UserDetails = { - identifier?: string; - uuid?: string; - firstName?: string; - lastName?: string; - fullName?: string; - email?: string; + identifier?: string, + uuid?: string, + firstName?: string, + lastName?: string, + fullName?: string, + email?: string, }; export type RawUserData = { - identifier?: string; - uuid?: string; - firstName?: string; - lastName?: string; - fullName?: string; - email?: string; + identifier?: string, + uuid?: string, + firstName?: string, + lastName?: string, + fullName?: string, + email?: string, }; export type OfflineStorageOptions = { - cachePath: string; - cacheLimit?: number; + cachePath: string, + cacheLimit?: number, }; export type Transport = { - send(options: SendOptions): Promise; + send(options: SendOptions): Promise, }; export type MessageTransport = { - send(message: string): void; + send(message: string): void, }; export type Hook = ( @@ -144,32 +144,32 @@ export type Hook = ( ) => T; export interface IOfflineStorage { - init(options: OfflineStorageOptions | undefined): void; - save(message: string, callback: (error: Error | null) => void): void; + init(options: OfflineStorageOptions | undefined): void, + save(message: string, callback: (error: Error | null) => void): void, retrieve( callback: (error: NodeJS.ErrnoException | null, items: string[]) => void, - ): void; - send(callback: (error: Error | null, items?: string[]) => void): void; + ): void, + send(callback: (error: Error | null, items?: string[]) => void): void, } export type RaygunOptions = { - apiKey: string; - filters?: string[]; - host?: string; - port?: number; - useSSL?: boolean; - onBeforeSend?: Hook; - offlineStorage?: IOfflineStorage; - offlineStorageOptions?: OfflineStorageOptions; - isOffline?: boolean; - groupingKey?: Hook; - tags?: Tag[]; - useHumanStringForObject?: boolean; - reportColumnNumbers?: boolean; - innerErrorFieldName?: string; - batch?: boolean; - batchFrequency?: number; - reportUncaughtExceptions?: boolean; + apiKey: string, + filters?: string[], + host?: string, + port?: number, + useSSL?: boolean, + onBeforeSend?: Hook, + offlineStorage?: IOfflineStorage, + offlineStorageOptions?: OfflineStorageOptions, + isOffline?: boolean, + groupingKey?: Hook, + tags?: Tag[], + useHumanStringForObject?: boolean, + reportColumnNumbers?: boolean, + innerErrorFieldName?: string, + batch?: boolean, + batchFrequency?: number, + reportUncaughtExceptions?: boolean, }; export type CallbackNoError = (t: T | null) => void; diff --git a/test/raygun.messageBuilder_test.js b/test/raygun.messageBuilder_test.js index 43c2482..e1b0d6a 100644 --- a/test/raygun.messageBuilder_test.js +++ b/test/raygun.messageBuilder_test.js @@ -2,8 +2,8 @@ const deepEqual = require("assert").deepEqual; var test = require("tap").test; -var MessageBuilder = - require("../lib/raygun.messageBuilder.ts").RaygunMessageBuilder; +var MessageBuilder + = require("../lib/raygun.messageBuilder.ts").RaygunMessageBuilder; var VError = require("verror"); test("basic builder tests", function (t) { @@ -369,7 +369,7 @@ test("filter keys tests", function (t) { remember: true, }; var queryString = { username: "admin@raygun.io", remember: false }; - var headers = { "X-ApiKey": "123456", Host: "app.raygun.io" }; + var headers = { "X-ApiKey": "123456", "Host": "app.raygun.io" }; builder.setRequestDetails({ body: body, query: queryString, diff --git a/test/raygun_express_test.js b/test/raygun_express_test.js index 38a9e3a..25e7437 100644 --- a/test/raygun_express_test.js +++ b/test/raygun_express_test.js @@ -30,8 +30,8 @@ test("reporting express errors", async function (t) { }); test("batch reporting errors", async function (t) { - const { client, server, stop, nextBatchRequest } = - await makeClientWithMockServer({ + const { client, server, stop, nextBatchRequest } + = await makeClientWithMockServer({ batch: true, batchFrequency: 1000, }); @@ -49,14 +49,14 @@ test("batch reporting errors", async function (t) { t.equal(server.entries.length, 0); t.equal(server.bulkEntries.length, 1); t.same( - server.bulkEntries[0].map((e) => e.details.error.message), + server.bulkEntries[0].map(e => e.details.error.message), ["a", "b", "c"], ); }); test("batch transport discards massive errors", async function (t) { - const { client, server, stop, nextBatchRequest } = - await makeClientWithMockServer({ + const { client, server, stop, nextBatchRequest } + = await makeClientWithMockServer({ batch: true, batchFrequency: 1000, }); @@ -73,7 +73,7 @@ test("batch transport discards massive errors", async function (t) { t.equal(server.entries.length, 0); t.equal(server.bulkEntries.length, 1); t.same( - server.bulkEntries[0].map((e) => e.details.error.message), + server.bulkEntries[0].map(e => e.details.error.message), ["b"], ); }); diff --git a/test/raygun_offline_test.js b/test/raygun_offline_test.js index 69e058d..9823a22 100644 --- a/test/raygun_offline_test.js +++ b/test/raygun_offline_test.js @@ -91,7 +91,7 @@ test("batched offline message storage and sending", async function (t) { `Expected to find no stored error files but instead found ${filesAfterSend.length}`, ); - t.same(batch.map((e) => e.details.error.message).sort(), [ + t.same(batch.map(e => e.details.error.message).sort(), [ "offline error", "offline error 2", ]); From 397d2dc87018c013e2f8266b481e0458f7de15ad Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 09:51:40 +0200 Subject: [PATCH 05/37] wip --- eslint.config.mjs | 29 +++-- lib/raygun.batch.ts | 26 ++-- lib/raygun.messageBuilder.ts | 14 +-- lib/raygun.transport.ts | 2 +- lib/raygun.ts | 28 ++--- lib/types.ts | 186 ++++++++++++++--------------- test/raygun.messageBuilder_test.js | 6 +- test/raygun_express_test.js | 12 +- test/raygun_offline_test.js | 2 +- 9 files changed, 154 insertions(+), 151 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1b70594..af53b84 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -39,24 +39,27 @@ export default tseslint.config( "@stylistic/quotes": ["error", "double"], '@stylistic/ts/indent': ['error', 2], - "@stylistic/ts/member-delimiter-style": ["error", - { - "multiline": { - "delimiter": "comma", - "requireLast": true - }, - "singleline": { - "delimiter": "comma", - "requireLast": false - }, - "multilineDetection": "brackets" - } - ], + // "@stylistic/ts/member-delimiter-style": ["error", + // { + // "multiline": { + // "delimiter": "comma", + // "requireLast": true + // }, + // "singleline": { + // "delimiter": "comma", + // "requireLast": false + // }, + // "multilineDetection": "brackets" + // } + // ], "@stylistic/ts/object-curly-spacing": ["error", "always"], "@stylistic/ts/comma-dangle": ["off", 0], "@stylistic/ts/quote-props": ["off", 0], "@stylistic/member-delimiter-style": ["off", 0], + "@stylistic/ts/space-before-function-paren": ["off", 0], + "@stylistic/brace-style": ["error", "1tbs"], + "@stylistic/no-mixed-operators": ["off", 0], } } ); \ No newline at end of file diff --git a/lib/raygun.batch.ts b/lib/raygun.batch.ts index cedc0e0..4655802 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -11,19 +11,19 @@ import { const debug = require("debug")("raygun"); export type MessageAndCallback = { - serializedMessage: string, - callback: Callback | null, + serializedMessage: string; + callback: Callback | null; }; export type PreparedBatch = { - payload: string, - messageCount: number, - callbacks: Array | null>, + payload: string; + messageCount: number; + callbacks: Array | null>; }; export type BatchState = { - messages: MessageAndCallback[], - messageSizeInBytes: number, + messages: MessageAndCallback[]; + messageSizeInBytes: number; }; export const MAX_MESSAGES_IN_BATCH = 100; @@ -41,7 +41,7 @@ export class RaygunBatchTransport { private batchState: BatchState = { messages: [], messageSizeInBytes: 0 }; - constructor(options: { interval: number, httpOptions: HTTPOptions }) { + constructor(options: { interval: number; httpOptions: HTTPOptions }) { this.interval = options.interval; this.httpOptions = options.httpOptions; } @@ -94,9 +94,9 @@ export class RaygunBatchTransport { throw Error(errorMessage); } - const messageIsTooLargeToAddToBatch - = this.batchState.messageSizeInBytes + messageLength - > MAX_BATCH_INNER_SIZE_BYTES; + const messageIsTooLargeToAddToBatch = + this.batchState.messageSizeInBytes + messageLength > + MAX_BATCH_INNER_SIZE_BYTES; if (messageIsTooLargeToAddToBatch) { this.processBatch(); @@ -119,13 +119,13 @@ export class RaygunBatchTransport { private processBatch() { const payload = this.batchState.messages - .map(m => m.serializedMessage) + .map((m) => m.serializedMessage) .join(","); const batch: PreparedBatch = { payload: `[${payload}]`, messageCount: this.batchState.messages.length, - callbacks: this.batchState.messages.map(m => m.callback), + callbacks: this.batchState.messages.map((m) => m.callback), }; this.sendBatch(batch); diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index 7512153..96ebf29 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -83,8 +83,8 @@ function getStackTrace( }; if ( - !!options.reportColumnNumbers - && typeof callSite.getColumnNumber === "function" + !!options.reportColumnNumbers && + typeof callSite.getColumnNumber === "function" ) { frame.columnNumber = callSite.getColumnNumber(); } @@ -108,8 +108,8 @@ function buildError( let innerError: Error | undefined = undefined; if (options.innerErrorFieldName) { - innerError - = typeof error[options.innerErrorFieldName!] === "function" + innerError = + typeof error[options.innerErrorFieldName!] === "function" ? error[options.innerErrorFieldName!]() : error[options.innerErrorFieldName!]; } @@ -153,9 +153,9 @@ export class RaygunMessageBuilder { // eslint-disable-next-line @typescript-eslint/no-explicit-any setErrorDetails(error: Error | string | any) { if ( - !(error instanceof Error) - && typeof error !== "string" - && this.options.useHumanStringForObject + !(error instanceof Error) && + typeof error !== "string" && + this.options.useHumanStringForObject ) { error = humanString(error); this.message.details.groupingKey = error diff --git a/lib/raygun.transport.ts b/lib/raygun.transport.ts index 8a0b288..3cd82f9 100644 --- a/lib/raygun.transport.ts +++ b/lib/raygun.transport.ts @@ -42,7 +42,7 @@ export function send( path: path, method: "POST", headers: { - "Host": API_HOST, + Host: API_HOST, "Content-Type": "application/json", "Content-Length": data.length, "X-ApiKey": options.http.apiKey, diff --git a/lib/raygun.ts b/lib/raygun.ts index 6d5d589..cf1c55f 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -36,8 +36,8 @@ import * as raygunSyncTransport from "./raygun.sync.transport"; import { v4 as uuidv4 } from "uuid"; type SendOptionsResult = - | { valid: true, message: Message, options: SendOptions } - | { valid: false, message: Message }; + | { valid: true; message: Message; options: SendOptions } + | { valid: false; message: Message }; const debug = require("debug")("raygun"); @@ -109,8 +109,8 @@ class Raygun { this._isOffline = options.isOffline; this._groupingKey = options.groupingKey; this._tags = options.tags; - this._useHumanStringForObject - = options.useHumanStringForObject === undefined + this._useHumanStringForObject = + options.useHumanStringForObject === undefined ? true : options.useHumanStringForObject; this._reportColumnNumbers = options.reportColumnNumbers; @@ -140,8 +140,8 @@ class Raygun { this.sendWithCallback = this.sendWithCallback.bind(this); this.send = this.send.bind(this); - this._offlineStorage - = options.offlineStorage || new OfflineStorage(this.offlineTransport()); + this._offlineStorage = + options.offlineStorage || new OfflineStorage(this.offlineTransport()); this._offlineStorageOptions = options.offlineStorageOptions; if (this._isOffline) { @@ -302,12 +302,12 @@ class Raygun { private reportUncaughtExceptions() { const [major, minor] = process.versions.node .split(".") - .map(part => parseInt(part, 10)); + .map((part) => parseInt(part, 10)); if ( - major < 12 - || major === 12 && minor < 17 - || major === 13 && minor < 7 + major < 12 || + (major === 12 && minor < 17) || + (major === 13 && minor < 7) ) { console.log( "[Raygun4Node] Warning: reportUncaughtExceptions requires at least Node v12.17.0 or v13.7.0. Uncaught exceptions will not be automatically reported.", @@ -404,15 +404,15 @@ class Raygun { let message = builder.build(); if (this._groupingKey) { - message.details.groupingKey - = typeof this._groupingKey === "function" + message.details.groupingKey = + typeof this._groupingKey === "function" ? this._groupingKey(message, exception, customData, request, tags) : null; } if (this._onBeforeSend) { - message - = typeof this._onBeforeSend === "function" + message = + typeof this._onBeforeSend === "function" ? this._onBeforeSend(message, exception, customData, request, tags) : message; } diff --git a/lib/types.ts b/lib/types.ts index d169980..7fc506a 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -6,76 +6,76 @@ import type { IncomingMessage } from "http"; export type IndexableError = Error & Record; export type MessageBuilderOptions = { - reportColumnNumbers?: boolean, - useHumanStringForObject?: boolean, - innerErrorFieldName?: string, - filters?: string[], + reportColumnNumbers?: boolean; + useHumanStringForObject?: boolean; + innerErrorFieldName?: string; + filters?: string[]; }; export type StackFrame = { - lineNumber: number, - columnNumber?: number, - className: string, - fileName: string, - methodName: string, + lineNumber: number; + columnNumber?: number; + className: string; + fileName: string; + methodName: string; }; export type Message = { - occurredOn: Date, - details: MessageDetails, + occurredOn: Date; + details: MessageDetails; }; export type MessageBuilding = { - occurredOn: Date, - details: Partial, + occurredOn: Date; + details: Partial; }; export type BuiltError = { - message: string, - stackTrace?: StackFrame[], - className?: string, - innerError?: BuiltError, + message: string; + stackTrace?: StackFrame[]; + className?: string; + innerError?: BuiltError; }; export type MessageDetails = { client: { - name: string, - version: string, - }, - groupingKey: string | null, - error: BuiltError, - version: string, - user: UserDetails, - request: RequestDetails, - tags: Tag[], - userCustomData: CustomData, - machineName: string, - environment: Environment, - correlationId: string | null, + name: string; + version: string; + }; + groupingKey: string | null; + error: BuiltError; + version: string; + user: UserDetails; + request: RequestDetails; + tags: Tag[]; + userCustomData: CustomData; + machineName: string; + environment: Environment; + correlationId: string | null; }; export type Environment = { - osVersion: string, - architecture: string, - totalPhysicalMemory: number, - availablePhysicalMemory: number, - utcOffset: number, - processorCount?: number, - cpu?: string, + osVersion: string; + architecture: string; + totalPhysicalMemory: number; + availablePhysicalMemory: number; + utcOffset: number; + processorCount?: number; + cpu?: string; }; export type Tag = string; export type SendOptions = { - message: string, - http: HTTPOptions, + message: string; + http: HTTPOptions; }; export type HTTPOptions = { - useSSL: boolean, - host: string | undefined, - port: number | undefined, - apiKey: string, + useSSL: boolean; + host: string | undefined; + port: number | undefined; + apiKey: string; }; // Allow any because users are free to set anything as CustomData @@ -86,53 +86,53 @@ export type RequestParams = ({ host: string } | { hostname: string }) & CommonRequestParams; type CommonRequestParams = { - path: string, - method: string, - ip: string, - query: object, - headers: object, - body: object, + path: string; + method: string; + ip: string; + query: object; + headers: object; + body: object; }; export type RequestDetails = { - hostName: string, - url: string, - httpMethod: string, - ipAddress: string, - queryString: object, - headers: object, - form: object, + hostName: string; + url: string; + httpMethod: string; + ipAddress: string; + queryString: object; + headers: object; + form: object; }; export type UserDetails = { - identifier?: string, - uuid?: string, - firstName?: string, - lastName?: string, - fullName?: string, - email?: string, + identifier?: string; + uuid?: string; + firstName?: string; + lastName?: string; + fullName?: string; + email?: string; }; export type RawUserData = { - identifier?: string, - uuid?: string, - firstName?: string, - lastName?: string, - fullName?: string, - email?: string, + identifier?: string; + uuid?: string; + firstName?: string; + lastName?: string; + fullName?: string; + email?: string; }; export type OfflineStorageOptions = { - cachePath: string, - cacheLimit?: number, + cachePath: string; + cacheLimit?: number; }; export type Transport = { - send(options: SendOptions): Promise, + send(options: SendOptions): Promise; }; export type MessageTransport = { - send(message: string): void, + send(message: string): void; }; export type Hook = ( @@ -144,32 +144,32 @@ export type Hook = ( ) => T; export interface IOfflineStorage { - init(options: OfflineStorageOptions | undefined): void, - save(message: string, callback: (error: Error | null) => void): void, + init(options: OfflineStorageOptions | undefined): void; + save(message: string, callback: (error: Error | null) => void): void; retrieve( callback: (error: NodeJS.ErrnoException | null, items: string[]) => void, - ): void, - send(callback: (error: Error | null, items?: string[]) => void): void, + ): void; + send(callback: (error: Error | null, items?: string[]) => void): void; } export type RaygunOptions = { - apiKey: string, - filters?: string[], - host?: string, - port?: number, - useSSL?: boolean, - onBeforeSend?: Hook, - offlineStorage?: IOfflineStorage, - offlineStorageOptions?: OfflineStorageOptions, - isOffline?: boolean, - groupingKey?: Hook, - tags?: Tag[], - useHumanStringForObject?: boolean, - reportColumnNumbers?: boolean, - innerErrorFieldName?: string, - batch?: boolean, - batchFrequency?: number, - reportUncaughtExceptions?: boolean, + apiKey: string; + filters?: string[]; + host?: string; + port?: number; + useSSL?: boolean; + onBeforeSend?: Hook; + offlineStorage?: IOfflineStorage; + offlineStorageOptions?: OfflineStorageOptions; + isOffline?: boolean; + groupingKey?: Hook; + tags?: Tag[]; + useHumanStringForObject?: boolean; + reportColumnNumbers?: boolean; + innerErrorFieldName?: string; + batch?: boolean; + batchFrequency?: number; + reportUncaughtExceptions?: boolean; }; export type CallbackNoError = (t: T | null) => void; diff --git a/test/raygun.messageBuilder_test.js b/test/raygun.messageBuilder_test.js index e1b0d6a..43c2482 100644 --- a/test/raygun.messageBuilder_test.js +++ b/test/raygun.messageBuilder_test.js @@ -2,8 +2,8 @@ const deepEqual = require("assert").deepEqual; var test = require("tap").test; -var MessageBuilder - = require("../lib/raygun.messageBuilder.ts").RaygunMessageBuilder; +var MessageBuilder = + require("../lib/raygun.messageBuilder.ts").RaygunMessageBuilder; var VError = require("verror"); test("basic builder tests", function (t) { @@ -369,7 +369,7 @@ test("filter keys tests", function (t) { remember: true, }; var queryString = { username: "admin@raygun.io", remember: false }; - var headers = { "X-ApiKey": "123456", "Host": "app.raygun.io" }; + var headers = { "X-ApiKey": "123456", Host: "app.raygun.io" }; builder.setRequestDetails({ body: body, query: queryString, diff --git a/test/raygun_express_test.js b/test/raygun_express_test.js index 25e7437..38a9e3a 100644 --- a/test/raygun_express_test.js +++ b/test/raygun_express_test.js @@ -30,8 +30,8 @@ test("reporting express errors", async function (t) { }); test("batch reporting errors", async function (t) { - const { client, server, stop, nextBatchRequest } - = await makeClientWithMockServer({ + const { client, server, stop, nextBatchRequest } = + await makeClientWithMockServer({ batch: true, batchFrequency: 1000, }); @@ -49,14 +49,14 @@ test("batch reporting errors", async function (t) { t.equal(server.entries.length, 0); t.equal(server.bulkEntries.length, 1); t.same( - server.bulkEntries[0].map(e => e.details.error.message), + server.bulkEntries[0].map((e) => e.details.error.message), ["a", "b", "c"], ); }); test("batch transport discards massive errors", async function (t) { - const { client, server, stop, nextBatchRequest } - = await makeClientWithMockServer({ + const { client, server, stop, nextBatchRequest } = + await makeClientWithMockServer({ batch: true, batchFrequency: 1000, }); @@ -73,7 +73,7 @@ test("batch transport discards massive errors", async function (t) { t.equal(server.entries.length, 0); t.equal(server.bulkEntries.length, 1); t.same( - server.bulkEntries[0].map(e => e.details.error.message), + server.bulkEntries[0].map((e) => e.details.error.message), ["b"], ); }); diff --git a/test/raygun_offline_test.js b/test/raygun_offline_test.js index 9823a22..69e058d 100644 --- a/test/raygun_offline_test.js +++ b/test/raygun_offline_test.js @@ -91,7 +91,7 @@ test("batched offline message storage and sending", async function (t) { `Expected to find no stored error files but instead found ${filesAfterSend.length}`, ); - t.same(batch.map(e => e.details.error.message).sort(), [ + t.same(batch.map((e) => e.details.error.message).sort(), [ "offline error", "offline error 2", ]); From 093e31421e370d7f3c95570ada69f3658a8464c3 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 09:57:05 +0200 Subject: [PATCH 06/37] add rules --- eslint.config.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eslint.config.mjs b/eslint.config.mjs index af53b84..b2fa695 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -60,6 +60,10 @@ export default tseslint.config( "@stylistic/ts/space-before-function-paren": ["off", 0], "@stylistic/brace-style": ["error", "1tbs"], "@stylistic/no-mixed-operators": ["off", 0], + + "@stylistic/arrow-parens": ["error", "always"], + // Collides with Prettier + "@stylistic/operator-linebreak": ["off", 0], } } ); \ No newline at end of file From 0aec06713773c83e11af1c02d03fceb84eac0d9e Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 10:01:03 +0200 Subject: [PATCH 07/37] define rules --- eslint.config.mjs | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index b2fa695..ca64b35 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,32 +38,21 @@ export default tseslint.config( // Stick to double quotes "@stylistic/quotes": ["error", "double"], '@stylistic/ts/indent': ['error', 2], - - // "@stylistic/ts/member-delimiter-style": ["error", - // { - // "multiline": { - // "delimiter": "comma", - // "requireLast": true - // }, - // "singleline": { - // "delimiter": "comma", - // "requireLast": false - // }, - // "multilineDetection": "brackets" - // } - // ], - + // Enforce curly braces spacing "@stylistic/ts/object-curly-spacing": ["error", "always"], - "@stylistic/ts/comma-dangle": ["off", 0], - "@stylistic/ts/quote-props": ["off", 0], - "@stylistic/member-delimiter-style": ["off", 0], - "@stylistic/ts/space-before-function-paren": ["off", 0], + // Enforce "one true brace style" "@stylistic/brace-style": ["error", "1tbs"], - "@stylistic/no-mixed-operators": ["off", 0], - + // Enforce parenthesis in functions: "(a) => a" "@stylistic/arrow-parens": ["error", "always"], - // Collides with Prettier + // Disabled rules that collide with Prettier + "@stylistic/member-delimiter-style": ["off", 0], + "@stylistic/no-mixed-operators": ["off", 0], "@stylistic/operator-linebreak": ["off", 0], + "@stylistic/quote-props": ["off", 0], + "@stylistic/ts/comma-dangle": ["off", 0], + "@stylistic/ts/no-extra-parens": ["off", 0], + "@stylistic/ts/quote-props": ["off", 0], + "@stylistic/ts/space-before-function-paren": ["off", 0], } } ); \ No newline at end of file From ebe0b55e44ea3bb26d52af4e53074bbfd5d73d5f Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 10:07:18 +0200 Subject: [PATCH 08/37] add comment --- eslint.config.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index ca64b35..aa0feab 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,9 +6,13 @@ import stylisticTs from '@stylistic/eslint-plugin-ts'; import tseslint from 'typescript-eslint'; export default tseslint.config( + // Basic eslint rules eslint.configs.recommended, + // Codestyle rules for JS stylistic.configs["recommended-flat"], + // Codestyle rules for TS stylisticTs.configs["all-flat"], + // Eslint rules for TS ...tseslint.configs.recommended, { languageOptions: { @@ -37,6 +41,7 @@ export default tseslint.config( "@stylistic/semi": ["error", "always"], // Stick to double quotes "@stylistic/quotes": ["error", "double"], + // Always indent with two spaces '@stylistic/ts/indent': ['error', 2], // Enforce curly braces spacing "@stylistic/ts/object-curly-spacing": ["error", "always"], @@ -44,7 +49,7 @@ export default tseslint.config( "@stylistic/brace-style": ["error", "1tbs"], // Enforce parenthesis in functions: "(a) => a" "@stylistic/arrow-parens": ["error", "always"], - // Disabled rules that collide with Prettier + // Disabled rules that collide with Prettier formatter "@stylistic/member-delimiter-style": ["off", 0], "@stylistic/no-mixed-operators": ["off", 0], "@stylistic/operator-linebreak": ["off", 0], From 76c3bc0a0de5ed55cb3d094b25f998aaced2a68a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 10:46:42 +0200 Subject: [PATCH 09/37] add implementation from old breadcrumbs branch --- lib/breadcrumbs.ts | 164 ++++++++++++++++++++++++++++++++ test/raygun_breadcrumbs_test.js | 50 ++++++++++ 2 files changed, 214 insertions(+) create mode 100644 lib/breadcrumbs.ts create mode 100644 test/raygun_breadcrumbs_test.js diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts new file mode 100644 index 0000000..7519b42 --- /dev/null +++ b/lib/breadcrumbs.ts @@ -0,0 +1,164 @@ +import type { AsyncLocalStorage } from "async_hooks"; +import type { Breadcrumb, InternalBreadcrumb } from "./types"; +import type { Request, Response, NextFunction } from "express"; +import path from "path"; +const debug = require("debug")("raygun").bind(null, "[breadcrumbs]"); + +let asyncLocalStorage: AsyncLocalStorage | null = null; + +try { + asyncLocalStorage = new (require("async_hooks").AsyncLocalStorage)(); + debug("initialized successfully"); +} catch (e) { + debug( + "failed to load async_hooks.AsyncLocalStorage - initialization failed\n", + e + ); +} + +type SourceFile = { + fileName: string; + functionName: string; + lineNumber: number | null; +}; + +function returnCallerSite( + err: Error, + callsites: NodeJS.CallSite[] +): SourceFile | null { + for (const callsite of callsites) { + const fileName = callsite.getFileName() || ""; + + if (fileName.startsWith(__dirname)) { + continue; + } + + return { + fileName: fileName, + functionName: callsite.getFunctionName() || "", + lineNumber: callsite.getLineNumber(), + }; + } + + return null; +} + +function getCallsite(): SourceFile | null { + const originalPrepareStacktrace = Error.prepareStackTrace; + + Error.prepareStackTrace = returnCallerSite; + + const output: any = {}; + + Error.captureStackTrace(output); + + const callsite = output.stack; + Error.prepareStackTrace = originalPrepareStacktrace; + return callsite; +} + +export function addBreadcrumb( + breadcrumb: string | Breadcrumb, + type: InternalBreadcrumb["type"] = "manual" +) { + const crumbs = getBreadcrumbs(); + + if (!crumbs) { + return; + } + + if (typeof breadcrumb === "string") { + const expandedBreadcrumb: Breadcrumb = { + message: breadcrumb, + level: "info", + category: "", + }; + + breadcrumb = expandedBreadcrumb; + } + + const callsite = getCallsite(); + + const internalCrumb: InternalBreadcrumb = { + ...(breadcrumb as Breadcrumb), + category: breadcrumb.category || "", + message: breadcrumb.message || "", + level: breadcrumb.level || "info", + timestamp: Number(new Date()), + type, + className: callsite?.fileName, + methodName: callsite?.functionName, + + // TODO - do we need to do any source mapping? + lineNumber: callsite?.lineNumber || undefined, + }; + + debug("recorded breadcrumb:", internalCrumb); + + crumbs.push(internalCrumb); +} + +export function addRequestBreadcrumb(request: Request) { + const crumbs = getBreadcrumbs(); + + if (!crumbs) { + return; + } + + const internalCrumb: InternalBreadcrumb = { + category: "http", + message: `${request.method} ${request.url}`, + level: "info", + timestamp: Number(new Date()), + type: "request", + }; + + crumbs.push(internalCrumb); +} + +export function getBreadcrumbs(): InternalBreadcrumb[] | null { + if (!asyncLocalStorage) { + return null; + } + + const store = asyncLocalStorage.getStore(); + + if (store) { + return store; + } + + const newStore: InternalBreadcrumb[] = []; + + asyncLocalStorage.enterWith(newStore); + + return newStore; +} + +export function runWithBreadcrumbs(f: () => void) { + if (!asyncLocalStorage) { + f(); + return; + } + debug("running function with breadcrumbs"); + + asyncLocalStorage.run([], f); +} + +const consoleMethods: [keyof typeof console, InternalBreadcrumb["level"]][] = [ + ["debug", "debug"], + ["log", "info"], + ["info", "info"], + ["warn", "warning"], + ["error", "error"], +]; + +for (const [method, level] of consoleMethods) { + const oldMethod = (console as any)[method]; + (console as any)[method] = function logWithBreadcrumb( + this: T, + ...args: any[] + ) { + addBreadcrumb({ message: args.join(" "), level }, "console"); + return oldMethod.apply(this, args); + }; +} diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js new file mode 100644 index 0000000..63e52bf --- /dev/null +++ b/test/raygun_breadcrumbs_test.js @@ -0,0 +1,50 @@ +const express = require("express"); +const test = require("tap").test; +const { listen, makeClientWithMockServer, request } = require("./utils"); + +test("capturing breadcrumbs", async function (t) { + const app = express(); + const testEnv = await makeClientWithMockServer(); + const raygun = testEnv.client; + + app.use(raygun.breadcrumbs); + + function requestHandler(req, res) { + raygun.addBreadcrumb("first!"); + setTimeout(() => { + raygun.addBreadcrumb("second!"); + raygun.send(new Error("test end")); + res.send("done!"); + }, 1); + } + + app.get("/", requestHandler); + + const server = await listen(app); + + await request(`http://localhost:${server.address().port}`); + const message = await testEnv.nextRequest(); + + server.close(); + testEnv.stop(); + + t.deepEquals( + message.details.breadcrumbs.map((b) => b.message), + ["GET /", "first!", "second!"] + ); + + t.deepEquals( + message.details.breadcrumbs.map((b) => b.methodName), + [undefined, requestHandler.name, ""] + ); + + t.deepEquals( + message.details.breadcrumbs.map((b) => b.className), + [undefined, __filename, __filename] + ); + + t.deepEquals( + message.details.breadcrumbs.map((b) => b.lineNumber), + [undefined, 13, 15] + ); +}); From 18790c584acbb71e44a5c891e793cbe7b034a2a8 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 11:23:20 +0200 Subject: [PATCH 10/37] tests passing --- lib/breadcrumbs.ts | 6 ++--- lib/raygun.messageBuilder.ts | 8 ++++++ lib/raygun.ts | 19 +++++++++++++++ lib/types.ts | 17 +++++++++++++ test/raygun_breadcrumbs_test.js | 43 +++++++++++++++++++++++++++------ 5 files changed, 82 insertions(+), 11 deletions(-) diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index 7519b42..01cb05f 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -12,7 +12,7 @@ try { } catch (e) { debug( "failed to load async_hooks.AsyncLocalStorage - initialization failed\n", - e + e, ); } @@ -24,7 +24,7 @@ type SourceFile = { function returnCallerSite( err: Error, - callsites: NodeJS.CallSite[] + callsites: NodeJS.CallSite[], ): SourceFile | null { for (const callsite of callsites) { const fileName = callsite.getFileName() || ""; @@ -59,7 +59,7 @@ function getCallsite(): SourceFile | null { export function addBreadcrumb( breadcrumb: string | Breadcrumb, - type: InternalBreadcrumb["type"] = "manual" + type: InternalBreadcrumb["type"] = "manual", ) { const crumbs = getBreadcrumbs(); diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index 96ebf29..b47c575 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -24,6 +24,7 @@ import { CustomData, Environment, BuiltError, + InternalBreadcrumb, } from "./types"; type UserMessageData = RawUserData | string | undefined; @@ -276,4 +277,11 @@ export class RaygunMessageBuilder { } return data; } + + setBreadcrumbs(breadcrumbs: InternalBreadcrumb[] | null) { + if (breadcrumbs) { + this.message.details.breadcrumbs = [...breadcrumbs]; + } + return this; + } } diff --git a/lib/raygun.ts b/lib/raygun.ts index cf1c55f..b46d5d5 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -9,6 +9,7 @@ "use strict"; import { + Breadcrumb, callVariadicCallback, Callback, CustomData, @@ -24,6 +25,12 @@ import { Tag, Transport, } from "./types"; +import { + addBreadcrumb, + addRequestBreadcrumb, + getBreadcrumbs, + runWithBreadcrumbs, +} from "./breadcrumbs"; import type { IncomingMessage } from "http"; import type { Request, Response, NextFunction } from "express"; import { RaygunBatchTransport } from "./raygun.batch"; @@ -195,6 +202,17 @@ class Raygun { this._tags = tags; } + breadcrumbs(req: Request, res: Response, next: NextFunction) { + runWithBreadcrumbs(() => { + addRequestBreadcrumb(req); + next(); + }); + } + + addBreadcrumb(b: string | Breadcrumb) { + addBreadcrumb(b); + } + transport(): Transport { if (this._batch && this._batchTransport) { return this._batchTransport; @@ -399,6 +417,7 @@ class Raygun { .setUserCustomData(customData) .setUser(this.user(request) || this._user) .setVersion(this._version) + .setBreadcrumbs(getBreadcrumbs()) .setTags(mergedTags); let message = builder.build(); diff --git a/lib/types.ts b/lib/types.ts index 7fc506a..fa20b9f 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -52,6 +52,7 @@ export type MessageDetails = { machineName: string; environment: Environment; correlationId: string | null; + breadcrumbs?: InternalBreadcrumb[]; }; export type Environment = { @@ -194,3 +195,19 @@ export function callVariadicCallback( } export type Callback = CallbackNoError | CallbackWithError; + +export type InternalBreadcrumb = { + timestamp: number; + level: "debug" | "info" | "warning" | "error"; + type: "manual" | "navigation" | "click-event" | "request" | "console"; + category: string; + message: string; + customData?: CustomData; + className?: string; + methodName?: string; + lineNumber?: number; +}; + +export type Breadcrumb = Partial< + Pick +>; diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 63e52bf..ae4368d 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -1,6 +1,31 @@ const express = require("express"); +const deepEqual = require("assert").deepEqual; const test = require("tap").test; const { listen, makeClientWithMockServer, request } = require("./utils"); +const Raygun = require("../lib/raygun"); + +test("add breadcrumbs to payload details", {}, function (t) { + let client = new Raygun.Client().init({ apiKey: "TEST" }); + + client.addBreadcrumb("BREADCRUMB-1"); + client.addBreadcrumb("BREADCRUMB-2"); + + client.onBeforeSend(function (payload) { + t.equal(payload.details.breadcrumbs.length, 2); + t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); + t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); + return payload; + }); + + client + .send(new Error()) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); test("capturing breadcrumbs", async function (t) { const app = express(); @@ -28,23 +53,25 @@ test("capturing breadcrumbs", async function (t) { server.close(); testEnv.stop(); - t.deepEquals( + deepEqual( message.details.breadcrumbs.map((b) => b.message), - ["GET /", "first!", "second!"] + ["GET /", "first!", "second!"], ); - t.deepEquals( + deepEqual( message.details.breadcrumbs.map((b) => b.methodName), - [undefined, requestHandler.name, ""] + [undefined, requestHandler.name, ""], ); - t.deepEquals( + deepEqual( message.details.breadcrumbs.map((b) => b.className), - [undefined, __filename, __filename] + [undefined, __filename, __filename], ); - t.deepEquals( + // line numbers correspond to the calls to `addBreadcrumb` in this test + // update accordingly if they change + t.equal( message.details.breadcrumbs.map((b) => b.lineNumber), - [undefined, 13, 15] + [undefined, 38, 40], ); }); From 276f9c0ab5d1d6021c2b549ee3d0db02fe3685a6 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Thu, 9 May 2024 11:47:35 +0200 Subject: [PATCH 11/37] add todo --- lib/raygun.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/raygun.ts b/lib/raygun.ts index b46d5d5..3e26b85 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -420,6 +420,7 @@ class Raygun { .setBreadcrumbs(getBreadcrumbs()) .setTags(mergedTags); + let message = builder.build(); if (this._groupingKey) { @@ -449,6 +450,8 @@ class Raygun { return { valid: false, message }; } + // TODO: clear breadcrumbs as they have been incorporated into the message + return { valid: true, message, From 346dadc937491c15ce984a1a900c1d675e1eb933 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 10 May 2024 12:43:07 +0200 Subject: [PATCH 12/37] WIP --- examples/express-sample/app.js | 4 ++ examples/express-sample/package-lock.json | 2 + lib/breadcrumbs.ts | 8 ++- lib/raygun.messageBuilder.ts | 3 + lib/raygun.ts | 5 +- test/raygun_breadcrumbs_test.js | 84 ++++++++++++++++++++++- 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 2882b0d..42f4c01 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -58,7 +58,11 @@ app.use(express.static(path.join(__dirname, "public"))); app.use("/", routes); app.use("/users", users); +// Add the Raygun Breadcrumb handler +app.use(raygunClient.breadcrumbs); // Add the Raygun Express handler app.use(raygunClient.expressHandler); +raygunClient.addBreadcrumb("Express Server started!"); + module.exports = app; diff --git a/examples/express-sample/package-lock.json b/examples/express-sample/package-lock.json index 0c063b5..c065f65 100644 --- a/examples/express-sample/package-lock.json +++ b/examples/express-sample/package-lock.json @@ -32,6 +32,8 @@ }, "devDependencies": { "@eslint/js": "^9.2.0", + "@stylistic/eslint-plugin": "^2.0.0", + "@stylistic/eslint-plugin-ts": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index 01cb05f..13e41a9 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -63,6 +63,8 @@ export function addBreadcrumb( ) { const crumbs = getBreadcrumbs(); + debug("using breadcrum store:", crumbs); + if (!crumbs) { return; } @@ -93,15 +95,17 @@ export function addBreadcrumb( lineNumber: callsite?.lineNumber || undefined, }; - debug("recorded breadcrumb:", internalCrumb); + debug("[breadcrumbs.ts] recorded breadcrumb:", internalCrumb); crumbs.push(internalCrumb); } export function addRequestBreadcrumb(request: Request) { + debug(`Add request breadcrumb: ${request}`); const crumbs = getBreadcrumbs(); if (!crumbs) { + debug(`Add request breadcrumb skip, no store!`); return; } @@ -124,11 +128,13 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { const store = asyncLocalStorage.getStore(); if (store) { + debug("found store:", store); return store; } const newStore: InternalBreadcrumb[] = []; + debug("enter with new store"); asyncLocalStorage.enterWith(newStore); return newStore; diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index b47c575..4562d5a 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -27,6 +27,8 @@ import { InternalBreadcrumb, } from "./types"; +const debug = require("debug")("raygun"); + type UserMessageData = RawUserData | string | undefined; const humanString = require("object-to-human-string"); @@ -279,6 +281,7 @@ export class RaygunMessageBuilder { } setBreadcrumbs(breadcrumbs: InternalBreadcrumb[] | null) { + debug(`[raygun.messageBuilder.ts] Added breadcrumbs: ${breadcrumbs?.length || 0}`); if (breadcrumbs) { this.message.details.breadcrumbs = [...breadcrumbs]; } diff --git a/lib/raygun.ts b/lib/raygun.ts index 3e26b85..cbc1cda 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -420,9 +420,10 @@ class Raygun { .setBreadcrumbs(getBreadcrumbs()) .setTags(mergedTags); - let message = builder.build(); + // TODO: Do we need to clear the breadcrumbs after adding them to the message? + if (this._groupingKey) { message.details.groupingKey = typeof this._groupingKey === "function" @@ -450,8 +451,6 @@ class Raygun { return { valid: false, message }; } - // TODO: clear breadcrumbs as they have been incorporated into the message - return { valid: true, message, diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index ae4368d..8348f1d 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -70,8 +70,90 @@ test("capturing breadcrumbs", async function (t) { // line numbers correspond to the calls to `addBreadcrumb` in this test // update accordingly if they change - t.equal( + deepEqual( message.details.breadcrumbs.map((b) => b.lineNumber), [undefined, 38, 40], ); }); + +test("capturing breadcrumbs in different contexts", async function (t) { + const app = express(); + const testEnv = await makeClientWithMockServer(); + const raygun = testEnv.client; + + app.use(raygun.breadcrumbs); + + app.get("/endpoint1", (req, res) => { + raygun.addBreadcrumb("endpoint1: 1"); + setTimeout(() => { + raygun.addBreadcrumb("endpoint1: 2"); + raygun.send(new Error("error1")); + res.send("done!"); + }, 1); + }); + + app.get("/endpoint2", (req, res) => { + raygun.addBreadcrumb("endpoint2: 1"); + setTimeout(() => { + raygun.addBreadcrumb("endpoint2: 2"); + raygun.send(new Error("error2")); + res.send("done!"); + }, 1); + }); + + const server = await listen(app); + + await request(`http://localhost:${server.address().port}/endpoint1`); + const message1 = await testEnv.nextRequest(); + + deepEqual( + message1.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], + ); + + await request(`http://localhost:${server.address().port}/endpoint2`); + const message2 = await testEnv.nextRequest(); + + deepEqual( + message2.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], + ); + + server.close(); + testEnv.stop(); +}); + +// test("expressHandler and breadcrumbs", async function (t) { +// const app = express(); +// +// const testEnvironment = await makeClientWithMockServer(); +// const raygunClient = testEnvironment.client; +// +// app.get("/", (req, res) => { +// throw new Error("surprise error!"); +// }); +// +// // Add breadcrumbs capture to express server +// app.use(raygunClient.breadcrumbs); +// // Add Raydun express handler +// app.use(raygunClient.expressHandler); +// +// // Start test server and request root +// const server = await listen(app); +// await request(`http://localhost:${server.address().port}`); +// const message = await testEnvironment.nextRequest(); +// +// server.close(); +// testEnvironment.stop(); +// +// // Error captured by expressHandler +// t.ok( +// message.details.tags.includes("UnhandledException") +// ); +// +// // Error should include at least one breadcrumb! +// t.equal( +// message.details.breadcrumbs.length, +// 1, +// ); +// }); From 28f2dc9452b69bcbb700a147cb377b030e3d5f88 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Fri, 10 May 2024 15:31:38 +0200 Subject: [PATCH 13/37] cleaning up and adding docs --- lib/breadcrumbs.ts | 9 +++ lib/raygun.ts | 38 ++++++++--- test/raygun_breadcrumbs_test.js | 115 +++++++++++++++++++------------- 3 files changed, 105 insertions(+), 57 deletions(-) diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index 13e41a9..974ef0d 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -150,6 +150,15 @@ export function runWithBreadcrumbs(f: () => void) { asyncLocalStorage.run([], f); } +export function clear() { + if (!asyncLocalStorage) { + return; + } + + debug("clearing stored breadcrumbs, entering with new store"); + asyncLocalStorage.enterWith([]); +} + const consoleMethods: [keyof typeof console, InternalBreadcrumb["level"]][] = [ ["debug", "debug"], ["log", "info"], diff --git a/lib/raygun.ts b/lib/raygun.ts index cbc1cda..2ebb677 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -25,12 +25,7 @@ import { Tag, Transport, } from "./types"; -import { - addBreadcrumb, - addRequestBreadcrumb, - getBreadcrumbs, - runWithBreadcrumbs, -} from "./breadcrumbs"; +import * as breadcrumbs from "./breadcrumbs"; import type { IncomingMessage } from "http"; import type { Request, Response, NextFunction } from "express"; import { RaygunBatchTransport } from "./raygun.batch"; @@ -202,15 +197,34 @@ class Raygun { this._tags = tags; } - breadcrumbs(req: Request, res: Response, next: NextFunction) { - runWithBreadcrumbs(() => { - addRequestBreadcrumb(req); + /** + * Attach as express middleware to create a breadcrumb store scope per request. + * e.g. `app.use(raygun.expressHandlerBreadcrumbs);` + * Then call to `raygun.addBreadcrumb(...)` to add breadcrumbs to the future Raygun `send` call. + * @param req + * @param res + * @param next + */ + expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { + breadcrumbs.runWithBreadcrumbs(() => { + breadcrumbs.addRequestBreadcrumb(req); next(); }); } - addBreadcrumb(b: string | Breadcrumb) { - addBreadcrumb(b); + /** + * Adds breadcrumb to current context + * @param breadcrumb either a string message or a Breadcrumb object + */ + addBreadcrumb(breadcrumb: string | Breadcrumb) { + breadcrumbs.addBreadcrumb(breadcrumb); + } + + /** + * Manually clear stored breadcrumbs for current context + */ + clearBreadcrumbs() { + breadcrumbs.clear(); } transport(): Transport { @@ -373,6 +387,8 @@ class Raygun { body: req.body, }; + // TODO: Maybe we need to run this inside a context to get the right breadcrumbs! + this.send(err, customData || {}, requestParams, [ "UnhandledException", ]).catch((err) => { diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 8348f1d..7dc7820 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -27,6 +27,29 @@ test("add breadcrumbs to payload details", {}, function (t) { }); }); +test("add breadcrumbs to payload details", {}, function (t) { + let client = new Raygun.Client().init({ apiKey: "TEST" }); + + client.onBeforeSend(function (payload) { + t.equal(payload.details.breadcrumbs.length, 2); + t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); + t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); + return payload; + }); + + client.addBreadcrumb("BREADCRUMB-1"); + client.addBreadcrumb("BREADCRUMB-2"); + + client + .send(new Error()) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + test("capturing breadcrumbs", async function (t) { const app = express(); const testEnv = await makeClientWithMockServer(); @@ -76,52 +99,52 @@ test("capturing breadcrumbs", async function (t) { ); }); -test("capturing breadcrumbs in different contexts", async function (t) { - const app = express(); - const testEnv = await makeClientWithMockServer(); - const raygun = testEnv.client; - - app.use(raygun.breadcrumbs); - - app.get("/endpoint1", (req, res) => { - raygun.addBreadcrumb("endpoint1: 1"); - setTimeout(() => { - raygun.addBreadcrumb("endpoint1: 2"); - raygun.send(new Error("error1")); - res.send("done!"); - }, 1); - }); - - app.get("/endpoint2", (req, res) => { - raygun.addBreadcrumb("endpoint2: 1"); - setTimeout(() => { - raygun.addBreadcrumb("endpoint2: 2"); - raygun.send(new Error("error2")); - res.send("done!"); - }, 1); - }); - - const server = await listen(app); - - await request(`http://localhost:${server.address().port}/endpoint1`); - const message1 = await testEnv.nextRequest(); - - deepEqual( - message1.details.breadcrumbs.map((b) => b.message), - ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], - ); - - await request(`http://localhost:${server.address().port}/endpoint2`); - const message2 = await testEnv.nextRequest(); - - deepEqual( - message2.details.breadcrumbs.map((b) => b.message), - ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], - ); - - server.close(); - testEnv.stop(); -}); +// test("capturing breadcrumbs in different contexts", async function (t) { +// const app = express(); +// const testEnv = await makeClientWithMockServer(); +// const raygun = testEnv.client; +// +// app.use(raygun.breadcrumbs); +// +// app.get("/endpoint1", (req, res) => { +// raygun.addBreadcrumb("endpoint1: 1"); +// setTimeout(() => { +// raygun.addBreadcrumb("endpoint1: 2"); +// raygun.send(new Error("error1")); +// res.send("done!"); +// }, 1); +// }); +// +// app.get("/endpoint2", (req, res) => { +// raygun.addBreadcrumb("endpoint2: 1"); +// setTimeout(() => { +// raygun.addBreadcrumb("endpoint2: 2"); +// raygun.send(new Error("error2")); +// res.send("done!"); +// }, 1); +// }); +// +// const server = await listen(app); +// +// await request(`http://localhost:${server.address().port}/endpoint1`); +// const message1 = await testEnv.nextRequest(); +// +// await request(`http://localhost:${server.address().port}/endpoint2`); +// const message2 = await testEnv.nextRequest(); +// +// server.close(); +// testEnv.stop(); +// +// deepEqual( +// message1.details.breadcrumbs.map((b) => b.message), +// ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], +// ); +// +// deepEqual( +// message2.details.breadcrumbs.map((b) => b.message), +// ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], +// ); +// }); // test("expressHandler and breadcrumbs", async function (t) { // const app = express(); From 54207984e61f0323bb102105ab7ec090d1e1d6d8 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 10:00:21 +0200 Subject: [PATCH 14/37] scoping breadcrumbs in error handler --- examples/express-sample/app.js | 7 ++-- lib/breadcrumbs.ts | 46 +++++++++++----------- lib/raygun.ts | 72 +++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 48 deletions(-) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 42f4c01..bbb404a 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -34,6 +34,9 @@ raygunClient.user = function (req) { app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); +// Add the Raygun breadcrumb Express handler +app.use(raygunClient.expressHandlerBreadcrumbs); + // uncomment after placing your favicon in /public // app.use(favicon(__dirname + '/public/favicon.ico')); app.use(logger("dev")); @@ -58,9 +61,7 @@ app.use(express.static(path.join(__dirname, "public"))); app.use("/", routes); app.use("/users", users); -// Add the Raygun Breadcrumb handler -app.use(raygunClient.breadcrumbs); -// Add the Raygun Express handler +// Add the Raygun error Express handler app.use(raygunClient.expressHandler); raygunClient.addBreadcrumb("Express Server started!"); diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index 974ef0d..c28d775 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -100,7 +100,7 @@ export function addBreadcrumb( crumbs.push(internalCrumb); } -export function addRequestBreadcrumb(request: Request) { +export function addRequestBreadcrumb(request: Request, response: Response) { debug(`Add request breadcrumb: ${request}`); const crumbs = getBreadcrumbs(); @@ -118,6 +118,9 @@ export function addRequestBreadcrumb(request: Request) { }; crumbs.push(internalCrumb); + + // Make the current breadcrumb store available to the express error handler + response.locals.breadcrumbs = crumbs; } export function getBreadcrumbs(): InternalBreadcrumb[] | null { @@ -128,7 +131,6 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { const store = asyncLocalStorage.getStore(); if (store) { - debug("found store:", store); return store; } @@ -140,14 +142,14 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { return newStore; } -export function runWithBreadcrumbs(f: () => void) { +export function runWithBreadcrumbs(f: () => void, store: InternalBreadcrumb[] = []) { if (!asyncLocalStorage) { f(); return; } debug("running function with breadcrumbs"); - asyncLocalStorage.run([], f); + asyncLocalStorage.run(store, f); } export function clear() { @@ -159,21 +161,21 @@ export function clear() { asyncLocalStorage.enterWith([]); } -const consoleMethods: [keyof typeof console, InternalBreadcrumb["level"]][] = [ - ["debug", "debug"], - ["log", "info"], - ["info", "info"], - ["warn", "warning"], - ["error", "error"], -]; - -for (const [method, level] of consoleMethods) { - const oldMethod = (console as any)[method]; - (console as any)[method] = function logWithBreadcrumb( - this: T, - ...args: any[] - ) { - addBreadcrumb({ message: args.join(" "), level }, "console"); - return oldMethod.apply(this, args); - }; -} +// const consoleMethods: [keyof typeof console, InternalBreadcrumb["level"]][] = [ +// ["debug", "debug"], +// ["log", "info"], +// ["info", "info"], +// ["warn", "warning"], +// ["error", "error"], +// ]; + +// for (const [method, level] of consoleMethods) { +// const oldMethod = (console as any)[method]; +// (console as any)[method] = function logWithBreadcrumb( +// this: T, +// ...args: any[] +// ) { +// addBreadcrumb({ message: args.join(" "), level }, "console"); +// return oldMethod.apply(this, args); +// }; +// } diff --git a/lib/raygun.ts b/lib/raygun.ts index 2ebb677..c23b09e 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -27,7 +27,7 @@ import { } from "./types"; import * as breadcrumbs from "./breadcrumbs"; import type { IncomingMessage } from "http"; -import type { Request, Response, NextFunction } from "express"; +import {Request, Response, NextFunction, response} from "express"; import { RaygunBatchTransport } from "./raygun.batch"; import { RaygunMessageBuilder } from "./raygun.messageBuilder"; import { OfflineStorage } from "./raygun.offline"; @@ -197,21 +197,6 @@ class Raygun { this._tags = tags; } - /** - * Attach as express middleware to create a breadcrumb store scope per request. - * e.g. `app.use(raygun.expressHandlerBreadcrumbs);` - * Then call to `raygun.addBreadcrumb(...)` to add breadcrumbs to the future Raygun `send` call. - * @param req - * @param res - * @param next - */ - expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { - breadcrumbs.runWithBreadcrumbs(() => { - breadcrumbs.addRequestBreadcrumb(req); - next(); - }); - } - /** * Adds breadcrumb to current context * @param breadcrumb either a string message or a Breadcrumb object @@ -367,6 +352,29 @@ class Raygun { } } + /** + * Attach as express middleware to create a breadcrumb store scope per request. + * e.g. `app.use(raygun.expressHandlerBreadcrumbs);` + * Then call to `raygun.addBreadcrumb(...)` to add breadcrumbs to the future Raygun `send` call. + * @param req + * @param res + * @param next + */ + expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { + breadcrumbs.runWithBreadcrumbs(() => { + breadcrumbs.addRequestBreadcrumb(req, res); + next(); + }); + } + + /** + * Attach as express middleware to report application errors to Raygun automatically. + * e.g. `app.use(raygun.expressHandler);` + * @param err + * @param req + * @param res + * @param next + */ expressHandler(err: Error, req: Request, res: Response, next: NextFunction) { let customData; @@ -387,13 +395,31 @@ class Raygun { body: req.body, }; - // TODO: Maybe we need to run this inside a context to get the right breadcrumbs! + // If a local store of breadcrumbs exist in the response + // run in scoped breadcrumbs store + if (res.locals.breadcrumbs) { + breadcrumbs.runWithBreadcrumbs(() => { + debug("sending express error with scoped breadcrumbs store"); + this.send(err, customData || {}, requestParams, [ + "UnhandledException", + ]).catch((err) => { + console.error("[Raygun] Failed to send Express error", err); + }); + }, res.locals.breadcrumbs); + } else { + debug("sending express error with global breadcrumbs store"); + // Otherwise, run with the global breadcrumbs store + this.send(err, customData || {}, requestParams, [ + "UnhandledException", + ]).then((response) => { + // Clear global breadcrumbs store after successful sent + breadcrumbs.clear(); + }).catch((err) => { + console.error("[Raygun] Failed to send Express error", err); + }); + + } - this.send(err, customData || {}, requestParams, [ - "UnhandledException", - ]).catch((err) => { - console.error("[Raygun] Failed to send Express error", err); - }); next(err); } @@ -433,7 +459,7 @@ class Raygun { .setUserCustomData(customData) .setUser(this.user(request) || this._user) .setVersion(this._version) - .setBreadcrumbs(getBreadcrumbs()) + .setBreadcrumbs(breadcrumbs.getBreadcrumbs()) .setTags(mergedTags); let message = builder.build(); From 34751e059b055f1ec6f6994b7f677c1f4cd1cb82 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 10:27:45 +0200 Subject: [PATCH 15/37] improving tests --- lib/raygun.ts | 4 +- test/raygun_breadcrumbs_test.js | 247 ++++++++++++++++---------------- 2 files changed, 121 insertions(+), 130 deletions(-) diff --git a/lib/raygun.ts b/lib/raygun.ts index c23b09e..d9846a5 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -397,7 +397,7 @@ class Raygun { // If a local store of breadcrumbs exist in the response // run in scoped breadcrumbs store - if (res.locals.breadcrumbs) { + if (res.locals && res.locals.breadcrumbs) { breadcrumbs.runWithBreadcrumbs(() => { debug("sending express error with scoped breadcrumbs store"); this.send(err, customData || {}, requestParams, [ @@ -464,8 +464,6 @@ class Raygun { let message = builder.build(); - // TODO: Do we need to clear the breadcrumbs after adding them to the message? - if (this._groupingKey) { message.details.groupingKey = typeof this._groupingKey === "function" diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 7dc7820..d273cb2 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -4,58 +4,36 @@ const test = require("tap").test; const { listen, makeClientWithMockServer, request } = require("./utils"); const Raygun = require("../lib/raygun"); -test("add breadcrumbs to payload details", {}, function (t) { - let client = new Raygun.Client().init({ apiKey: "TEST" }); - - client.addBreadcrumb("BREADCRUMB-1"); - client.addBreadcrumb("BREADCRUMB-2"); - - client.onBeforeSend(function (payload) { - t.equal(payload.details.breadcrumbs.length, 2); - t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); - t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); - return payload; - }); - - client - .send(new Error()) - .then((message) => { - t.end(); - }) - .catch((err) => { - t.fail(err); - }); -}); - -test("add breadcrumbs to payload details", {}, function (t) { - let client = new Raygun.Client().init({ apiKey: "TEST" }); - - client.onBeforeSend(function (payload) { - t.equal(payload.details.breadcrumbs.length, 2); - t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); - t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); - return payload; - }); - - client.addBreadcrumb("BREADCRUMB-1"); - client.addBreadcrumb("BREADCRUMB-2"); - - client - .send(new Error()) - .then((message) => { - t.end(); - }) - .catch((err) => { - t.fail(err); - }); -}); +// test("add breadcrumbs to payload details", {}, function (t) { +// let client = new Raygun.Client().init({ apiKey: "TEST" }); +// +// client.addBreadcrumb("BREADCRUMB-1"); +// client.addBreadcrumb("BREADCRUMB-2"); +// +// client.onBeforeSend(function (payload) { +// t.equal(payload.details.breadcrumbs.length, 2); +// t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); +// t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); +// return payload; +// }); +// +// client +// .send(new Error()) +// .then((message) => { +// t.end(); +// }) +// .catch((err) => { +// t.fail(err); +// }); +// }); test("capturing breadcrumbs", async function (t) { const app = express(); const testEnv = await makeClientWithMockServer(); const raygun = testEnv.client; - app.use(raygun.breadcrumbs); + // Must be defined before any endpoint is configured + app.use(raygun.expressHandlerBreadcrumbs); function requestHandler(req, res) { raygun.addBreadcrumb("first!"); @@ -95,88 +73,103 @@ test("capturing breadcrumbs", async function (t) { // update accordingly if they change deepEqual( message.details.breadcrumbs.map((b) => b.lineNumber), - [undefined, 38, 40], + [undefined, 39, 41], ); + + t.end(); }); -// test("capturing breadcrumbs in different contexts", async function (t) { -// const app = express(); -// const testEnv = await makeClientWithMockServer(); -// const raygun = testEnv.client; -// -// app.use(raygun.breadcrumbs); -// -// app.get("/endpoint1", (req, res) => { -// raygun.addBreadcrumb("endpoint1: 1"); -// setTimeout(() => { -// raygun.addBreadcrumb("endpoint1: 2"); -// raygun.send(new Error("error1")); -// res.send("done!"); -// }, 1); -// }); -// -// app.get("/endpoint2", (req, res) => { -// raygun.addBreadcrumb("endpoint2: 1"); -// setTimeout(() => { -// raygun.addBreadcrumb("endpoint2: 2"); -// raygun.send(new Error("error2")); -// res.send("done!"); -// }, 1); -// }); -// -// const server = await listen(app); -// -// await request(`http://localhost:${server.address().port}/endpoint1`); -// const message1 = await testEnv.nextRequest(); -// -// await request(`http://localhost:${server.address().port}/endpoint2`); -// const message2 = await testEnv.nextRequest(); -// -// server.close(); -// testEnv.stop(); -// -// deepEqual( -// message1.details.breadcrumbs.map((b) => b.message), -// ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], -// ); -// -// deepEqual( -// message2.details.breadcrumbs.map((b) => b.message), -// ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], -// ); -// }); +test("capturing breadcrumbs in different contexts", async function (t) { + const app = express(); + const testEnv = await makeClientWithMockServer(); + const raygun = testEnv.client; -// test("expressHandler and breadcrumbs", async function (t) { -// const app = express(); -// -// const testEnvironment = await makeClientWithMockServer(); -// const raygunClient = testEnvironment.client; -// -// app.get("/", (req, res) => { -// throw new Error("surprise error!"); -// }); -// -// // Add breadcrumbs capture to express server -// app.use(raygunClient.breadcrumbs); -// // Add Raydun express handler -// app.use(raygunClient.expressHandler); -// -// // Start test server and request root -// const server = await listen(app); -// await request(`http://localhost:${server.address().port}`); -// const message = await testEnvironment.nextRequest(); -// -// server.close(); -// testEnvironment.stop(); -// -// // Error captured by expressHandler -// t.ok( -// message.details.tags.includes("UnhandledException") -// ); -// -// // Error should include at least one breadcrumb! -// t.equal( -// message.details.breadcrumbs.length, -// 1, -// ); -// }); + // Must be defined before any endpoint is configured + app.use(raygun.expressHandlerBreadcrumbs); + + app.get("/endpoint1", (req, res) => { + raygun.addBreadcrumb("endpoint1: 1"); + setTimeout(() => { + raygun.addBreadcrumb("endpoint1: 2"); + raygun.send(new Error("error1")); + res.send("done!"); + }, 1); + }); + + app.get("/endpoint2", (req, res) => { + raygun.addBreadcrumb("endpoint2: 1"); + setTimeout(() => { + raygun.addBreadcrumb("endpoint2: 2"); + raygun.send(new Error("error2")); + res.send("done!"); + }, 1); + }); + + const server = await listen(app); + + await request(`http://localhost:${server.address().port}/endpoint1`); + const message1 = await testEnv.nextRequest(); + + await request(`http://localhost:${server.address().port}/endpoint2`); + const message2 = await testEnv.nextRequest(); + + server.close(); + testEnv.stop(); + + deepEqual( + message1.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], + ); + + deepEqual( + message2.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], + ); + + t.end(); +}); + +test("expressHandler and breadcrumbs", async function (t) { + const app = express(); + + const testEnvironment = await makeClientWithMockServer(); + const raygunClient = testEnvironment.client; + + // Must be defined before any endpoint is configured + app.use(raygunClient.expressHandlerBreadcrumbs); + + // This breadcrumb is in the global app scope, so it won't be included + raygunClient.addBreadcrumb("breadcrumb in global scope"); + + // Define root endpoint which throws an error + app.get("/", (req, res) => { + + // Add an extra breadcrumb before throwing + raygunClient.addBreadcrumb("breadcrumb-1"); + + // Throw error, should be captured by the expressHandler + throw new Error("surprise error!"); + }); + + // Add Raygun error express handler + app.use(raygunClient.expressHandler); + + // Start test server and request root + const server = await listen(app); + await request(`http://localhost:${server.address().port}`); + const message = await testEnvironment.nextRequest(); + + server.close(); + testEnvironment.stop(); + + // Error captured by expressHandler + t.ok( + message.details.tags.includes("UnhandledException") + ); + + // Error should include breadcrumbs from the scoped store + t.equal(message.details.breadcrumbs.length, 2); + t.equal(message.details.breadcrumbs[0].message, "GET /"); + t.equal(message.details.breadcrumbs[1].message, "breadcrumb-1"); + t.end(); +}); From bdfe7c8d677daaa32579a14a0144ff14245c1714 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 10:42:37 +0200 Subject: [PATCH 16/37] cleanup tests --- test/raygun_breadcrumbs_test.js | 66 +++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index d273cb2..816b632 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -2,32 +2,30 @@ const express = require("express"); const deepEqual = require("assert").deepEqual; const test = require("tap").test; const { listen, makeClientWithMockServer, request } = require("./utils"); -const Raygun = require("../lib/raygun"); - -// test("add breadcrumbs to payload details", {}, function (t) { -// let client = new Raygun.Client().init({ apiKey: "TEST" }); -// -// client.addBreadcrumb("BREADCRUMB-1"); -// client.addBreadcrumb("BREADCRUMB-2"); -// -// client.onBeforeSend(function (payload) { -// t.equal(payload.details.breadcrumbs.length, 2); -// t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); -// t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); -// return payload; -// }); -// -// client -// .send(new Error()) -// .then((message) => { -// t.end(); -// }) -// .catch((err) => { -// t.fail(err); -// }); -// }); - -test("capturing breadcrumbs", async function (t) { + +test("add breadcrumbs to payload details", {}, async function (t) { + const testEnv = await makeClientWithMockServer(); + const client = testEnv.client; + + // Add breadcrumbs in global scope + client.addBreadcrumb("BREADCRUMB-1"); + client.addBreadcrumb("BREADCRUMB-2"); + + client.onBeforeSend(function (payload) { + // Raygun payload should include breadcrumbs + t.equal(payload.details.breadcrumbs.length, 2); + t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB-1"); + t.equal(payload.details.breadcrumbs[1].message, "BREADCRUMB-2"); + return payload; + }); + + // Send Raygun error + await client.send(new Error()); + + testEnv.stop(); +}); + +test("capturing breadcrumbs in single scope", async function (t) { const app = express(); const testEnv = await makeClientWithMockServer(); const raygun = testEnv.client; @@ -36,9 +34,12 @@ test("capturing breadcrumbs", async function (t) { app.use(raygun.expressHandlerBreadcrumbs); function requestHandler(req, res) { + // Breadcrumbs are added in the scope of the request raygun.addBreadcrumb("first!"); setTimeout(() => { raygun.addBreadcrumb("second!"); + + // Send custom Raygun error with scoped breadcrumbs raygun.send(new Error("test end")); res.send("done!"); }, 1); @@ -54,16 +55,19 @@ test("capturing breadcrumbs", async function (t) { server.close(); testEnv.stop(); + // Error should include breadcrumbs from scope deepEqual( message.details.breadcrumbs.map((b) => b.message), ["GET /", "first!", "second!"], ); + // Breadcrumbs include method names deepEqual( message.details.breadcrumbs.map((b) => b.methodName), [undefined, requestHandler.name, ""], ); + // Breadcrumbs include class names deepEqual( message.details.breadcrumbs.map((b) => b.className), [undefined, __filename, __filename], @@ -73,7 +77,7 @@ test("capturing breadcrumbs", async function (t) { // update accordingly if they change deepEqual( message.details.breadcrumbs.map((b) => b.lineNumber), - [undefined, 39, 41], + [undefined, 38, 40], ); t.end(); @@ -88,18 +92,24 @@ test("capturing breadcrumbs in different contexts", async function (t) { app.use(raygun.expressHandlerBreadcrumbs); app.get("/endpoint1", (req, res) => { + // Breadcrumbs are added in the scope of the request for endpoint1 raygun.addBreadcrumb("endpoint1: 1"); setTimeout(() => { raygun.addBreadcrumb("endpoint1: 2"); + + // Send custom Raygun error with scoped breadcrumbs raygun.send(new Error("error1")); res.send("done!"); }, 1); }); app.get("/endpoint2", (req, res) => { + // Breadcrumbs are added in the scope of the request for endpoint2 raygun.addBreadcrumb("endpoint2: 1"); setTimeout(() => { raygun.addBreadcrumb("endpoint2: 2"); + + // Send custom Raygun error with scoped breadcrumbs raygun.send(new Error("error2")); res.send("done!"); }, 1); @@ -116,11 +126,13 @@ test("capturing breadcrumbs in different contexts", async function (t) { server.close(); testEnv.stop(); + // First error should include breadcrumbs from scope deepEqual( message1.details.breadcrumbs.map((b) => b.message), ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], ); + // Second error should include breadcrumbs from scope deepEqual( message2.details.breadcrumbs.map((b) => b.message), ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], From 06127fe41c5f1ac34fceefc2a3515d992b805c1f Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 10:48:33 +0200 Subject: [PATCH 17/37] fix eslint --- eslint.config.mjs | 1 + lib/breadcrumbs.ts | 7 ++++--- lib/raygun.ts | 3 +-- test/raygun_breadcrumbs_test.js | 11 +++++------ 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index aa0feab..1fa70b5 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,6 +19,7 @@ export default tseslint.config( // Add node globals to ignore undefined globals: { "__dirname": false, + "__filename": false, "console": false, "module": false, "process": false, diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index c28d775..e23c6e8 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -1,7 +1,6 @@ import type { AsyncLocalStorage } from "async_hooks"; import type { Breadcrumb, InternalBreadcrumb } from "./types"; -import type { Request, Response, NextFunction } from "express"; -import path from "path"; +import type { Request, Response } from "express"; const debug = require("debug")("raygun").bind(null, "[breadcrumbs]"); let asyncLocalStorage: AsyncLocalStorage | null = null; @@ -48,6 +47,8 @@ function getCallsite(): SourceFile | null { Error.prepareStackTrace = returnCallerSite; + // Ignore use of any, required for captureStackTrace + // eslint-disable-next-line @typescript-eslint/no-explicit-any const output: any = {}; Error.captureStackTrace(output); @@ -105,7 +106,7 @@ export function addRequestBreadcrumb(request: Request, response: Response) { const crumbs = getBreadcrumbs(); if (!crumbs) { - debug(`Add request breadcrumb skip, no store!`); + debug("Add request breadcrumb skip, no store!"); return; } diff --git a/lib/raygun.ts b/lib/raygun.ts index d9846a5..83e51d1 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -27,7 +27,7 @@ import { } from "./types"; import * as breadcrumbs from "./breadcrumbs"; import type { IncomingMessage } from "http"; -import {Request, Response, NextFunction, response} from "express"; +import { Request, Response, NextFunction } from "express"; import { RaygunBatchTransport } from "./raygun.batch"; import { RaygunMessageBuilder } from "./raygun.messageBuilder"; import { OfflineStorage } from "./raygun.offline"; @@ -417,7 +417,6 @@ class Raygun { }).catch((err) => { console.error("[Raygun] Failed to send Express error", err); }); - } next(err); diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 816b632..a10b297 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -128,14 +128,14 @@ test("capturing breadcrumbs in different contexts", async function (t) { // First error should include breadcrumbs from scope deepEqual( - message1.details.breadcrumbs.map((b) => b.message), - ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], + message1.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint1", "endpoint1: 1", "endpoint1: 2"], ); // Second error should include breadcrumbs from scope deepEqual( - message2.details.breadcrumbs.map((b) => b.message), - ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], + message2.details.breadcrumbs.map((b) => b.message), + ["GET /endpoint2", "endpoint2: 1", "endpoint2: 2"], ); t.end(); @@ -155,7 +155,6 @@ test("expressHandler and breadcrumbs", async function (t) { // Define root endpoint which throws an error app.get("/", (req, res) => { - // Add an extra breadcrumb before throwing raygunClient.addBreadcrumb("breadcrumb-1"); @@ -176,7 +175,7 @@ test("expressHandler and breadcrumbs", async function (t) { // Error captured by expressHandler t.ok( - message.details.tags.includes("UnhandledException") + message.details.tags.includes("UnhandledException"), ); // Error should include breadcrumbs from the scoped store From ffe86baf315ef0e7c1158437dfd916fd4e0389e9 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 10:48:44 +0200 Subject: [PATCH 18/37] prettier --- lib/breadcrumbs.ts | 5 ++++- lib/raygun.messageBuilder.ts | 4 +++- lib/raygun.ts | 16 ++++++++-------- test/raygun_breadcrumbs_test.js | 4 +--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index e23c6e8..122efeb 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -143,7 +143,10 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { return newStore; } -export function runWithBreadcrumbs(f: () => void, store: InternalBreadcrumb[] = []) { +export function runWithBreadcrumbs( + f: () => void, + store: InternalBreadcrumb[] = [], +) { if (!asyncLocalStorage) { f(); return; diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index 4562d5a..81de028 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -281,7 +281,9 @@ export class RaygunMessageBuilder { } setBreadcrumbs(breadcrumbs: InternalBreadcrumb[] | null) { - debug(`[raygun.messageBuilder.ts] Added breadcrumbs: ${breadcrumbs?.length || 0}`); + debug( + `[raygun.messageBuilder.ts] Added breadcrumbs: ${breadcrumbs?.length || 0}`, + ); if (breadcrumbs) { this.message.details.breadcrumbs = [...breadcrumbs]; } diff --git a/lib/raygun.ts b/lib/raygun.ts index 83e51d1..cf78bb0 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -409,14 +409,14 @@ class Raygun { } else { debug("sending express error with global breadcrumbs store"); // Otherwise, run with the global breadcrumbs store - this.send(err, customData || {}, requestParams, [ - "UnhandledException", - ]).then((response) => { - // Clear global breadcrumbs store after successful sent - breadcrumbs.clear(); - }).catch((err) => { - console.error("[Raygun] Failed to send Express error", err); - }); + this.send(err, customData || {}, requestParams, ["UnhandledException"]) + .then((response) => { + // Clear global breadcrumbs store after successful sent + breadcrumbs.clear(); + }) + .catch((err) => { + console.error("[Raygun] Failed to send Express error", err); + }); } next(err); diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index a10b297..dfd52c2 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -174,9 +174,7 @@ test("expressHandler and breadcrumbs", async function (t) { testEnvironment.stop(); // Error captured by expressHandler - t.ok( - message.details.tags.includes("UnhandledException"), - ); + t.ok(message.details.tags.includes("UnhandledException")); // Error should include breadcrumbs from the scoped store t.equal(message.details.breadcrumbs.length, 2); From 15f4120d313556ee504f1a9bb9a3cb87b5914656 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Mon, 13 May 2024 11:29:09 +0200 Subject: [PATCH 19/37] moved method --- lib/breadcrumbs.ts | 3 --- lib/raygun.ts | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index 122efeb..efc48fd 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -119,9 +119,6 @@ export function addRequestBreadcrumb(request: Request, response: Response) { }; crumbs.push(internalCrumb); - - // Make the current breadcrumb store available to the express error handler - response.locals.breadcrumbs = crumbs; } export function getBreadcrumbs(): InternalBreadcrumb[] | null { diff --git a/lib/raygun.ts b/lib/raygun.ts index cf78bb0..90dfc6a 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -363,6 +363,8 @@ class Raygun { expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { breadcrumbs.runWithBreadcrumbs(() => { breadcrumbs.addRequestBreadcrumb(req, res); + // Make the current breadcrumb store available to the express error handler + res.locals.breadcrumbs = breadcrumbs.getBreadcrumbs(); next(); }); } From 62b342dcfedd5f871e8483286c592464c58ee078 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 07:41:51 +0200 Subject: [PATCH 20/37] add Breadcrumb object test --- test/raygun_breadcrumbs_test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index dfd52c2..2c7acf0 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -182,3 +182,34 @@ test("expressHandler and breadcrumbs", async function (t) { t.equal(message.details.breadcrumbs[1].message, "breadcrumb-1"); t.end(); }); + +test("custom breadcrumb objects", {}, async function (t) { + const testEnv = await makeClientWithMockServer(); + const client = testEnv.client; + + // Breadcrumb object + client.addBreadcrumb({ + level: "info", + category: "CATEGORY", + message: "MESSAGE", + customData: { + "custom": "data", + } + }); + + client.onBeforeSend(function (payload) { + // Raygun payload should include breadcrumbs + t.equal(payload.details.breadcrumbs.length, 1); + t.equal(payload.details.breadcrumbs[0].message, "MESSAGE"); + t.equal(payload.details.breadcrumbs[0].category, "CATEGORY"); + t.equal(payload.details.breadcrumbs[0].level, "info"); + t.equal(payload.details.breadcrumbs[0].customData["custom"], "data"); + return payload; + }); + + // Send Raygun error + await client.send(new Error()); + + testEnv.stop(); +}); + From 03a6ec1f216d105db5246a02ffa7b9bfeb9997f0 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 07:42:00 +0200 Subject: [PATCH 21/37] add Breadcrumb object test --- test/raygun_breadcrumbs_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 2c7acf0..e5a8124 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -199,7 +199,7 @@ test("custom breadcrumb objects", {}, async function (t) { client.onBeforeSend(function (payload) { // Raygun payload should include breadcrumbs - t.equal(payload.details.breadcrumbs.length, 1); + t.equal(payload.details.breadcrumbs.length, 69); t.equal(payload.details.breadcrumbs[0].message, "MESSAGE"); t.equal(payload.details.breadcrumbs[0].category, "CATEGORY"); t.equal(payload.details.breadcrumbs[0].level, "info"); From 5fee12ae9e18a73bf84d22e32fdf3d6e965e05a7 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 07:42:23 +0200 Subject: [PATCH 22/37] fix test --- test/raygun_breadcrumbs_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index e5a8124..2c7acf0 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -199,7 +199,7 @@ test("custom breadcrumb objects", {}, async function (t) { client.onBeforeSend(function (payload) { // Raygun payload should include breadcrumbs - t.equal(payload.details.breadcrumbs.length, 69); + t.equal(payload.details.breadcrumbs.length, 1); t.equal(payload.details.breadcrumbs[0].message, "MESSAGE"); t.equal(payload.details.breadcrumbs[0].category, "CATEGORY"); t.equal(payload.details.breadcrumbs[0].level, "info"); From fb7c68f9f3c01c24b7bd560516589f926f87be97 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 08:35:03 +0200 Subject: [PATCH 23/37] expanded the express-example --- examples/express-sample/README.md | 11 ++++--- examples/express-sample/app.js | 14 +-------- examples/express-sample/logger.js | 16 ++++++++++ examples/express-sample/routes/index.js | 40 +++++++++++++++++++++++-- examples/express-sample/views/index.ejs | 3 ++ examples/express-sample/views/send.ejs | 18 +++++++++++ 6 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 examples/express-sample/logger.js create mode 100644 examples/express-sample/views/send.ejs diff --git a/examples/express-sample/README.md b/examples/express-sample/README.md index 7774d7d..2c90fed 100644 --- a/examples/express-sample/README.md +++ b/examples/express-sample/README.md @@ -24,9 +24,12 @@ in the subdirectory where you found this README.md file. ## Interesting files to look +- `logger.js` + - Setup of Raygun (lines 9-14) - `app.js` - - Setup of Raygun (lines 9-12) - - Sets the user (lines 27-29) - - Attaches Raygun to Express (line 60) + - Sets the user (lines 17-19) + - Attaches Raygun Breadcrumb middleware to Express (line 26) + - Attaches Raygun to Express (line 53) - `routes/index.js` - - Tries to use a fake object, which bounces up to the Express handler (lines 11-15) + - `/send` endpoint: Sends a custom error to Raygun (lines 11-34) + - `/error` endpoint: Tries to use a fake object, which bounces up to the Express handler (lines 36-49) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index bbb404a..4e47425 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -1,17 +1,4 @@ -var config = require("config"); -if (config.Raygun.Key === "YOUR_API_KEY") { - console.error( - "[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file", - ); - process.exit(1); -} - -// Setup Raygun -var raygun = require("raygun"); -var raygunClient = new raygun.Client().init({ - apiKey: config.Raygun.Key, -}); var express = require("express"); var path = require("path"); @@ -19,6 +6,7 @@ var logger = require("morgan"); var cookieParser = require("cookie-parser"); var bodyParser = require("body-parser"); var sassMiddleware = require("node-sass-middleware"); +var raygunClient = require("./logger"); var routes = require("./routes/index"); var users = require("./routes/users"); diff --git a/examples/express-sample/logger.js b/examples/express-sample/logger.js new file mode 100644 index 0000000..3ce73c4 --- /dev/null +++ b/examples/express-sample/logger.js @@ -0,0 +1,16 @@ +var config = require("config"); + +if (config.Raygun.Key === "YOUR_API_KEY") { + console.error( + "[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file", + ); + process.exit(1); +} + +// Setup Raygun +var raygun = require("raygun"); +var raygunClient = new raygun.Client().init({ + apiKey: config.Raygun.Key, +}); + +module.exports = raygunClient; diff --git a/examples/express-sample/routes/index.js b/examples/express-sample/routes/index.js index 523d931..f7c0c39 100644 --- a/examples/express-sample/routes/index.js +++ b/examples/express-sample/routes/index.js @@ -1,14 +1,48 @@ -var express = require("express"); -var router = express.Router(); +const express = require("express"); +const router = express.Router(); +const raygunClient = require("../logger"); /* GET home page. */ router.get("/", function (req, res, next) { res.render("index", { - title: "Express", + title: "Raygun Express Example", }); }); +router.get("/send", function (req, res, next) { + + raygunClient.addBreadcrumb({ + level: "debug", + category: "Example", + message: "Breadcrumb in /send endpoint", + customData: { + "custom-data": "data", + } + }); + + raygunClient.send("Error in /send endpoint").then((message) => { + res.render("send", { + title: "Sent custom error to Raygun", + body: `Raygun status code: ${message.statusCode}`, + }); + }).catch((error) => { + res.render("send", { + title: "Failed to send custom error to Raygun", + body: error.toString(), + }); + }) +}); + router.get("/error", function (req, res, next) { + raygunClient.addBreadcrumb({ + level: "debug", + category: "Example", + message: "Breadcrumb in /error endpoint", + customData: { + "custom-data": "data", + } + }); + // Call an object that doesn't exist to send an error to Raygun fakeObject.FakeMethod(); res.send(500); diff --git a/examples/express-sample/views/index.ejs b/examples/express-sample/views/index.ejs index a5ef3e5..9f3d474 100644 --- a/examples/express-sample/views/index.ejs +++ b/examples/express-sample/views/index.ejs @@ -8,6 +8,9 @@

<%= title %>

Welcome to <%= title %>

+ diff --git a/examples/express-sample/views/send.ejs b/examples/express-sample/views/send.ejs new file mode 100644 index 0000000..09be1db --- /dev/null +++ b/examples/express-sample/views/send.ejs @@ -0,0 +1,18 @@ + + + + <%= title %> + + + +

<%= title %>

+

<%= title %>

+
+ <%= body %> +
+ + + + From 702180332731aa3890021e6ce0c39c71b866324a Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 08:39:26 +0200 Subject: [PATCH 24/37] prettier --- examples/express-sample/app.js | 2 -- examples/express-sample/logger.js | 10 +++++----- test/raygun_breadcrumbs_test.js | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 4e47425..5ad257a 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -1,5 +1,3 @@ - - var express = require("express"); var path = require("path"); var logger = require("morgan"); diff --git a/examples/express-sample/logger.js b/examples/express-sample/logger.js index 3ce73c4..77314f8 100644 --- a/examples/express-sample/logger.js +++ b/examples/express-sample/logger.js @@ -1,16 +1,16 @@ var config = require("config"); if (config.Raygun.Key === "YOUR_API_KEY") { - console.error( - "[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file", - ); - process.exit(1); + console.error( + "[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file", + ); + process.exit(1); } // Setup Raygun var raygun = require("raygun"); var raygunClient = new raygun.Client().init({ - apiKey: config.Raygun.Key, + apiKey: config.Raygun.Key, }); module.exports = raygunClient; diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 2c7acf0..1a67ae8 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -193,8 +193,8 @@ test("custom breadcrumb objects", {}, async function (t) { category: "CATEGORY", message: "MESSAGE", customData: { - "custom": "data", - } + custom: "data", + }, }); client.onBeforeSend(function (payload) { @@ -212,4 +212,3 @@ test("custom breadcrumb objects", {}, async function (t) { testEnv.stop(); }); - From a92b5865ca629e9283f61ff99116d051c420d4cf Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 08:44:15 +0200 Subject: [PATCH 25/37] prettier --- examples/express-sample/routes/index.js | 28 +++++++++++++------------ 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/express-sample/routes/index.js b/examples/express-sample/routes/index.js index f7c0c39..81c9e43 100644 --- a/examples/express-sample/routes/index.js +++ b/examples/express-sample/routes/index.js @@ -10,27 +10,29 @@ router.get("/", function (req, res, next) { }); router.get("/send", function (req, res, next) { - raygunClient.addBreadcrumb({ level: "debug", category: "Example", message: "Breadcrumb in /send endpoint", customData: { "custom-data": "data", - } + }, }); - raygunClient.send("Error in /send endpoint").then((message) => { - res.render("send", { - title: "Sent custom error to Raygun", - body: `Raygun status code: ${message.statusCode}`, - }); - }).catch((error) => { - res.render("send", { - title: "Failed to send custom error to Raygun", - body: error.toString(), + raygunClient + .send("Error in /send endpoint") + .then((message) => { + res.render("send", { + title: "Sent custom error to Raygun", + body: `Raygun status code: ${message.statusCode}`, + }); + }) + .catch((error) => { + res.render("send", { + title: "Failed to send custom error to Raygun", + body: error.toString(), + }); }); - }) }); router.get("/error", function (req, res, next) { @@ -40,7 +42,7 @@ router.get("/error", function (req, res, next) { message: "Breadcrumb in /error endpoint", customData: { "custom-data": "data", - } + }, }); // Call an object that doesn't exist to send an error to Raygun From 73ae88dbd7436221340b20ec15645d8eb814a3fc Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 09:05:33 +0200 Subject: [PATCH 26/37] add breadcrumbs to domains app --- examples/using-domains/app.js | 2 ++ examples/using-domains/package-lock.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/examples/using-domains/app.js b/examples/using-domains/app.js index c78e2f5..e92a887 100644 --- a/examples/using-domains/app.js +++ b/examples/using-domains/app.js @@ -17,6 +17,7 @@ var appDomain = require("domain").create(); // Add the error handler so we can pass errors to Raygun when the domain // crashes appDomain.on("error", function (err) { + raygunClient.addBreadcrumb("Domain error caught!"); console.log(`[Raygun4Node-Domains-Sample] Domain error caught: ${err}`); // Try send data to Raygun raygunClient @@ -40,6 +41,7 @@ appDomain.on("error", function (err) { appDomain.run(function () { var fs = require("fs"); + raygunClient.addBreadcrumb("Running example app"); console.log("[Raygun4Node-Domains-Sample] Running example app"); // Try and read a file that doesn't exist diff --git a/examples/using-domains/package-lock.json b/examples/using-domains/package-lock.json index de945f5..9fdac34 100644 --- a/examples/using-domains/package-lock.json +++ b/examples/using-domains/package-lock.json @@ -25,6 +25,8 @@ }, "devDependencies": { "@eslint/js": "^9.2.0", + "@stylistic/eslint-plugin": "^2.0.0", + "@stylistic/eslint-plugin-ts": "^2.0.0", "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", From fe75b49c75d69aa865f5cc8ec73f3985f6eb5254 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 13:13:04 +0200 Subject: [PATCH 27/37] cleanup and test for clear breadcrumbs --- lib/breadcrumbs.ts | 11 +++++------ lib/raygun.ts | 2 +- test/raygun_breadcrumbs_test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/lib/breadcrumbs.ts b/lib/breadcrumbs.ts index efc48fd..d055058 100644 --- a/lib/breadcrumbs.ts +++ b/lib/breadcrumbs.ts @@ -64,8 +64,6 @@ export function addBreadcrumb( ) { const crumbs = getBreadcrumbs(); - debug("using breadcrum store:", crumbs); - if (!crumbs) { return; } @@ -96,13 +94,12 @@ export function addBreadcrumb( lineNumber: callsite?.lineNumber || undefined, }; - debug("[breadcrumbs.ts] recorded breadcrumb:", internalCrumb); + debug(`recorded breadcrumb: ${internalCrumb}`); crumbs.push(internalCrumb); } -export function addRequestBreadcrumb(request: Request, response: Response) { - debug(`Add request breadcrumb: ${request}`); +export function addRequestBreadcrumb(request: Request) { const crumbs = getBreadcrumbs(); if (!crumbs) { @@ -118,6 +115,8 @@ export function addRequestBreadcrumb(request: Request, response: Response) { type: "request", }; + debug(`recorded request breadcrumb: ${internalCrumb}`); + crumbs.push(internalCrumb); } @@ -148,8 +147,8 @@ export function runWithBreadcrumbs( f(); return; } - debug("running function with breadcrumbs"); + debug("running function with breadcrumbs"); asyncLocalStorage.run(store, f); } diff --git a/lib/raygun.ts b/lib/raygun.ts index 90dfc6a..1518d24 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -362,7 +362,7 @@ class Raygun { */ expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { breadcrumbs.runWithBreadcrumbs(() => { - breadcrumbs.addRequestBreadcrumb(req, res); + breadcrumbs.addRequestBreadcrumb(req); // Make the current breadcrumb store available to the express error handler res.locals.breadcrumbs = breadcrumbs.getBreadcrumbs(); next(); diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js index 1a67ae8..96bf247 100644 --- a/test/raygun_breadcrumbs_test.js +++ b/test/raygun_breadcrumbs_test.js @@ -212,3 +212,29 @@ test("custom breadcrumb objects", {}, async function (t) { testEnv.stop(); }); + +test("clear breadcrumbs", {}, async function (t) { + const testEnv = await makeClientWithMockServer(); + const client = testEnv.client; + + // Add one Breadcrumb + client.addBreadcrumb("SHOULD BE CLEARED"); + + // Clear breadcrumbs + client.clearBreadcrumbs(); + + // Add one Breadcrumb + client.addBreadcrumb("BREADCRUMB"); + + client.onBeforeSend(function (payload) { + // Only BREADCRUMB should exist + t.equal(payload.details.breadcrumbs.length, 1); + t.equal(payload.details.breadcrumbs[0].message, "BREADCRUMB"); + return payload; + }); + + // Send Raygun error + await client.send(new Error()); + + testEnv.stop(); +}); From b2def16ca70c1f5e68b8547a7eaeee122de37e97 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:18:39 +0200 Subject: [PATCH 28/37] prettier --- README.md | 4 +++ lib/raygun.breadcrumbs.express.ts | 33 +++++++++++++++++++ lib/{breadcrumbs.ts => raygun.breadcrumbs.ts} | 24 +------------- lib/raygun.ts | 5 +-- 4 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 lib/raygun.breadcrumbs.express.ts rename lib/{breadcrumbs.ts => raygun.breadcrumbs.ts} (86%) diff --git a/README.md b/README.md index 9677e97..be7b240 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,10 @@ Raygun can cache errors thrown by your Node application when it's running in 'of Raygun includes an on-disk cache provider out of the box, which required write permissions to the folder you wish to use. You cal also pass in your own cache storage. +### Breadcrumbs + + + ##### Getting setup with the default offline provide When creating your Raygun client you need to pass through a cache path diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts new file mode 100644 index 0000000..8382ec5 --- /dev/null +++ b/lib/raygun.breadcrumbs.express.ts @@ -0,0 +1,33 @@ +import type { Request } from "express"; +import type { InternalBreadcrumb } from "./types"; +import { getBreadcrumbs } from "./raygun.breadcrumbs"; + +const debug = require("debug")("raygun").bind( + null, + "[raygun.breadcrumbs.express.js]", +); + +/** + * Parses an ExpressJS Request and adds it to the breadcrumbs store + * @param request + */ +export function addRequestBreadcrumb(request: Request) { + const crumbs = getBreadcrumbs(); + + if (!crumbs) { + debug("Add request breadcrumb skip, no store!"); + return; + } + + const internalCrumb: InternalBreadcrumb = { + category: "http", + message: `${request.method} ${request.url}`, + level: "info", + timestamp: Number(new Date()), + type: "request", + }; + + debug(`recorded request breadcrumb: ${internalCrumb}`); + + crumbs.push(internalCrumb); +} diff --git a/lib/breadcrumbs.ts b/lib/raygun.breadcrumbs.ts similarity index 86% rename from lib/breadcrumbs.ts rename to lib/raygun.breadcrumbs.ts index d055058..9c25316 100644 --- a/lib/breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -1,7 +1,6 @@ import type { AsyncLocalStorage } from "async_hooks"; import type { Breadcrumb, InternalBreadcrumb } from "./types"; -import type { Request, Response } from "express"; -const debug = require("debug")("raygun").bind(null, "[breadcrumbs]"); +const debug = require("debug")("raygun").bind(null, "[raygun.breadcrumbs.js]"); let asyncLocalStorage: AsyncLocalStorage | null = null; @@ -99,27 +98,6 @@ export function addBreadcrumb( crumbs.push(internalCrumb); } -export function addRequestBreadcrumb(request: Request) { - const crumbs = getBreadcrumbs(); - - if (!crumbs) { - debug("Add request breadcrumb skip, no store!"); - return; - } - - const internalCrumb: InternalBreadcrumb = { - category: "http", - message: `${request.method} ${request.url}`, - level: "info", - timestamp: Number(new Date()), - type: "request", - }; - - debug(`recorded request breadcrumb: ${internalCrumb}`); - - crumbs.push(internalCrumb); -} - export function getBreadcrumbs(): InternalBreadcrumb[] | null { if (!asyncLocalStorage) { return null; diff --git a/lib/raygun.ts b/lib/raygun.ts index 1518d24..589d533 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -25,7 +25,8 @@ import { Tag, Transport, } from "./types"; -import * as breadcrumbs from "./breadcrumbs"; +import * as breadcrumbs from "./raygun.breadcrumbs"; +import * as breadcrumbsExpressJs from "./raygun.breadcrumbs.express"; import type { IncomingMessage } from "http"; import { Request, Response, NextFunction } from "express"; import { RaygunBatchTransport } from "./raygun.batch"; @@ -362,7 +363,7 @@ class Raygun { */ expressHandlerBreadcrumbs(req: Request, res: Response, next: NextFunction) { breadcrumbs.runWithBreadcrumbs(() => { - breadcrumbs.addRequestBreadcrumb(req); + breadcrumbsExpressJs.addRequestBreadcrumb(req); // Make the current breadcrumb store available to the express error handler res.locals.breadcrumbs = breadcrumbs.getBreadcrumbs(); next(); From 89c39af0288a3ea300f09a7a6e9c388054d96018 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:22:15 +0200 Subject: [PATCH 29/37] rename --- lib/raygun.breadcrumbs.express.ts | 2 +- lib/raygun.breadcrumbs.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts index 8382ec5..58e3bdc 100644 --- a/lib/raygun.breadcrumbs.express.ts +++ b/lib/raygun.breadcrumbs.express.ts @@ -4,7 +4,7 @@ import { getBreadcrumbs } from "./raygun.breadcrumbs"; const debug = require("debug")("raygun").bind( null, - "[raygun.breadcrumbs.express.js]", + "[raygun.breadcrumbs.express.ts]", ); /** diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts index 9c25316..2c750c2 100644 --- a/lib/raygun.breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -1,6 +1,6 @@ import type { AsyncLocalStorage } from "async_hooks"; import type { Breadcrumb, InternalBreadcrumb } from "./types"; -const debug = require("debug")("raygun").bind(null, "[raygun.breadcrumbs.js]"); +const debug = require("debug")("raygun").bind(null, "[raygun.breadcrumbs.ts]"); let asyncLocalStorage: AsyncLocalStorage | null = null; From 61a55702e413227cd7d9ac353793d54cbd469a27 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:27:57 +0200 Subject: [PATCH 30/37] refactor debug statements --- lib/raygun.breadcrumbs.express.ts | 9 +++------ lib/raygun.breadcrumbs.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts index 58e3bdc..122617e 100644 --- a/lib/raygun.breadcrumbs.express.ts +++ b/lib/raygun.breadcrumbs.express.ts @@ -2,10 +2,7 @@ import type { Request } from "express"; import type { InternalBreadcrumb } from "./types"; import { getBreadcrumbs } from "./raygun.breadcrumbs"; -const debug = require("debug")("raygun").bind( - null, - "[raygun.breadcrumbs.express.ts]", -); +const debug = require("debug")("raygun"); /** * Parses an ExpressJS Request and adds it to the breadcrumbs store @@ -15,7 +12,7 @@ export function addRequestBreadcrumb(request: Request) { const crumbs = getBreadcrumbs(); if (!crumbs) { - debug("Add request breadcrumb skip, no store!"); + debug("[raygun.breadcrumbs.express.ts] Add request breadcrumb skip, no store!"); return; } @@ -27,7 +24,7 @@ export function addRequestBreadcrumb(request: Request) { type: "request", }; - debug(`recorded request breadcrumb: ${internalCrumb}`); + debug(`[raygun.breadcrumbs.express.ts] recorded request breadcrumb: ${internalCrumb}`); crumbs.push(internalCrumb); } diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts index 2c750c2..0feac8a 100644 --- a/lib/raygun.breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -1,15 +1,15 @@ import type { AsyncLocalStorage } from "async_hooks"; import type { Breadcrumb, InternalBreadcrumb } from "./types"; -const debug = require("debug")("raygun").bind(null, "[raygun.breadcrumbs.ts]"); +const debug = require("debug")("raygun"); let asyncLocalStorage: AsyncLocalStorage | null = null; try { asyncLocalStorage = new (require("async_hooks").AsyncLocalStorage)(); - debug("initialized successfully"); + debug("[raygun.breadcrumbs.ts] initialized successfully"); } catch (e) { debug( - "failed to load async_hooks.AsyncLocalStorage - initialization failed\n", + "[raygun.breadcrumbs.ts] failed to load async_hooks.AsyncLocalStorage - initialization failed\n", e, ); } @@ -93,7 +93,7 @@ export function addBreadcrumb( lineNumber: callsite?.lineNumber || undefined, }; - debug(`recorded breadcrumb: ${internalCrumb}`); + debug(`[raygun.breadcrumbs.ts] recorded breadcrumb: ${internalCrumb}`); crumbs.push(internalCrumb); } @@ -111,7 +111,7 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { const newStore: InternalBreadcrumb[] = []; - debug("enter with new store"); + debug("[raygun.breadcrumbs.ts] enter with new store"); asyncLocalStorage.enterWith(newStore); return newStore; @@ -126,7 +126,7 @@ export function runWithBreadcrumbs( return; } - debug("running function with breadcrumbs"); + debug("[raygun.breadcrumbs.ts] running function with breadcrumbs"); asyncLocalStorage.run(store, f); } @@ -135,7 +135,7 @@ export function clear() { return; } - debug("clearing stored breadcrumbs, entering with new store"); + debug("[raygun.breadcrumbs.ts] clearing stored breadcrumbs, entering with new store"); asyncLocalStorage.enterWith([]); } From 9d6fab3488383e06cb2788ac009e91cbfab42d6e Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:30:33 +0200 Subject: [PATCH 31/37] rename logger.js to raygun.client.js --- examples/express-sample/README.md | 2 +- examples/express-sample/app.js | 2 +- examples/express-sample/{logger.js => raygun.client.js} | 0 examples/express-sample/routes/index.js | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename examples/express-sample/{logger.js => raygun.client.js} (100%) diff --git a/examples/express-sample/README.md b/examples/express-sample/README.md index 2c90fed..615c68a 100644 --- a/examples/express-sample/README.md +++ b/examples/express-sample/README.md @@ -24,7 +24,7 @@ in the subdirectory where you found this README.md file. ## Interesting files to look -- `logger.js` +- `raygun.client.js` - Setup of Raygun (lines 9-14) - `app.js` - Sets the user (lines 17-19) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 5ad257a..bc85f69 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -4,7 +4,7 @@ var logger = require("morgan"); var cookieParser = require("cookie-parser"); var bodyParser = require("body-parser"); var sassMiddleware = require("node-sass-middleware"); -var raygunClient = require("./logger"); +var raygunClient = require("./raygun.client"); var routes = require("./routes/index"); var users = require("./routes/users"); diff --git a/examples/express-sample/logger.js b/examples/express-sample/raygun.client.js similarity index 100% rename from examples/express-sample/logger.js rename to examples/express-sample/raygun.client.js diff --git a/examples/express-sample/routes/index.js b/examples/express-sample/routes/index.js index 81c9e43..55d50b1 100644 --- a/examples/express-sample/routes/index.js +++ b/examples/express-sample/routes/index.js @@ -1,6 +1,6 @@ const express = require("express"); const router = express.Router(); -const raygunClient = require("../logger"); +const raygunClient = require("../raygun.client"); /* GET home page. */ router.get("/", function (req, res, next) { From 4a4543ad88623311ebe44cece5865f052bfaea95 Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:34:21 +0200 Subject: [PATCH 32/37] prettier --- lib/raygun.breadcrumbs.express.ts | 8 ++++++-- lib/raygun.breadcrumbs.ts | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts index 122617e..9650e20 100644 --- a/lib/raygun.breadcrumbs.express.ts +++ b/lib/raygun.breadcrumbs.express.ts @@ -12,7 +12,9 @@ export function addRequestBreadcrumb(request: Request) { const crumbs = getBreadcrumbs(); if (!crumbs) { - debug("[raygun.breadcrumbs.express.ts] Add request breadcrumb skip, no store!"); + debug( + "[raygun.breadcrumbs.express.ts] Add request breadcrumb skip, no store!", + ); return; } @@ -24,7 +26,9 @@ export function addRequestBreadcrumb(request: Request) { type: "request", }; - debug(`[raygun.breadcrumbs.express.ts] recorded request breadcrumb: ${internalCrumb}`); + debug( + `[raygun.breadcrumbs.express.ts] recorded request breadcrumb: ${internalCrumb}`, + ); crumbs.push(internalCrumb); } diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts index 0feac8a..a50abb7 100644 --- a/lib/raygun.breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -135,7 +135,9 @@ export function clear() { return; } - debug("[raygun.breadcrumbs.ts] clearing stored breadcrumbs, entering with new store"); + debug( + "[raygun.breadcrumbs.ts] clearing stored breadcrumbs, entering with new store", + ); asyncLocalStorage.enterWith([]); } From a7c9e852101c5237c1b2f50995e306dc0016b13f Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 14:49:43 +0200 Subject: [PATCH 33/37] rename --- examples/express-sample/routes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/express-sample/routes/index.js b/examples/express-sample/routes/index.js index 55d50b1..ea893fa 100644 --- a/examples/express-sample/routes/index.js +++ b/examples/express-sample/routes/index.js @@ -20,7 +20,7 @@ router.get("/send", function (req, res, next) { }); raygunClient - .send("Error in /send endpoint") + .send("Custom Raygun Error in /send endpoint") .then((message) => { res.render("send", { title: "Sent custom error to Raygun", From 937615e1d10ee64064121a94cc70a8a6173b47bd Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 15:07:55 +0200 Subject: [PATCH 34/37] document Breadcrumbs and rename Breadcrumb type --- README.md | 50 ++++++++++++++++++++++++++++--- lib/raygun.breadcrumbs.express.ts | 4 +-- lib/raygun.breadcrumbs.ts | 23 +++++++------- lib/raygun.messageBuilder.ts | 4 +-- lib/raygun.ts | 4 +-- lib/types.ts | 8 ++--- 6 files changed, 66 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index be7b240..154efb0 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,52 @@ const myBeforeSend = function (payload, exception, customData, request, tags) { Raygun.onBeforeSend(myBeforeSend); ``` +### Breadcrumbs + +Breadcrumbs can be sent to Raygun to provide additional information to look into and debug issues stemming from crash reports. + +Breadcrumbs can be created in two ways. + +#### Simple string: + +Call `client.addBreadcrumb(message)`, where message is just a string: + +```js +client.addBreadcrumb('test breadcrumb'); +``` + +#### Using `BreadcrumbMessage`: + +Create your own `BreadcrumbMessage` object and send more than just a message with `client.addBreadcrumb(BreadcrumbMessage)`. + +The structure of the type `BreadcrumbMessage` is as shown here: + +```js +BreadcrumbMessage: { + level: "debug" | "info" | "warning" | "error"; + category: string; + message: string; + customData?: CustomData; +} +``` + +Breadcrumbs can be cleared with `client.clearBreadcrumbs()`. + +#### Breadcrumbs and ExpressJS + +Raygun4Node provides a special ExpressJS middleware that helps to scope Breadcrumbs to a specific request. +As well, this middleware will add a Breadcrumb with information about the performed request. + +To set up, add the Raygun Breadcrumbs ExpressJS handler before configuring any endpoints. + +```js +// Add the Raygun Breadcrumb ExpressJS handler +app.use(raygunClient.expressHandlerBreadcrumbs); + +// Setup the rest of the app, e.g. +app.use("/", routes); +``` + ### Batched error transport You can enable a batched transport mode for the Raygun client by passing `{batch: true}` when initializing. @@ -317,10 +363,6 @@ Raygun can cache errors thrown by your Node application when it's running in 'of Raygun includes an on-disk cache provider out of the box, which required write permissions to the folder you wish to use. You cal also pass in your own cache storage. -### Breadcrumbs - - - ##### Getting setup with the default offline provide When creating your Raygun client you need to pass through a cache path diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts index 9650e20..b6e4bd8 100644 --- a/lib/raygun.breadcrumbs.express.ts +++ b/lib/raygun.breadcrumbs.express.ts @@ -1,5 +1,5 @@ import type { Request } from "express"; -import type { InternalBreadcrumb } from "./types"; +import type { Breadcrumb } from "./types"; import { getBreadcrumbs } from "./raygun.breadcrumbs"; const debug = require("debug")("raygun"); @@ -18,7 +18,7 @@ export function addRequestBreadcrumb(request: Request) { return; } - const internalCrumb: InternalBreadcrumb = { + const internalCrumb: Breadcrumb = { category: "http", message: `${request.method} ${request.url}`, level: "info", diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts index a50abb7..7e2efb2 100644 --- a/lib/raygun.breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -1,8 +1,8 @@ import type { AsyncLocalStorage } from "async_hooks"; -import type { Breadcrumb, InternalBreadcrumb } from "./types"; +import type { BreadcrumbMessage, Breadcrumb } from "./types"; const debug = require("debug")("raygun"); -let asyncLocalStorage: AsyncLocalStorage | null = null; +let asyncLocalStorage: AsyncLocalStorage | null = null; try { asyncLocalStorage = new (require("async_hooks").AsyncLocalStorage)(); @@ -58,8 +58,8 @@ function getCallsite(): SourceFile | null { } export function addBreadcrumb( - breadcrumb: string | Breadcrumb, - type: InternalBreadcrumb["type"] = "manual", + breadcrumb: string | BreadcrumbMessage, + type: Breadcrumb["type"] = "manual", ) { const crumbs = getBreadcrumbs(); @@ -68,7 +68,7 @@ export function addBreadcrumb( } if (typeof breadcrumb === "string") { - const expandedBreadcrumb: Breadcrumb = { + const expandedBreadcrumb: BreadcrumbMessage = { message: breadcrumb, level: "info", category: "", @@ -79,8 +79,8 @@ export function addBreadcrumb( const callsite = getCallsite(); - const internalCrumb: InternalBreadcrumb = { - ...(breadcrumb as Breadcrumb), + const internalCrumb: Breadcrumb = { + ...(breadcrumb as BreadcrumbMessage), category: breadcrumb.category || "", message: breadcrumb.message || "", level: breadcrumb.level || "info", @@ -98,7 +98,7 @@ export function addBreadcrumb( crumbs.push(internalCrumb); } -export function getBreadcrumbs(): InternalBreadcrumb[] | null { +export function getBreadcrumbs(): Breadcrumb[] | null { if (!asyncLocalStorage) { return null; } @@ -109,7 +109,7 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { return store; } - const newStore: InternalBreadcrumb[] = []; + const newStore: Breadcrumb[] = []; debug("[raygun.breadcrumbs.ts] enter with new store"); asyncLocalStorage.enterWith(newStore); @@ -117,10 +117,7 @@ export function getBreadcrumbs(): InternalBreadcrumb[] | null { return newStore; } -export function runWithBreadcrumbs( - f: () => void, - store: InternalBreadcrumb[] = [], -) { +export function runWithBreadcrumbs(f: () => void, store: Breadcrumb[] = []) { if (!asyncLocalStorage) { f(); return; diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index 81de028..d4e6454 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -24,7 +24,7 @@ import { CustomData, Environment, BuiltError, - InternalBreadcrumb, + Breadcrumb, } from "./types"; const debug = require("debug")("raygun"); @@ -280,7 +280,7 @@ export class RaygunMessageBuilder { return data; } - setBreadcrumbs(breadcrumbs: InternalBreadcrumb[] | null) { + setBreadcrumbs(breadcrumbs: Breadcrumb[] | null) { debug( `[raygun.messageBuilder.ts] Added breadcrumbs: ${breadcrumbs?.length || 0}`, ); diff --git a/lib/raygun.ts b/lib/raygun.ts index 589d533..871ea7d 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -9,7 +9,7 @@ "use strict"; import { - Breadcrumb, + BreadcrumbMessage, callVariadicCallback, Callback, CustomData, @@ -202,7 +202,7 @@ class Raygun { * Adds breadcrumb to current context * @param breadcrumb either a string message or a Breadcrumb object */ - addBreadcrumb(breadcrumb: string | Breadcrumb) { + addBreadcrumb(breadcrumb: string | BreadcrumbMessage) { breadcrumbs.addBreadcrumb(breadcrumb); } diff --git a/lib/types.ts b/lib/types.ts index fa20b9f..58e2fa6 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -52,7 +52,7 @@ export type MessageDetails = { machineName: string; environment: Environment; correlationId: string | null; - breadcrumbs?: InternalBreadcrumb[]; + breadcrumbs?: Breadcrumb[]; }; export type Environment = { @@ -196,7 +196,7 @@ export function callVariadicCallback( export type Callback = CallbackNoError | CallbackWithError; -export type InternalBreadcrumb = { +export type Breadcrumb = { timestamp: number; level: "debug" | "info" | "warning" | "error"; type: "manual" | "navigation" | "click-event" | "request" | "console"; @@ -208,6 +208,6 @@ export type InternalBreadcrumb = { lineNumber?: number; }; -export type Breadcrumb = Partial< - Pick +export type BreadcrumbMessage = Partial< + Pick >; From 22842148305fe783aaabf38a075121c45b92ceaf Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 15:12:50 +0200 Subject: [PATCH 35/37] improve docs --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 154efb0..cbfddf7 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ Breadcrumbs can be cleared with `client.clearBreadcrumbs()`. #### Breadcrumbs and ExpressJS -Raygun4Node provides a special ExpressJS middleware that helps to scope Breadcrumbs to a specific request. +Raygun4Node provides a custom ExpressJS middleware that helps to scope Breadcrumbs to a specific request. As well, this middleware will add a Breadcrumb with information about the performed request. To set up, add the Raygun Breadcrumbs ExpressJS handler before configuring any endpoints. @@ -337,6 +337,14 @@ app.use(raygunClient.expressHandlerBreadcrumbs); app.use("/", routes); ``` +This middleware can be user together with the provided ExpressJS error handler. +The order in which the middlewares are configured is important. `expressHandlerBreadcrumbs` should go first to scope breadcrumbs correctly. + +```js +app.use(raygunClient.expressHandlerBreadcrumbs); +app.use(raygunClient.expressHandler); +``` + ### Batched error transport You can enable a batched transport mode for the Raygun client by passing `{batch: true}` when initializing. From 5db3977724386ace7311fd23490092ffd05b900d Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 15:13:35 +0200 Subject: [PATCH 36/37] add word --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbfddf7..d496a89 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ app.use(raygunClient.expressHandlerBreadcrumbs); app.use("/", routes); ``` -This middleware can be user together with the provided ExpressJS error handler. +This middleware can be user together with the provided ExpressJS error handler `expressHandler`. The order in which the middlewares are configured is important. `expressHandlerBreadcrumbs` should go first to scope breadcrumbs correctly. ```js From add1c11e38de870f6114fa56910d6c1237955a5c Mon Sep 17 00:00:00 2001 From: Miguel Beltran Date: Tue, 14 May 2024 16:06:01 +0200 Subject: [PATCH 37/37] removed commented out code --- lib/raygun.breadcrumbs.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts index 7e2efb2..84aca34 100644 --- a/lib/raygun.breadcrumbs.ts +++ b/lib/raygun.breadcrumbs.ts @@ -137,22 +137,3 @@ export function clear() { ); asyncLocalStorage.enterWith([]); } - -// const consoleMethods: [keyof typeof console, InternalBreadcrumb["level"]][] = [ -// ["debug", "debug"], -// ["log", "info"], -// ["info", "info"], -// ["warn", "warning"], -// ["error", "error"], -// ]; - -// for (const [method, level] of consoleMethods) { -// const oldMethod = (console as any)[method]; -// (console as any)[method] = function logWithBreadcrumb( -// this: T, -// ...args: any[] -// ) { -// addBreadcrumb({ message: args.join(" "), level }, "console"); -// return oldMethod.apply(this, args); -// }; -// }