diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 6f14c3f1..a690f17b 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -117,15 +117,7 @@ jobs: working-directory: examples/${{ matrix.example }} - run: npm audit signatures working-directory: examples/${{ matrix.example }} - - name: Smoke test Express example - if: matrix.example == 'express-sample' - working-directory: examples/express-sample + - name: Test example error reporting against a local endpoint env: - NODE_ENV: production - run: node -e "require('./app')" - - name: Smoke test domains example - if: matrix.example == 'using-domains' - working-directory: examples/using-domains - run: | - node --check app.js - node -e "require('config'); require('raygun')" + RAYGUN_TEST_EXAMPLE: ${{ matrix.example }} + run: npm run test:examples diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d5f3e95..33a3230e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +- Updated vulnerable transitive development dependencies and the Express example's Morgan and proxy-addr dependencies. +- Removed report payloads and transport error text from transport diagnostics while preserving errors returned to callers. +- Documented request-filter boundaries and redaction with `onBeforeSend`. + ## 2.2.9 - Maintenance-only release; no public API or runtime behavior changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aab67fa8..2734d711 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,20 @@ The recommended IDE for working on this project is Visual Studio Code. To run tests, run `npm run test` or run all tests from VSCode. +To test the actual examples against a local mock Raygun endpoint, run these +commands from the repository root (Node.js 20 or later): + +```sh +npm run prepare +npm --prefix examples/express-sample ci --ignore-scripts +npm --prefix examples/using-domains ci --ignore-scripts +npm run test:examples +``` + +These tests use a test API key and do not contact Raygun. Set +`RAYGUN_TEST_EXAMPLE=express-sample` or `RAYGUN_TEST_EXAMPLE=using-domains` to run +only one example. + ### Code analysis To check the code, run `npm run eslint` and `npm run tseslint`. @@ -63,4 +77,3 @@ Wait for a review by the Raygun team. The team will leave you feedback and might ask you to do changes in your code. Once the PR is approved, the team will merge it. - diff --git a/README.md b/README.md index 8fd59827..6a1a3cdb 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,35 @@ const raygun = require('raygun'); const raygunClient = new raygun.Client().init({ apiKey: 'YOUR_API_KEY', filters: ['password', 'creditcard'] }); ``` +### Protecting sensitive data + +No request keys are filtered by default. `filters` removes exact, case-sensitive keys +recursively from request headers, query parameters, and parsed request bodies. +For Node.js request headers, use lowercase keys such as `authorization` and `cookie`. +Filters do not sanitize raw strings, error messages, user information, custom data, +or breadcrumbs. Avoid collecting credentials and personal data in these fields. + +Use `onBeforeSend` to review the complete report before transport. For example, +remove custom data and breadcrumbs if your application does not need them: + +```javascript +const raygunClient = new raygun.Client().init({ + apiKey: 'YOUR_API_KEY', + filters: ['authorization', 'cookie', 'password', 'creditcard'], + onBeforeSend: function (payload) { + delete payload.details.userCustomData; + delete payload.details.breadcrumbs; + return payload; + } +}); +``` + +If you retain these sections, allowlist the fields your application needs and redact +their values before sending. Review error messages and user information separately. +Transport diagnostics do not log report payloads or transport error text, but other +SDK debug messages can contain application-provided text. Keep `DEBUG=raygun` +disabled in production unless you have reviewed the data and log access controls. + ### Tags You can add tags to your error in the Send() function, as the `tags` parameter. For example: diff --git a/examples/express-sample/package-lock.json b/examples/express-sample/package-lock.json index 7675cd68..d2a59f50 100644 --- a/examples/express-sample/package-lock.json +++ b/examples/express-sample/package-lock.json @@ -14,7 +14,7 @@ "debug": "^4.3.4", "ejs": "~6.0.1", "express": "^4.22.2", - "morgan": "^1.12.0", + "morgan": "^1.12.1", "raygun": "file:../..", "serve-favicon": "^2.5.1" } @@ -744,9 +744,9 @@ } }, "node_modules/morgan": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.0.tgz", - "integrity": "sha512-OHpTRQwn2ezasILW8iKe+Yww1XsfWsZIpUOLF7RDb2g5GwO3trPaRwi7+8BDiJ7HFx2Kg2mfUdCBcVhwYlOz2g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.1.tgz", + "integrity": "sha512-tljKC0ex20AjO58Ob/eZ53JloycbVswbVNCHx6V6VLGzqt/w8dIynVGL0G8qVjNKwiA7sYSogCrN6QtJ82IV+g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -842,9 +842,9 @@ "license": "MIT" }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -852,6 +852,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/qs": { diff --git a/examples/express-sample/package.json b/examples/express-sample/package.json index 41a23370..6c453730 100644 --- a/examples/express-sample/package.json +++ b/examples/express-sample/package.json @@ -12,7 +12,7 @@ "debug": "^4.3.4", "ejs": "~6.0.1", "express": "^4.22.2", - "morgan": "^1.12.0", + "morgan": "^1.12.1", "raygun": "file:../..", "serve-favicon": "^2.5.1" }, diff --git a/lib/raygun.batch.ts b/lib/raygun.batch.ts index 97f200ab..0a51036a 100644 --- a/lib/raygun.batch.ts +++ b/lib/raygun.batch.ts @@ -82,9 +82,8 @@ export class RaygunBatchTransport { if (messageLength >= MAX_BATCH_INNER_SIZE_BYTES) { const messageSize = Math.ceil(messageLength / 1024); - const startOfMessage = serializedMessage.slice(0, 1000); - const errorMessage = `Error is too large to send to Raygun (${messageSize}kb)\nStart of error: ${startOfMessage}`; + const errorMessage = `Error is too large to send to Raygun (${messageSize}kb)`; console.error(`[Raygun4Node] ${errorMessage}`); throw Error(errorMessage); } @@ -153,7 +152,7 @@ export class RaygunBatchTransport { const durationInMs = stopTimer(); if (err) { debug( - `[raygun.batch.ts] Batch transport - error sending batch (id=${batchId}, duration=${durationInMs}ms): ${err}`, + `[raygun.batch.ts] Batch transport - error sending batch (id=${batchId}, duration=${durationInMs}ms)`, ); for (const promise of promises) { promise.reject(err); diff --git a/lib/raygun.transport.ts b/lib/raygun.transport.ts index 9962d031..05216181 100644 --- a/lib/raygun.transport.ts +++ b/lib/raygun.transport.ts @@ -61,26 +61,20 @@ export function send( resolve(response); // destroy the request after successful completion request.destroy(); - debug( - `[raygun.transport.ts] Request destroyed for message: ${options.message}`, - ); + debug("[raygun.transport.ts] Request destroyed after sending report"); }, ); if (options.http?.timeout) { debug(`[raygun.transport.ts] Timeout set: ${options.http.timeout}ms`); request.setTimeout(options.http.timeout, () => { - console.error( - `[Raygun4Node] request timed out while attempting to send error with message: ${options.message}`, - ); + console.error("[Raygun4Node] Request timed out while sending report"); request.destroy(new Error("Request timed out")); }); } request.on("error", function (e) { - console.error( - `[Raygun4Node] Error with details "${e.message}" occurred while attempting to send error with message: ${options.message}`, - ); + console.error("[Raygun4Node] Transport error while sending report"); // request failed reject(e); @@ -90,9 +84,7 @@ export function send( request.end(); }); } catch (e) { - console.error( - `[Raygun4Node] Error "${e}" occurred while attempting to send error with message: ${options.message}`, - ); + console.error("[Raygun4Node] Failed to prepare report for transport"); return Promise.reject(e); } } diff --git a/lib/raygun.ts b/lib/raygun.ts index 161b95ad..95848401 100644 --- a/lib/raygun.ts +++ b/lib/raygun.ts @@ -312,9 +312,7 @@ class Raygun { } if (sendOptionsResult.skip) { - console.log( - `[Raygun4Node] Skip sending message: ${sendOptionsResult.message.details.error}.`, - ); + console.log("[Raygun4Node] Skipped sending report"); return Promise.resolve(null); } @@ -353,7 +351,7 @@ class Raygun { .catch((error) => { const durationInMs = stopTimer(); debug( - `[raygun.ts] Error sending message (duration=${durationInMs}ms): ${error}`, + `[raygun.ts] Error sending message (duration=${durationInMs}ms)`, ); return error; }); @@ -405,9 +403,7 @@ class Raygun { if (result.valid) { if (result.skip) { - console.log( - `[Raygun4Node] Skip sending message: ${result.message.details.error}.`, - ); + console.log("[Raygun4Node] Skipped sending report"); return; } raygunSyncTransport.send(result.options); diff --git a/package-lock.json b/package-lock.json index c68bc263..fa09e2fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2324,9 +2324,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3281,9 +3281,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -3645,9 +3645,9 @@ "license": "MIT" }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "dev": true, "license": "BSD-2-Clause" }, @@ -3866,9 +3866,9 @@ } }, "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "dev": true, "license": "MIT", "engines": { @@ -5078,9 +5078,9 @@ } }, "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5089,6 +5089,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/punycode": { @@ -6235,9 +6239,9 @@ } }, "node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 08b5d9b6..c304b326 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "prepare": "tsc", "//test": "runs tests on main project", "test": "tap --node-arg=-r --node-arg=ts-node/register --disable-coverage test/*_test.js", + "test:examples": "tap --node-arg=-r --node-arg=ts-node/register --disable-coverage test/examples_integration.js", "//test:cjs": "tests CommonJS compatibility (catches ESM-only deps)", "test:cjs": "node --no-experimental-require-module scripts/test-cjs-compat.js" }, diff --git a/test/examples_integration.js b/test/examples_integration.js new file mode 100644 index 00000000..21545f56 --- /dev/null +++ b/test/examples_integration.js @@ -0,0 +1,77 @@ +const test = require("tap").test; +const { execFile } = require("node:child_process"); +const { promisify } = require("node:util"); +const path = require("node:path"); +const { makeClientWithMockServer } = require("./utils"); + +for (const example of ["express-sample", "using-domains"]) { + if ( + process.env.RAYGUN_TEST_EXAMPLE && + process.env.RAYGUN_TEST_EXAMPLE !== example + ) { + continue; + } + test(`${example} sends real error reports to a local endpoint`, async (t) => { + const environment = await makeClientWithMockServer(); + t.teardown(() => environment.stop()); + let exitCode = 0; + try { + await promisify(execFile)( + process.execPath, + [path.join(__dirname, "fixtures/example_transport.js")], + { + cwd: path.join(__dirname, "../examples", example), + timeout: 10000, + env: { + ...process.env, + NODE_ENV: "production", + NODE_CONFIG: JSON.stringify({ Raygun: { Key: "TEST_API_KEY" } }), + RAYGUN_TEST_PORT: String(environment.address.port), + RAYGUN_TEST_EXAMPLE: example, + }, + }, + ); + } catch (error) { + if (example !== "using-domains" || error.code !== 1) throw error; + t.match(error.stdout, /Error sent to Raygun, exiting process/); + exitCode = error.code; + } + const entries = environment.server.entries; + if (example === "express-sample") { + t.equal(exitCode, 0); + t.equal(entries.length, 2, "skip route does not send a report"); + const [sent, failed] = entries; + t.equal( + sent.details.error.message, + "Custom Raygun Error in /send endpoint", + ); + t.same(sent.details.tags, ["request"]); + t.same(sent.details.user, { + identifier: "123456", + email: "custom@example.com", + }); + t.same( + sent.details.breadcrumbs.map((crumb) => crumb.message), + ["GET /send", "Breadcrumb in /send endpoint"], + ); + t.equal(failed.details.error.message, "fakeObject is not defined"); + t.same(failed.details.tags, ["UnhandledException"]); + t.equal(failed.details.request.url, "/error"); + t.same( + failed.details.breadcrumbs.map((crumb) => crumb.message), + ["GET /error", "Breadcrumb in /error endpoint"], + ); + } else { + t.equal(exitCode, 1, "sample exits after its intentional error"); + t.equal(entries.length, 1); + t.equal( + entries[0].details.error.message, + "fakeErrorHandler is not defined", + ); + t.same( + entries[0].details.breadcrumbs.map((crumb) => crumb.message), + ["Running example app", "Domain error caught!"], + ); + } + }); +} diff --git a/test/fixtures/example_transport.js b/test/fixtures/example_transport.js new file mode 100644 index 00000000..b3504ca3 --- /dev/null +++ b/test/fixtures/example_transport.js @@ -0,0 +1,52 @@ +const assert = require("node:assert/strict"); +const path = require("node:path"); +const { Client } = require("../.."); +const { fetch } = globalThis; + +// Redirect the real sample client before loading the sample. Never contact Raygun. +const init = Client.prototype.init; +Client.prototype.init = function (options) { + return init.call(this, { + ...options, + host: "localhost", + port: Number(process.env.RAYGUN_TEST_PORT), + useSSL: false, + }); +}; + +const app = require(path.resolve("app.js")); + +if (process.env.RAYGUN_TEST_EXAMPLE === "express-sample") { + const client = require(path.resolve("raygun.client.js")); + const pending = []; + const send = client.send; + client.send = (...args) => { + const result = send(...args); + pending.push(result); + return result; + }; + + const server = app.listen(0, "localhost", async () => { + try { + const base = `http://localhost:${server.address().port}`; + const sent = await fetch(`${base}/send`); + assert.equal(sent.status, 200); + assert.match(await sent.text(), /Raygun status code: 200/); + const failed = await fetch(`${base}/error`); + assert.equal(failed.status, 500); + await failed.text(); + const skipped = await fetch(`${base}/skip`); + assert.equal(skipped.status, 200); + assert.match(await skipped.text(), /Message should be null: null/); + await Promise.all(pending); + assert.equal(pending.length, 3); + } catch (error) { + console.error(error); + process.exitCode = 1; + } finally { + client.stop(); + server.close(); + server.closeAllConnections(); + } + }); +} diff --git a/test/raygun_test.js b/test/raygun_test.js index cbb9b329..f83ce243 100644 --- a/test/raygun_test.js +++ b/test/raygun_test.js @@ -26,3 +26,47 @@ test("user", function (t) { t.equal(client.user(req), "theuser"); t.end(); }); + +test("request filters and onBeforeSend protect separate report sections", async (t) => { + const { makeClientWithMockServer } = require("./utils"); + const secret = "private-data-sentinel"; + let hookCalled = false; + const environment = await makeClientWithMockServer({ + filters: ["authorization", "cookie", "password"], + onBeforeSend: (payload) => { + hookCalled = true; + t.same(payload.details.request.headers, { accept: "application/json" }); + t.same(payload.details.request.queryString, { page: "2" }); + t.same(payload.details.request.form, { nested: { safe: "retained" } }); + t.equal(payload.details.userCustomData.password, secret); + t.equal(payload.details.breadcrumbs[0].customData.password, secret); + delete payload.details.userCustomData; + delete payload.details.breadcrumbs; + return payload; + }, + }); + t.teardown(() => environment.stop()); + environment.client.addBreadcrumb({ + message: "operation started", + customData: { password: secret }, + }); + const nextRequest = environment.nextRequest(); + await environment.client.send(new Error("operation failed"), { + customData: { password: secret }, + request: { + headers: { + authorization: secret, + cookie: secret, + accept: "application/json", + }, + query: { password: secret, page: "2" }, + body: { nested: { password: secret, safe: "retained" } }, + }, + }); + const message = await nextRequest; + t.ok(hookCalled); + t.same(message.details.request.form, { nested: { safe: "retained" } }); + t.notOk("userCustomData" in message.details); + t.notOk("breadcrumbs" in message.details); + t.notMatch(JSON.stringify(message), secret); +}); diff --git a/test/raygun_transport_test.js b/test/raygun_transport_test.js new file mode 100644 index 00000000..44b89c6e --- /dev/null +++ b/test/raygun_transport_test.js @@ -0,0 +1,116 @@ +"use strict"; + +const test = require("tap").test; +const http = require("http"); +const { EventEmitter } = require("events"); +const debug = require("debug"); +const { send, sendBatch } = require("../lib/raygun.transport"); + +for (const outcome of ["success", "error", "timeout", "invalid payload"]) { + test(`transport logs omit sensitive data on ${outcome}`, async (t) => { + const logs = []; + const originalError = console.error; + const originalLog = debug.log; + const originalRequest = http.request; + const namespaces = debug.disable(); + debug.enable("raygun"); + console.error = (...args) => logs.push(args.join(" ")); + debug.log = (...args) => logs.push(args.join(" ")); + t.teardown(() => { + console.error = originalError; + debug.log = originalLog; + http.request = originalRequest; + debug.enable(namespaces); + }); + + const secret = "private-report-sentinel"; + const message = JSON.stringify({ password: secret }); + const transportError = new Error(`remote error containing ${secret}`); + const response = { statusCode: 202 }; + let timeoutCallback; + let written; + let destroyed = false; + http.request = (options, callback) => { + t.equal(options.headers["X-ApiKey"], "private-api-key"); + const request = new EventEmitter(); + request.write = (data) => { + written = data.toString(); + }; + request.setTimeout = (timeout, handler) => { + t.equal(timeout, 50); + timeoutCallback = handler; + }; + request.destroy = (error) => { + destroyed = true; + if (error) request.emit("error", error); + }; + request.end = () => { + if (outcome === "success") callback(response); + if (outcome === "error") request.emit("error", transportError); + if (outcome === "timeout") timeoutCallback(); + }; + return request; + }; + + for (const transport of [send, sendBatch]) { + const result = transport({ + message: outcome === "invalid payload" ? undefined : message, + http: { apiKey: "private-api-key", timeout: 50, useSSL: false }, + }); + if (outcome === "success") { + t.equal(await result, response); + t.ok(destroyed); + } else if (outcome === "error") { + try { + await result; + t.fail("transport must reject"); + } catch (error) { + t.equal( + error, + transportError, + "caller still receives original error", + ); + } + } else { + await t.rejects( + result, + outcome === "timeout" ? /Request timed out/ : TypeError, + ); + } + if (outcome !== "invalid payload") t.equal(written, message); + } + + t.ok(logs.length, "transport emits diagnostics"); + t.notMatch(logs.join("\n"), secret); + t.notMatch(logs.join("\n"), "private-api-key"); + }); +} + +test("oversized batches do not expose report contents in logs or errors", (t) => { + const { + RaygunBatchTransport, + MAX_BATCH_SIZE_BYTES, + } = require("../lib/raygun.batch"); + const logs = []; + const originalError = console.error; + console.error = (...args) => logs.push(args.join(" ")); + t.teardown(() => { + console.error = originalError; + }); + const transport = new RaygunBatchTransport({ + interval: 1000, + httpOptions: {}, + }); + t.teardown(() => transport.stopProcessing()); + const secret = "oversized-private-sentinel"; + try { + transport.send({ message: secret + "x".repeat(MAX_BATCH_SIZE_BYTES) }); + t.fail("oversized report must be rejected"); + } catch (error) { + t.match(error.message, /Error is too large to send to Raygun/); + t.notMatch(error.message, secret); + } + t.ok(logs.length); + t.notMatch(logs.join("\n"), secret); + t.end(); +});