diff --git a/README.md b/README.md
index 9677e97..d496a89 100644
--- a/README.md
+++ b/README.md
@@ -291,6 +291,60 @@ 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 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.
+
+```js
+// Add the Raygun Breadcrumb ExpressJS handler
+app.use(raygunClient.expressHandlerBreadcrumbs);
+
+// Setup the rest of the app, e.g.
+app.use("/", routes);
+```
+
+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
+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.
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/examples/express-sample/README.md b/examples/express-sample/README.md
index 7774d7d..615c68a 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
+- `raygun.client.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 2882b0d..bc85f69 100644
--- a/examples/express-sample/app.js
+++ b/examples/express-sample/app.js
@@ -1,24 +1,10 @@
-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");
var logger = require("morgan");
var cookieParser = require("cookie-parser");
var bodyParser = require("body-parser");
var sassMiddleware = require("node-sass-middleware");
+var raygunClient = require("./raygun.client");
var routes = require("./routes/index");
var users = require("./routes/users");
@@ -34,6 +20,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,7 +47,9 @@ app.use(express.static(path.join(__dirname, "public")));
app.use("/", routes);
app.use("/users", users);
-// Add the Raygun Express handler
+// Add the Raygun error 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/examples/express-sample/raygun.client.js b/examples/express-sample/raygun.client.js
new file mode 100644
index 0000000..77314f8
--- /dev/null
+++ b/examples/express-sample/raygun.client.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..ea893fa 100644
--- a/examples/express-sample/routes/index.js
+++ b/examples/express-sample/routes/index.js
@@ -1,14 +1,50 @@
-var express = require("express");
-var router = express.Router();
+const express = require("express");
+const router = express.Router();
+const raygunClient = require("../raygun.client");
/* 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("Custom Raygun 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 %>
+
+
+
+
+
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",
diff --git a/lib/raygun.breadcrumbs.express.ts b/lib/raygun.breadcrumbs.express.ts
new file mode 100644
index 0000000..b6e4bd8
--- /dev/null
+++ b/lib/raygun.breadcrumbs.express.ts
@@ -0,0 +1,34 @@
+import type { Request } from "express";
+import type { Breadcrumb } from "./types";
+import { getBreadcrumbs } from "./raygun.breadcrumbs";
+
+const debug = require("debug")("raygun");
+
+/**
+ * Parses an ExpressJS Request and adds it to the breadcrumbs store
+ * @param request
+ */
+export function addRequestBreadcrumb(request: Request) {
+ const crumbs = getBreadcrumbs();
+
+ if (!crumbs) {
+ debug(
+ "[raygun.breadcrumbs.express.ts] Add request breadcrumb skip, no store!",
+ );
+ return;
+ }
+
+ const internalCrumb: Breadcrumb = {
+ category: "http",
+ message: `${request.method} ${request.url}`,
+ level: "info",
+ timestamp: Number(new Date()),
+ type: "request",
+ };
+
+ debug(
+ `[raygun.breadcrumbs.express.ts] recorded request breadcrumb: ${internalCrumb}`,
+ );
+
+ crumbs.push(internalCrumb);
+}
diff --git a/lib/raygun.breadcrumbs.ts b/lib/raygun.breadcrumbs.ts
new file mode 100644
index 0000000..84aca34
--- /dev/null
+++ b/lib/raygun.breadcrumbs.ts
@@ -0,0 +1,139 @@
+import type { AsyncLocalStorage } from "async_hooks";
+import type { BreadcrumbMessage, Breadcrumb } from "./types";
+const debug = require("debug")("raygun");
+
+let asyncLocalStorage: AsyncLocalStorage | null = null;
+
+try {
+ asyncLocalStorage = new (require("async_hooks").AsyncLocalStorage)();
+ debug("[raygun.breadcrumbs.ts] initialized successfully");
+} catch (e) {
+ debug(
+ "[raygun.breadcrumbs.ts] 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;
+
+ // Ignore use of any, required for captureStackTrace
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const output: any = {};
+
+ Error.captureStackTrace(output);
+
+ const callsite = output.stack;
+ Error.prepareStackTrace = originalPrepareStacktrace;
+ return callsite;
+}
+
+export function addBreadcrumb(
+ breadcrumb: string | BreadcrumbMessage,
+ type: Breadcrumb["type"] = "manual",
+) {
+ const crumbs = getBreadcrumbs();
+
+ if (!crumbs) {
+ return;
+ }
+
+ if (typeof breadcrumb === "string") {
+ const expandedBreadcrumb: BreadcrumbMessage = {
+ message: breadcrumb,
+ level: "info",
+ category: "",
+ };
+
+ breadcrumb = expandedBreadcrumb;
+ }
+
+ const callsite = getCallsite();
+
+ const internalCrumb: Breadcrumb = {
+ ...(breadcrumb as BreadcrumbMessage),
+ 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(`[raygun.breadcrumbs.ts] recorded breadcrumb: ${internalCrumb}`);
+
+ crumbs.push(internalCrumb);
+}
+
+export function getBreadcrumbs(): Breadcrumb[] | null {
+ if (!asyncLocalStorage) {
+ return null;
+ }
+
+ const store = asyncLocalStorage.getStore();
+
+ if (store) {
+ return store;
+ }
+
+ const newStore: Breadcrumb[] = [];
+
+ debug("[raygun.breadcrumbs.ts] enter with new store");
+ asyncLocalStorage.enterWith(newStore);
+
+ return newStore;
+}
+
+export function runWithBreadcrumbs(f: () => void, store: Breadcrumb[] = []) {
+ if (!asyncLocalStorage) {
+ f();
+ return;
+ }
+
+ debug("[raygun.breadcrumbs.ts] running function with breadcrumbs");
+ asyncLocalStorage.run(store, f);
+}
+
+export function clear() {
+ if (!asyncLocalStorage) {
+ return;
+ }
+
+ debug(
+ "[raygun.breadcrumbs.ts] clearing stored breadcrumbs, entering with new store",
+ );
+ asyncLocalStorage.enterWith([]);
+}
diff --git a/lib/raygun.messageBuilder.ts b/lib/raygun.messageBuilder.ts
index 96ebf29..d4e6454 100644
--- a/lib/raygun.messageBuilder.ts
+++ b/lib/raygun.messageBuilder.ts
@@ -24,8 +24,11 @@ import {
CustomData,
Environment,
BuiltError,
+ Breadcrumb,
} from "./types";
+const debug = require("debug")("raygun");
+
type UserMessageData = RawUserData | string | undefined;
const humanString = require("object-to-human-string");
@@ -276,4 +279,14 @@ export class RaygunMessageBuilder {
}
return data;
}
+
+ setBreadcrumbs(breadcrumbs: Breadcrumb[] | null) {
+ debug(
+ `[raygun.messageBuilder.ts] Added breadcrumbs: ${breadcrumbs?.length || 0}`,
+ );
+ if (breadcrumbs) {
+ this.message.details.breadcrumbs = [...breadcrumbs];
+ }
+ return this;
+ }
}
diff --git a/lib/raygun.ts b/lib/raygun.ts
index cf1c55f..871ea7d 100644
--- a/lib/raygun.ts
+++ b/lib/raygun.ts
@@ -9,6 +9,7 @@
"use strict";
import {
+ BreadcrumbMessage,
callVariadicCallback,
Callback,
CustomData,
@@ -24,8 +25,10 @@ import {
Tag,
Transport,
} from "./types";
+import * as breadcrumbs from "./raygun.breadcrumbs";
+import * as breadcrumbsExpressJs from "./raygun.breadcrumbs.express";
import type { IncomingMessage } from "http";
-import type { Request, Response, NextFunction } from "express";
+import { Request, Response, NextFunction } from "express";
import { RaygunBatchTransport } from "./raygun.batch";
import { RaygunMessageBuilder } from "./raygun.messageBuilder";
import { OfflineStorage } from "./raygun.offline";
@@ -195,6 +198,21 @@ class Raygun {
this._tags = tags;
}
+ /**
+ * Adds breadcrumb to current context
+ * @param breadcrumb either a string message or a Breadcrumb object
+ */
+ addBreadcrumb(breadcrumb: string | BreadcrumbMessage) {
+ breadcrumbs.addBreadcrumb(breadcrumb);
+ }
+
+ /**
+ * Manually clear stored breadcrumbs for current context
+ */
+ clearBreadcrumbs() {
+ breadcrumbs.clear();
+ }
+
transport(): Transport {
if (this._batch && this._batchTransport) {
return this._batchTransport;
@@ -335,6 +353,31 @@ 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(() => {
+ breadcrumbsExpressJs.addRequestBreadcrumb(req);
+ // Make the current breadcrumb store available to the express error handler
+ res.locals.breadcrumbs = breadcrumbs.getBreadcrumbs();
+ 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;
@@ -355,11 +398,30 @@ class Raygun {
body: req.body,
};
- this.send(err, customData || {}, requestParams, [
- "UnhandledException",
- ]).catch((err) => {
- console.error("[Raygun] Failed to send Express error", err);
- });
+ // If a local store of breadcrumbs exist in the response
+ // run in scoped breadcrumbs store
+ if (res.locals && 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);
+ });
+ }
+
next(err);
}
@@ -399,6 +461,7 @@ class Raygun {
.setUserCustomData(customData)
.setUser(this.user(request) || this._user)
.setVersion(this._version)
+ .setBreadcrumbs(breadcrumbs.getBreadcrumbs())
.setTags(mergedTags);
let message = builder.build();
diff --git a/lib/types.ts b/lib/types.ts
index 7fc506a..58e2fa6 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -52,6 +52,7 @@ export type MessageDetails = {
machineName: string;
environment: Environment;
correlationId: string | null;
+ breadcrumbs?: Breadcrumb[];
};
export type Environment = {
@@ -194,3 +195,19 @@ export function callVariadicCallback(
}
export type Callback = CallbackNoError | CallbackWithError;
+
+export type Breadcrumb = {
+ 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 BreadcrumbMessage = Partial<
+ Pick
+>;
diff --git a/test/raygun_breadcrumbs_test.js b/test/raygun_breadcrumbs_test.js
new file mode 100644
index 0000000..96bf247
--- /dev/null
+++ b/test/raygun_breadcrumbs_test.js
@@ -0,0 +1,240 @@
+const express = require("express");
+const deepEqual = require("assert").deepEqual;
+const test = require("tap").test;
+const { listen, makeClientWithMockServer, request } = require("./utils");
+
+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;
+
+ // Must be defined before any endpoint is configured
+ 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);
+ }
+
+ app.get("/", requestHandler);
+
+ const server = await listen(app);
+
+ await request(`http://localhost:${server.address().port}`);
+ const message = await testEnv.nextRequest();
+
+ 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],
+ );
+
+ // line numbers correspond to the calls to `addBreadcrumb` in this test
+ // update accordingly if they change
+ deepEqual(
+ message.details.breadcrumbs.map((b) => b.lineNumber),
+ [undefined, 38, 40],
+ );
+
+ t.end();
+});
+
+test("capturing breadcrumbs in different contexts", async function (t) {
+ const app = express();
+ const testEnv = await makeClientWithMockServer();
+ const raygun = testEnv.client;
+
+ // Must be defined before any endpoint is configured
+ 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);
+ });
+
+ 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();
+
+ // 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"],
+ );
+
+ 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();
+});
+
+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();
+});
+
+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();
+});