diff --git a/.github/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/bug_report.md rename to .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .github/feature_request.md rename to .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index af7b292..4277e37 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Make sure the actual branch is checked out when running on pull requests ref: ${{ github.head_ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index be83f3c..0deaf69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.15.0-0 +- async/await `send()` support +- Upgrade dependencies +- Improvements in filter method +- Improvements in documentation + ## 0.14.0 - Upgrade dependencies - Support for Node v20 diff --git a/README.md b/README.md index 8be77b0..4da624a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,55 @@ Note that the Express middleware handler will pick up and transmit any `err` obj ## Documentation -### Callbacks +### Send + +The `send()` function is asynchronous and returns a `Promise` of type `IncomingMessage`. + +Note that `IncomingMessage` can be `null` if the request was stored because the application was offline. + +`IncomingMessage` is the response from the Raygun API - there's nothing in the body, it's just a status code response. +If everything went ok, you'll get a 202 response code. +Otherwise, we throw 401 for incorrect API keys, 403 if you're over your plan limits, or anything in the 500+ range for internal errors. + +We use the nodejs http/https library to make the POST to Raygun, you can see more documentation about that callback here: https://nodejs.org/api/http.html#http_http_request_options_callback + +You can `await` the call to obtain the result, or use `then/catch`. + +#### Using `await` + +Use `await` to obtain the `IncomingMessage`, remember to `catch` any possible thrown errors from the `send()` method. + +```js +try { + let message = await client.send(error); +} catch (e) { + // error sending message +} +``` + +#### Using `then/catch` + +You can also use `then()` to obtain the `IncomingMessage`, as well, use `catch()` to catch any possible thrown errors from the `send()` method. + +```js +client.send(error) + .then((message) => { + // message sent to Raygun + }) + .catch((error) => { + // error sending message + }); +``` + +### Legacy `sendWithCallback` + +```javascript +client.sendWithCallback(new Error(), {}, function (response){ }); +``` + +The client still provides a legacy `send()` method that supports callbacks instead of `Promises`. + +**This method is deprecated and will be removed in the future.** The callback should be a node-style callback: `function(err, response) { /*...*/ }`. *Note*: If the callback only takes one parameter (`function(response){ /*...*/ }`) @@ -98,7 +146,7 @@ backwards compatibility; the Node-style callback should be preferred. You can pass custom data in on the Send() function, as the second parameter. For instance (based off the call in test/raygun_test.js): ```javascript -client.send(new Error(), { 'mykey': 'beta' }, function (response){ }); +client.send(new Error(), { 'mykey': 'beta' }); ``` #### Sending custom data with Expressjs @@ -113,19 +161,11 @@ raygunClient.expressCustomData = function (err, req) { }; ``` -### Callback - -```javascript -client.send(new Error(), {}, function (response){ }); -``` - -The argument to the 3rd argument callback is the response from the Raygun API - there's nothing in the body, it's just a status code response. If everything went ok, you'll get a 202 response code. Otherwise we throw 401 for incorrect API keys, 403 if you're over your plan limits, or anything in the 500+ range for internal errors. We use the nodejs http/https library to make the POST to Raygun, you can see more documentation about that callback here: https://nodejs.org/api/http.html#http_http_request_options_callback - ### Sending request data You can send the request data in the Send() function, as the fourth parameter. For example: ```javascript -client.send(new Error(), {}, function () {}, request); +client.send(new Error(), {}, request); ``` If you want to filter any of the request data then you can pass in an array of keys to filter when @@ -139,7 +179,7 @@ const raygunClient = new raygun.Client().init({ apiKey: 'YOUR_API_KEY', filters: You can add tags to your error in the Send() function, as the fifth parameter. For example: ```javascript -client.send(new Error(), {}, function () {}, {}, ['Custom Tag 1', 'Important Error']); +client.send(new Error(), {}, {}, ['Custom Tag 1', 'Important Error']); ``` Tags can also be set globally using setTags diff --git a/examples/readme.md b/examples/README.md similarity index 100% rename from examples/readme.md rename to examples/README.md diff --git a/examples/express-sample/README.md b/examples/express-sample/README.md new file mode 100644 index 0000000..7774d7d --- /dev/null +++ b/examples/express-sample/README.md @@ -0,0 +1,32 @@ +# Raygun + ExpressJS sample + +This is a sample Express application to show how to use Raygun4Node and ExpressJS together. + +This example uses the local `raygun4node` package in the project root directory by simply pointing to the root directory as a dependency in package.json: + +``` +"raygun": "file:../..", +``` + +## Run the sample + +First, install the `raygun4node` package. + +To do so, navigate to the project root directory, then: + + npm install + +Once the package is installed, set your API key in the sample's `config/default.json` and run: + + npm install && npm start + +in the subdirectory where you found this README.md file. + +## Interesting files to look + +- `app.js` + - Setup of Raygun (lines 9-12) + - Sets the user (lines 27-29) + - Attaches Raygun to Express (line 60) +- `routes/index.js` + - Tries to use a fake object, which bounces up to the Express handler (lines 11-15) diff --git a/examples/express-sample/app.js b/examples/express-sample/app.js index 3ef9d4a..3b7f0e2 100644 --- a/examples/express-sample/app.js +++ b/examples/express-sample/app.js @@ -1,7 +1,9 @@ var config = require("config"); if (config.Raygun.Key === "YOUR_API_KEY") { - console.error("You need to set your Raygun API key in the config file"); + console.error( + `[Raygun4Node-Express-Sample] You need to set your Raygun API key in the config file`, + ); process.exit(1); } diff --git a/examples/express-sample/bin/www b/examples/express-sample/bin/www index 3c51e29..8b88819 100755 --- a/examples/express-sample/bin/www +++ b/examples/express-sample/bin/www @@ -65,11 +65,11 @@ function onError(error) { // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': - console.error(bind + ' requires elevated privileges'); + console.error(`[Raygun4Node-Express-Sample] ` + bind + ` requires elevated privileges`); process.exit(1); break; case 'EADDRINUSE': - console.error(bind + ' is already in use'); + console.error(`[Raygun4Node-Express-Sample] ` + bind + ` is already in use`); process.exit(1); break; default: diff --git a/examples/express-sample/package-lock.json b/examples/express-sample/package-lock.json index c70a2f9..89cd721 100644 --- a/examples/express-sample/package-lock.json +++ b/examples/express-sample/package-lock.json @@ -22,7 +22,7 @@ }, "../..": { "name": "raygun", - "version": "0.13.2", + "version": "0.14.0", "dependencies": { "@types/express": "^4.17.21", "debug": "^4.3.4", diff --git a/examples/express-sample/package.json b/examples/express-sample/package.json index d8d249a..3bc8601 100644 --- a/examples/express-sample/package.json +++ b/examples/express-sample/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "DEBUG=express-sample:server node ./bin/www" + "start": "DEBUG=raygun,express-sample:server node ./bin/www" }, "dependencies": { "body-parser": "^1.20.2", diff --git a/examples/express-sample/readme.md b/examples/express-sample/readme.md deleted file mode 100644 index 307be7b..0000000 --- a/examples/express-sample/readme.md +++ /dev/null @@ -1,24 +0,0 @@ -# Raygun + ExpressJS sample - -A demo on how to use Raygun4Node and ExpressJS together. - -This example uses the `raygun4node` package from the project root directory. - -## To run - -First, install the `raygun4node` package. -Navigate to the project root directory, then: - - npm install - -Once the package is installed, set your API key in `config/default.json` and run: - - npm install && npm start - -## Files to look at - -- `app.js` - - Sets the user (line 22) - - Attaches Raygun to Express (line 47) -- `routes/index.js` - - Calls a fake object, which bounces up to the Express handler (line 11) diff --git a/examples/using-domains/readme.md b/examples/using-domains/README.md similarity index 100% rename from examples/using-domains/readme.md rename to examples/using-domains/README.md diff --git a/examples/using-domains/app.js b/examples/using-domains/app.js index 1542996..42e1fd7 100644 --- a/examples/using-domains/app.js +++ b/examples/using-domains/app.js @@ -1,7 +1,9 @@ var config = require("config"); if (config.Raygun.Key === "YOUR_API_KEY") { - console.error("You need to set your Raygun API key in the config file"); + console.error( + `[Raygun4Node-Domains-Sample] You need to set your Raygun API key in the config file`, + ); process.exit(1); } @@ -15,27 +17,30 @@ 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) { - try { - console.log(`Domain error caught: ${err}`); - // Try send data to Raygun - raygunClient.send(err, {}, function () { + console.log(`[Raygun4Node-Domains-Sample] Domain error caught: ${err}`); + // Try send data to Raygun + raygunClient + .send(err) + .then((message) => { // Exit the process once the error has been sent - console.log("Error sent to Raygun, exiting process"); + console.log( + `[Raygun4Node-Domains-Sample] Error sent to Raygun, exiting process`, + ); + process.exit(1); + }) + .catch((error) => { + // If there was an error sending to Raygun, log it out and end the process. + // Could possibly log out to a text file here + console.log(error); process.exit(1); }); - } catch (e) { - // If there was an error sending to Raygun, log it out and end the process. - // Could possibly log out to a text file here - console.log(e); - process.exit(1); - } }); // Run the domain appDomain.run(function () { var fs = require("fs"); - console.log("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 c71ef77..f08d72b 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -43,6 +43,11 @@ export class RaygunBatchTransport { this.httpOptions = options.httpOptions; } + /** + * Enqueues send request to batch processor. + * Callback in SendOptions is called when the message is eventually processed. + * @param options + */ send(options: SendOptions) { this.onIncomingMessage({ serializedMessage: options.message, @@ -56,7 +61,7 @@ export class RaygunBatchTransport { stopProcessing() { if (this.timerId) { - debug("batch transport - stopping"); + debug(`[raygun.batch.ts] Batch transport - stopping`); clearInterval(this.timerId); this.timerId = null; @@ -123,7 +128,7 @@ export class RaygunBatchTransport { const { payload, messageCount, callbacks } = batch; debug( - `batch transport - processing ( ${messageCount} message(s) in batch)`, + `[raygun.batch.ts] Batch transport - processing (${messageCount} message(s) in batch)`, ); const batchId = this.batchId; @@ -137,14 +142,15 @@ export class RaygunBatchTransport { const durationInMs = stopTimer(); if (err) { debug( - `batch transport - error sending batch (id=${batchId}, duration=${durationInMs}ms): ${err}`, + `[raygun.batch.ts] Batch transport - error sending batch (id=${batchId}, duration=${durationInMs}ms): ${err}`, ); } else { debug( - `batch transport - successfully sent batch (id=${batchId}, duration=${durationInMs}ms)`, + `[raygun.batch.ts] Batch transport - successfully sent batch (id=${batchId}, duration=${durationInMs}ms)`, ); } + // TODO: Callbacks are processed in batch, see how can this be implemented with Promises for (const callback of callbacks) { if (callback) { callVariadicCallback(callback, err, response); @@ -153,7 +159,7 @@ export class RaygunBatchTransport { }; debug( - `batch transport - sending batch (id=${batchId}) (${messageCount} messages, ${payload.length} bytes)`, + `[raygun.batch.ts] Batch transport - sending batch (id=${batchId}, ${messageCount} messages, ${payload.length} bytes)`, ); const stopTimer = startTimer(); diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts index b79c5e1..c438e3c 100644 --- a/lib/raygun.messageBuilder.ts +++ b/lib/raygun.messageBuilder.ts @@ -31,18 +31,37 @@ type UserMessageData = RawUserData | string | undefined; const humanString = require("object-to-human-string"); const packageDetails = require("../package.json"); -function filterKeys(obj: object, filters: string[]): object { +/** + * Filter properties in obj according to provided filters. + * Also removes any recursive self-referencing object. + * @param obj object to apply filter + * @param filters list of keys to filter + * @param explored Set that contains already explored nodes, used internally + */ +function filterKeys( + obj: object, + filters: string[], + explored: Set | null = null, +): object { if (!obj || !filters || typeof obj !== "object") { return obj; } + + // create or update the explored set with the incoming object + const _explored = explored?.add(obj) || new Set([obj]); + // Make temporary copy of the object to avoid mutating the original // Cast to Record to enforce type check and avoid using any const _obj = { ...obj } as Record; - Object.keys(obj).forEach(function (i) { - if (filters.indexOf(i) > -1) { - delete _obj[i]; + + Object.keys(obj).forEach(function (key) { + // Remove child if: + // - the key is in the filter array + // - the value is already in the explored Set + if (filters.indexOf(key) > -1 || _explored.has(_obj[key])) { + delete _obj[key]; } else { - _obj[i] = filterKeys(_obj[i], filters); + _obj[key] = filterKeys(_obj[key], filters, _explored); } }); return _obj; diff --git a/lib/raygun.offline.ts b/lib/raygun.offline.ts index 2782a8e..7680a58 100644 --- a/lib/raygun.offline.ts +++ b/lib/raygun.offline.ts @@ -32,6 +32,7 @@ export class OfflineStorage implements IOfflineStorage { path.join(this.cachePath, item), "utf8", (err, cacheContents) => { + // TODO: MessageTransport ignores any errors from the send callback, could this be improved? this.transport.send(cacheContents); fs.unlink(path.join(this.cachePath, item), () => {}); }, @@ -47,7 +48,7 @@ export class OfflineStorage implements IOfflineStorage { this.cacheLimit = offlineStorageOptions.cacheLimit || 100; debug( - `offline storage - initialized (cachePath=${this.cachePath}, cacheLimit=${this.cacheLimit}`, + `[raygun.offline.ts] Offline storage - initialized (cachePath=${this.cachePath}, cacheLimit=${this.cacheLimit})`, ); if (!fs.existsSync(this.cachePath)) { @@ -62,23 +63,25 @@ export class OfflineStorage implements IOfflineStorage { fs.readdir(this.cachePath, (err, files) => { if (err) { - console.log("[Raygun] Error reading cache folder"); + console.log(`[Raygun4Node] Error reading cache folder`); console.log(err); return callback(err); } if (files.length > this.cacheLimit) { - console.log("[Raygun] Error cache reached limit"); + console.log(`[Raygun4Node] Error cache reached limit`); return callback(null); } fs.writeFile(filename, transportItem, "utf8", function (err) { if (!err) { - debug(`offline storage - wrote message to ${filename}`); + debug( + `[raygun.offline.ts] Offline storage - wrote message to ${filename}`, + ); return callback(null); } - console.log("[Raygun] Error writing to cache folder"); + console.log(`[Raygun4Node] Error writing to cache folder`); console.log(err); return callback(err); @@ -95,14 +98,14 @@ export class OfflineStorage implements IOfflineStorage { send(callback: (error: Error | null, items?: string[]) => void) { this.retrieve((err, items) => { if (err) { - console.log("[Raygun] Error reading cache folder"); + console.log(`[Raygun4Node] Error reading cache folder`); console.log(err); return callback(err); } if (items.length > 0) { debug( - "offline storage - transporting ${items.length} message(s) from cache", + `[raygun.offline.ts] Offline storage - transporting ${items.length} message(s) from cache`, ); } diff --git a/lib/raygun.sync.transport.ts b/lib/raygun.sync.transport.ts index 15aac77..9ce871c 100644 --- a/lib/raygun.sync.transport.ts +++ b/lib/raygun.sync.transport.ts @@ -21,12 +21,19 @@ function syncRequest(httpOptions: SendOptionsWithoutCB) { console.log(requestProcess.stdout.toString()); } +/** + * Spawns a synchronous send request. + * Errors are not returned and callback is ignored. + * Only used to report uncaught exceptions. + * @param options + */ export function send(options: SendOptionsWithoutCB) { try { syncRequest(options); } catch (e) { + // TODO: Is there a reason we ignore errors here? console.log( - `Raygun: error ${e} occurred while attempting to send error with message: ${options.message}`, + `[Raygun4Node] Error ${e} occurred while attempting to send error with message: ${options.message}`, ); } } diff --git a/lib/raygun.sync.worker.ts b/lib/raygun.sync.worker.ts index c04715d..8d42b93 100644 --- a/lib/raygun.sync.worker.ts +++ b/lib/raygun.sync.worker.ts @@ -13,10 +13,10 @@ transport.send(sendOptions); function callback(error: Error | null, result: IncomingMessage | null) { if (error) { - console.log("Error sending with sync transport", error); + console.log(`[Raygun4Node] Error sending with sync transport`, error); } else { console.log( - "[raygun-apm] Successfully reported uncaught exception to Raygun", + `[Raygun4Node] Successfully reported uncaught exception to Raygun`, ); } } diff --git a/lib/raygun.transport.ts b/lib/raygun.transport.ts index aa78f49..edc2aee 100644 --- a/lib/raygun.transport.ts +++ b/lib/raygun.transport.ts @@ -26,6 +26,12 @@ export function sendBatch(options: SendOptions) { return send(options, BATCH_ENDPOINT); } +// TODO: Convert this method callbacks to Promise. +/** + * Transport implementation that sends error to Raygun. + * Errors are reported back via callback. + * @param options + */ export function send(options: SendOptions, path = DEFAULT_ENDPOINT) { try { const data = Buffer.from(options.message); @@ -54,7 +60,7 @@ export function send(options: SendOptions, path = DEFAULT_ENDPOINT) { request.on("error", function (e) { console.log( - `Raygun: error ${e.message} occurred while attempting to send error with message: ${options.message}`, + `[Raygun4Node] Error ${e.message} occurred while attempting to send error with message: ${options.message}`, ); // If the callback has two parameters, it should expect an `error` value. @@ -66,8 +72,9 @@ export function send(options: SendOptions, path = DEFAULT_ENDPOINT) { request.write(data); request.end(); } catch (e) { + // TODO: Non-HTTP errors are being ignored, should be better pass them up? console.log( - `Raygun: error ${e} occurred while attempting to send error with message: ${options.message}`, + `[Raygun4Node] Error ${e} occurred while attempting to send error with message: ${options.message}`, ); } } diff --git a/lib/raygun.ts b/lib/raygun.ts index d7552a0..4e96926 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(`client initialized`); + debug(`[raygun.ts] Client initialized`); if (options.reportUncaughtExceptions) { this.reportUncaughtExceptions(); @@ -120,6 +120,7 @@ class Raygun { } this.expressHandler = this.expressHandler.bind(this); + this.sendWithCallback = this.sendWithCallback.bind(this); this.send = this.send.bind(this); this._offlineStorage = @@ -185,10 +186,45 @@ class Raygun { return raygunTransport; } - send( + /** + * Sends exception to Raygun. + * @param exception to send. + * @param customData to attach to the error report. + * @param request custom RequestParams. + * @param tags to attach to the error report. + * @return IncomingMessage if message was delivered, null if stored, rejected with Error if failed. + */ + async send( exception: Error | string, customData?: CustomData, - callback?: (err: Error | null) => void, + request?: RequestParams, + tags?: Tag[], + ): Promise { + // Convert internal sendWithCallback implementation to a Promise. + return new Promise((resolve, reject) => { + this.sendWithCallback( + exception, + customData, + function (err, message) { + if (err != null) { + reject(err); + } else { + resolve(message); + } + }, + request, + tags, + ); + }); + } + + /** + * @deprecated sendWithCallback is a deprecated method. Instead, use send, which supports async/await calls. + */ + sendWithCallback( + exception: Error | string, + customData?: CustomData, + callback?: Callback, request?: RequestParams, tags?: Tag[], ): Message { @@ -204,7 +240,7 @@ class Raygun { if (!sendOptionsResult.valid) { console.error( - `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 sendOptionsResult.message; } @@ -212,11 +248,14 @@ class Raygun { const sendOptions = sendOptionsResult.options; if (this._isOffline) { - this.offlineStorage().save( - JSON.stringify(message), - callback || emptyCallback, - ); + // make the save callback type compatible with Callback + const saveCallback = callback + ? (err: Error | null) => callVariadicCallback(callback, err, null) + : emptyCallback; + this.offlineStorage().save(JSON.stringify(message), saveCallback); } else { + // Use current transport to send request. + // Transport can be batch or default. this.transport().send(sendOptions); } @@ -234,7 +273,7 @@ class Raygun { (major === 13 && minor < 7) ) { console.log( - "[Raygun] 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; @@ -245,20 +284,14 @@ class Raygun { }); } - private sendSync( - exception: Error | string, - customData?: CustomData, - callback?: (err: Error | null) => void, - request?: RequestParams, - tags?: Tag[], - ): void { - const result = this.buildSendOptions( - exception, - customData, - callback, - request, - tags, - ); + /** + * Send error using synchronous transport. + * Only used to report uncaught exceptions. + * @param exception error to report + * @private + */ + private sendSync(exception: Error | string): void { + const result = this.buildSendOptions(exception); if (result.valid) { raygunSyncTransport.send(result.options); @@ -285,15 +318,17 @@ class Raygun { body: req.body, }; - this.send(err, customData || {}, function () {}, requestParams, [ + this.send(err, customData || {}, requestParams, [ "UnhandledException", - ]); + ]).catch((err) => { + console.log(`[Raygun] Failed to send Express error: ${err}`); + }); next(err); } stop() { if (this._batchTransport) { - debug("batch transport stopped"); + debug(`[raygun.ts] Batch transport stopped`); this._batchTransport.stopProcessing(); } } @@ -365,9 +400,13 @@ class Raygun { ) { const durationInMs = stopTimer(); if (error) { - debug(`error sending message (duration=${durationInMs}ms): ${error}`); + debug( + `[raygun.ts] Error sending message (duration=${durationInMs}ms): ${error}`, + ); } else { - debug(`successfully sent message (duration=${durationInMs}ms)`); + debug( + `[raygun.ts] Successfully sent message (duration=${durationInMs}ms)`, + ); } if (!callback) { return; @@ -403,6 +442,7 @@ class Raygun { }; return { + // TODO: MessageTransport ignores any errors from the send callback, could this be improved? send(message: string) { transport.send({ message, diff --git a/lib/types.ts b/lib/types.ts index a869c6c..3c9ebda 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -68,6 +68,7 @@ export type Tag = string; export type SendOptions = { message: string; + // TODO: Remove Callback in SendOptions and use Promises internally callback: Callback; http: HTTPOptions; }; diff --git a/package-lock.json b/package-lock.json index 0ae0dd4..b319106 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "raygun", - "version": "0.14.0", + "version": "0.15.0-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "raygun", - "version": "0.14.0", + "version": "0.15.0-0", "dependencies": { "@types/express": "^4.17.21", "debug": "^4.3.4", @@ -15,8 +15,8 @@ "uuid": "^9.0.1" }, "devDependencies": { - "@eslint/js": "^9.1.1", - "@types/node": "^20.12.7", + "@eslint/js": "^9.2.0", + "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", "eslint": "^8.57.0", @@ -29,7 +29,7 @@ "tap": "^18.7.2", "ts-node": "^10.9.2", "typescript": "^5.4.5", - "typescript-eslint": "^7.7.1", + "typescript-eslint": "^7.8.0", "verror": "^1.10.1" }, "engines": { @@ -154,9 +154,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.1.1.tgz", - "integrity": "sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", + "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1318,9 +1318,9 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", + "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "dependencies": { "undici-types": "~5.26.4" } @@ -1373,16 +1373,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", - "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", + "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/type-utils": "7.7.1", - "@typescript-eslint/utils": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/type-utils": "7.8.0", + "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -1408,15 +1408,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", - "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", + "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4" }, "engines": { @@ -1436,13 +1436,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", - "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1" + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1453,13 +1453,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", - "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", + "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1480,9 +1480,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", - "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1493,13 +1493,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", - "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1545,17 +1545,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", - "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", "semver": "^7.6.0" }, "engines": { @@ -1570,12 +1570,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", - "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/types": "7.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -6593,14 +6593,14 @@ } }, "node_modules/typescript-eslint": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.7.1.tgz", - "integrity": "sha512-ykEBfa3xx3odjZy6GRED4SCPrjo0rgHwstLlEgLX4EMEuv7QeIDSmfV+S6Kk+XkbsYn4BDEcPvsci1X26lRpMQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.8.0.tgz", + "integrity": "sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "7.7.1", - "@typescript-eslint/parser": "7.7.1", - "@typescript-eslint/utils": "7.7.1" + "@typescript-eslint/eslint-plugin": "7.8.0", + "@typescript-eslint/parser": "7.8.0", + "@typescript-eslint/utils": "7.8.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" diff --git a/package.json b/package.json index b94eda4..b1bea88 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "raygun", "description": "Raygun package for Node.js, written in TypeScript", - "version": "0.14.0", + "version": "0.15.0-0", "homepage": "https://github.com/MindscapeHQ/raygun4node", "author": { "name": "Raygun", @@ -44,8 +44,8 @@ "test": "tap --node-arg=-r --node-arg=ts-node/register --disable-coverage test/*_test.js" }, "devDependencies": { - "@eslint/js": "^9.1.1", - "@types/node": "^20.12.7", + "@eslint/js": "^9.2.0", + "@types/node": "^20.12.8", "@types/stack-trace": "0.0.33", "@types/uuid": "^9.0.8", "eslint": "^8.57.0", @@ -58,7 +58,7 @@ "tap": "^18.7.2", "ts-node": "^10.9.2", "typescript": "^5.4.5", - "typescript-eslint": "^7.7.1", + "typescript-eslint": "^7.8.0", "verror": "^1.10.1" }, "dependencies": { diff --git a/test/raygun.messageBuilder_test.js b/test/raygun.messageBuilder_test.js index c8cf3e6..43c2482 100644 --- a/test/raygun.messageBuilder_test.js +++ b/test/raygun.messageBuilder_test.js @@ -377,6 +377,10 @@ test("filter keys tests", function (t) { }); var message = builder.build(); + // Original object should not be modified + t.equal(body.username, "admin@raygun.io"); + t.equal(body.password, "nice try"); + t.test("form is filtered", function (tt) { tt.equal(message.details.request.form.username, undefined); tt.equal(message.details.request.form.password, undefined); @@ -399,6 +403,49 @@ test("filter keys tests", function (t) { }); }); +test("avoid infinite recursion in filter method", function (t) { + let builder = new MessageBuilder({ + filters: ["filtered"], + }); + + // body self-references, causing a potential infinite recursion in the filter method + // Causes exception: Maximum call stack size exceeded + let body = { + key: "value", + filtered: true, + }; + // second level self-reference + let other = { + body: body, + filtered: true, + }; + body.myself = body; + body.other = other; + + let queryString = {}; + let headers = {}; + + // performs filter on set + builder.setRequestDetails({ + body: body, + query: queryString, + headers: headers, + }); + var message = builder.build(); + + // key is preserved + t.equal(message.details.request.form.key, "value"); + // property in "filters" is filtered + t.equal(message.details.request.form.filtered, undefined); + t.equal(message.details.request.form.other.filtered, undefined); + // self-referencing objects are also not included + t.equal(message.details.request.form.myself, undefined); + t.equal(message.details.request.form.other.body, undefined); + + // test should finish + t.end(); +}); + test("custom tags", function (t) { t.test("with array", function (tt) { var builder = new MessageBuilder(); diff --git a/test/raygun_async_send_test.js b/test/raygun_async_send_test.js new file mode 100644 index 0000000..3d270c2 --- /dev/null +++ b/test/raygun_async_send_test.js @@ -0,0 +1,160 @@ +"use strict"; + +const test = require("tap").test; +const VError = require("verror"); +const nock = require("nock"); +const Raygun = require("../lib/raygun.ts"); + +nock(/.*/) + .post(/.*/, function () { + return true; + }) + .reply(202, {}) + .persist(); +const API_KEY = "apikey"; + +test("async send basic", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send complex", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client() + .init({ apiKey: API_KEY }) + .setUser("callum@mindscape.co.nz") + .setVersion("1.0.0.0"); + + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with inner error", {}, function (t) { + t.plan(1); + + let error = new Error("Outer"); + let innerError = new Error("Inner"); + + error.cause = function () { + return innerError; + }; + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(new Error()) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with verror", {}, function (t) { + t.plan(1); + + let error = new VError( + new VError(new VError("Deep Error"), "Inner Error"), + "Outer Error", + ); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + client + .send(error) + .then((response) => { + t.equal(response.statusCode, 202); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("async send with OnBeforeSend", {}, function (t) { + t.plan(1); + + let client = new Raygun.Client().init({ + apiKey: API_KEY, + }); + + let onBeforeSendCalled = false; + client.onBeforeSend(function (payload) { + onBeforeSendCalled = true; + return payload; + }); + + client + .send(new Error()) + .then((response) => { + t.equal(onBeforeSendCalled, true); + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("check that tags get passed through in async send", {}, function (t) { + let tag = ["Test"]; + let client = new Raygun.Client().init({ apiKey: "TEST" }); + + client.setTags(tag); + + client.onBeforeSend(function (payload) { + t.same(payload.details.tags, tag); + return payload; + }); + + client + .send(new Error()) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); + +test("check that tags get merged", {}, function (t) { + let client = new Raygun.Client().init({ apiKey: "TEST" }); + client.setTags(["Tag1"]); + + client.onBeforeSend(function (payload) { + t.same(payload.details.tags, ["Tag1", "Tag2"]); + return payload; + }); + + client + .send(new Error(), {}, null, ["Tag2"]) + .then((message) => { + t.end(); + }) + .catch((err) => { + t.fail(err); + }); +}); diff --git a/test/raygun_offline_test.js b/test/raygun_offline_test.js index aff1cd0..69e058d 100644 --- a/test/raygun_offline_test.js +++ b/test/raygun_offline_test.js @@ -17,18 +17,7 @@ test("offline message storage and sending", async function (t) { }, }); const raygunClient = testEnvironment.client; - const send = (e) => - new Promise((resolve, reject) => { - raygunClient.send(e, null, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - - await send(new Error("offline error")); + await raygunClient.send(new Error("offline error")); const files = fs.readdirSync(cachePath); @@ -44,7 +33,7 @@ test("offline message storage and sending", async function (t) { t.equal(data.details.error.message, "offline error"); - await send(new Error("offline error 2")); + await raygunClient.send(new Error("offline error 2")); await promisify(raygunClient.online.bind(raygunClient))(); await testEnvironment.nextRequest(); @@ -73,18 +62,7 @@ test("batched offline message storage and sending", async function (t) { }, }); const raygunClient = testEnvironment.client; - const send = (e) => - new Promise((resolve, reject) => { - raygunClient.send(e, null, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - - await send(new Error("offline error")); + await raygunClient.send(new Error("offline error")); const files = fs.readdirSync(cachePath); @@ -100,7 +78,7 @@ test("batched offline message storage and sending", async function (t) { t.equal(data.details.error.message, "offline error"); - await send(new Error("offline error 2")); + await raygunClient.send(new Error("offline error 2")); await promisify(raygunClient.online.bind(raygunClient))(); const batch = await testEnvironment.nextBatchRequest(); diff --git a/test/raygun_send_test.js b/test/raygun_send_test.js index 7ab340b..7fa9a6b 100644 --- a/test/raygun_send_test.js +++ b/test/raygun_send_test.js @@ -27,7 +27,7 @@ test("send basic", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(new Error(), {}, function (response) { + client.sendWithCallback(new Error(), {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -47,7 +47,7 @@ test("send complex", {}, function (t) { .setUser("callum@mindscape.co.nz") .setVersion("1.0.0.0"); - client.send(new Error(), {}, function (response) { + client.sendWithCallback(new Error(), {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -72,7 +72,7 @@ test("send with inner error", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(error, {}, function (response) { + client.sendWithCallback(error, {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -95,7 +95,7 @@ test("send with verror", {}, function (t) { var client = new Raygun.Client().init({ apiKey: API_KEY, }); - client.send(error, {}, function (response) { + client.sendWithCallback(error, {}, function (response) { t.equal(response.statusCode, 202); t.end(); }); @@ -120,7 +120,7 @@ test("send with OnBeforeSend", {}, function (t) { return payload; }); - client.send(new Error(), {}, function () { + client.sendWithCallback(new Error(), {}, function () { t.equal(onBeforeSendCalled, true); t.end(); }); @@ -135,9 +135,9 @@ test("send with expressHandler custom data", function (t) { client.expressCustomData = function () { return { test: "data" }; }; - client._send = client.send; - client.send = function (err, data) { - client.send = client._send; + client._send = client.sendWithCallback; + client.sendWithCallback = function (err, data) { + client.sendWithCallback = client._send; t.equal(data.test, "data"); t.end(); }; @@ -155,7 +155,7 @@ test("check that tags get passed through", {}, function (t) { return payload; }); - client.send(new Error(), {}, function () { + client.sendWithCallback(new Error(), {}, function () { t.end(); }); }); @@ -169,7 +169,7 @@ test("check that tags get merged", {}, function (t) { return payload; }); - client.send( + client.sendWithCallback( new Error(), {}, function () {