@drawbridge/drawbridge-telemetry 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @drawbridge/drawbridge-telemetry
2
+
3
+ Shared observability helpers for the drawbridge-* monorepo. Provides Sentry scope correlation across HTTP, BullMQ workers, and MongoDB change streams. Pino logger + event emitter arrive in Stage 2; OpenTelemetry SDK in Stage 4.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install --save-exact @drawbridge/drawbridge-telemetry
9
+ ```
10
+
11
+ Requires `@sentry/core` >= 10 as a peer dependency. Backend services satisfy this via `@sentry/node`; Next.js apps via `@sentry/nextjs`.
12
+
13
+ ## Subpaths
14
+
15
+ | Subpath | Purpose |
16
+ | --- | --- |
17
+ | `@drawbridge/drawbridge-telemetry` | Core: `attachProcessHandlers`, `withTraceScope`, `currentTraceId` |
18
+ | `@drawbridge/drawbridge-telemetry/express` | `expressContextMiddleware` for Express apps |
19
+ | `@drawbridge/drawbridge-telemetry/bullmq` | `wrapWorkerHandler`, `enqueueFromWorker`, `attachQueueEventsLogger` |
20
+ | `@drawbridge/drawbridge-telemetry/stream` | `enqueueWithTrace` for MongoDB change-stream handlers |
21
+
22
+ ## Naming conventions
23
+
24
+ Codified in the plan at `~/.claude/plans/yes-lets-see-it-enumerated-bumblebee.md`. Summary:
25
+
26
+ - **Event names** — `<noun>.<verb>` past tense, lowercase, dot-separated. Verbs match drawbridge-api's action status vocabulary (`processing` / `succeeded` / `failed`; never `pending` / `completed`). Nouns are singular and match Mongo collection names.
27
+ - **Tag keys** — camelCase. Canonical `traceId` everywhere; `X-Request-Id` header stays for wire compat.
28
+ - **Event envelope** — `{ name, traceId, userId, organizationId, data: {...}, createdAt }`.
29
+ - **Reserved keys in `job.data`** — `__traceId` (double-underscore prefix marks it as infrastructure data, not domain data).
30
+
31
+ ## TraceId flow
32
+
33
+ ```
34
+ HTTP request Mongo change event
35
+ │ │
36
+ ▼ req.id (randomUUID) ▼ _id._data (resume token)
37
+ expressContextMiddleware enqueueWithTrace
38
+ │ │
39
+ │ scope.setTag('traceId',_) │ job.data.__traceId
40
+ ▼ ▼
41
+ Sentry events for request BullMQ worker
42
+ │ wrapWorkerHandler reads
43
+ │ job.data.__traceId
44
+
45
+ scope.setTag('traceId',_)
46
+
47
+
48
+ Downstream enqueues
49
+ via enqueueFromWorker
50
+ forward active scope's traceId
51
+ ```
52
+
53
+ For cron-style repeatable BullMQ jobs (no parent request or change event), synthesize a `__traceId` at enqueue time — pass it via `data.__traceId` to `enqueueFromWorker`.
54
+
55
+ ## Build
56
+
57
+ ```sh
58
+ npm run build
59
+ ```
60
+
61
+ Builds CJS + ESM bundles into `dist/` and publishes. Bump `version` in package.json before publishing.
@@ -0,0 +1,108 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // bullmq.js
30
+ var bullmq_exports = {};
31
+ __export(bullmq_exports, {
32
+ attachQueueEventsLogger: () => attachQueueEventsLogger,
33
+ enqueueFromWorker: () => enqueueFromWorker,
34
+ wrapWorkerHandler: () => wrapWorkerHandler
35
+ });
36
+ module.exports = __toCommonJS(bullmq_exports);
37
+ var Sentry2 = __toESM(require("@sentry/core"), 1);
38
+
39
+ // index.js
40
+ var Sentry = __toESM(require("@sentry/core"), 1);
41
+ var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
42
+ if (traceId) scope.setTag("traceId", traceId);
43
+ return fn(scope);
44
+ });
45
+ var currentTraceId = () => {
46
+ var _a, _b;
47
+ const scope = Sentry.getCurrentScope();
48
+ const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
49
+ return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
50
+ };
51
+
52
+ // bullmq.js
53
+ var wrapWorkerHandler = (handler) => async (jobData, job) => {
54
+ var _a;
55
+ const traceId = (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId;
56
+ return withTraceScope(traceId, (scope) => {
57
+ if (job == null ? void 0 : job.queueName) {
58
+ scope.setTag("queue", job.queueName);
59
+ }
60
+ ;
61
+ if (job == null ? void 0 : job.name) {
62
+ scope.setTag("jobName", job.name);
63
+ }
64
+ ;
65
+ return handler(jobData, job);
66
+ });
67
+ };
68
+ var enqueueFromWorker = async (queue, name, data, options = {}) => {
69
+ const traceId = currentTraceId();
70
+ return queue.add(
71
+ name,
72
+ {
73
+ ...data,
74
+ __traceId: (data == null ? void 0 : data.__traceId) || traceId
75
+ },
76
+ options
77
+ );
78
+ };
79
+ var attachQueueEventsLogger = (queueEvents, queueName, logger) => {
80
+ if (!(logger == null ? void 0 : logger.event)) return;
81
+ queueEvents.on("completed", ({ jobId, returnvalue, prev }) => {
82
+ logger.event("job.completed", {
83
+ jobId,
84
+ queue: queueName,
85
+ prev
86
+ });
87
+ });
88
+ queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
89
+ logger.event("job.failed", {
90
+ jobId,
91
+ queue: queueName,
92
+ failedReason,
93
+ prev
94
+ });
95
+ });
96
+ queueEvents.on("stalled", ({ jobId }) => {
97
+ logger.event("job.stalled", {
98
+ jobId,
99
+ queue: queueName
100
+ });
101
+ });
102
+ };
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ attachQueueEventsLogger,
106
+ enqueueFromWorker,
107
+ wrapWorkerHandler
108
+ });
@@ -0,0 +1,101 @@
1
+ import { currentTraceId, withTraceScope } from './index.cjs';
2
+ import '@sentry/core';
3
+
4
+ // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
5
+ // with the job's traceId, queue name, and job name. The wrapped handler
6
+ // keeps the same `( jobData, job )` signature the existing queue-factory
7
+ // uses, so worker code itself doesn't change.
8
+ //
9
+ // The traceId originates from one of two places:
10
+ // - HTTP request: the API's req.id is forwarded as job.data.__traceId
11
+ // - Change stream: the Mongo resume token (_id._data) is forwarded
12
+ // Either way, the wrapper just reads job.data.__traceId.
13
+
14
+ const wrapWorkerHandler = ( handler ) => async ( jobData, job ) => {
15
+
16
+ const traceId = job?.data?.__traceId;
17
+
18
+ return withTraceScope( traceId, ( scope ) => {
19
+
20
+ if( job?.queueName ){
21
+
22
+ scope.setTag( 'queue', job.queueName );
23
+
24
+ }
25
+ if( job?.name ){
26
+
27
+ scope.setTag( 'jobName', job.name );
28
+
29
+ }
30
+ return handler( jobData, job );
31
+
32
+ } );
33
+
34
+ };
35
+
36
+ // Enqueue a downstream BullMQ job from inside a worker handler, forwarding
37
+ // the active scope's traceId as job.data.__traceId. Use this in place of
38
+ // `queue.add( ... )` inside any handler that creates more work.
39
+ //
40
+ // If no traceId is on the active scope (e.g. enqueuing outside any scope),
41
+ // the new job inherits no trace — downstream events won't correlate. This
42
+ // is the expected fallback for cron-style jobs; pass an explicit __traceId
43
+ // in `data` if you want to override the active scope's value.
44
+
45
+ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
46
+
47
+ const traceId = currentTraceId();
48
+
49
+ return queue.add(
50
+ name,
51
+ {
52
+ ...data,
53
+ __traceId : data?.__traceId || traceId
54
+ },
55
+ options
56
+ );
57
+
58
+ };
59
+
60
+ // Attach a QueueEvents listener that emits structured `job.completed` and
61
+ // `job.failed` records to the active Sentry logger. Use one per queue.
62
+ // Stage 2 only — Stage 1 ships the helper but it's a no-op until the
63
+ // unified logger lands.
64
+
65
+ const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
66
+
67
+ if( ! logger?.event ) return;
68
+
69
+ queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
70
+
71
+ logger.event( 'job.completed', {
72
+ jobId,
73
+ queue : queueName,
74
+ prev
75
+ });
76
+
77
+ });
78
+
79
+ queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
80
+
81
+ logger.event( 'job.failed', {
82
+ jobId,
83
+ queue : queueName,
84
+ failedReason,
85
+ prev
86
+ });
87
+
88
+ });
89
+
90
+ queueEvents.on( 'stalled', ({ jobId }) => {
91
+
92
+ logger.event( 'job.stalled', {
93
+ jobId,
94
+ queue : queueName
95
+ });
96
+
97
+ });
98
+
99
+ };
100
+
101
+ export { attachQueueEventsLogger, enqueueFromWorker, wrapWorkerHandler };
@@ -0,0 +1,101 @@
1
+ import { currentTraceId, withTraceScope } from './index.js';
2
+ import '@sentry/core';
3
+
4
+ // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
5
+ // with the job's traceId, queue name, and job name. The wrapped handler
6
+ // keeps the same `( jobData, job )` signature the existing queue-factory
7
+ // uses, so worker code itself doesn't change.
8
+ //
9
+ // The traceId originates from one of two places:
10
+ // - HTTP request: the API's req.id is forwarded as job.data.__traceId
11
+ // - Change stream: the Mongo resume token (_id._data) is forwarded
12
+ // Either way, the wrapper just reads job.data.__traceId.
13
+
14
+ const wrapWorkerHandler = ( handler ) => async ( jobData, job ) => {
15
+
16
+ const traceId = job?.data?.__traceId;
17
+
18
+ return withTraceScope( traceId, ( scope ) => {
19
+
20
+ if( job?.queueName ){
21
+
22
+ scope.setTag( 'queue', job.queueName );
23
+
24
+ }
25
+ if( job?.name ){
26
+
27
+ scope.setTag( 'jobName', job.name );
28
+
29
+ }
30
+ return handler( jobData, job );
31
+
32
+ } );
33
+
34
+ };
35
+
36
+ // Enqueue a downstream BullMQ job from inside a worker handler, forwarding
37
+ // the active scope's traceId as job.data.__traceId. Use this in place of
38
+ // `queue.add( ... )` inside any handler that creates more work.
39
+ //
40
+ // If no traceId is on the active scope (e.g. enqueuing outside any scope),
41
+ // the new job inherits no trace — downstream events won't correlate. This
42
+ // is the expected fallback for cron-style jobs; pass an explicit __traceId
43
+ // in `data` if you want to override the active scope's value.
44
+
45
+ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
46
+
47
+ const traceId = currentTraceId();
48
+
49
+ return queue.add(
50
+ name,
51
+ {
52
+ ...data,
53
+ __traceId : data?.__traceId || traceId
54
+ },
55
+ options
56
+ );
57
+
58
+ };
59
+
60
+ // Attach a QueueEvents listener that emits structured `job.completed` and
61
+ // `job.failed` records to the active Sentry logger. Use one per queue.
62
+ // Stage 2 only — Stage 1 ships the helper but it's a no-op until the
63
+ // unified logger lands.
64
+
65
+ const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
66
+
67
+ if( ! logger?.event ) return;
68
+
69
+ queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
70
+
71
+ logger.event( 'job.completed', {
72
+ jobId,
73
+ queue : queueName,
74
+ prev
75
+ });
76
+
77
+ });
78
+
79
+ queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
80
+
81
+ logger.event( 'job.failed', {
82
+ jobId,
83
+ queue : queueName,
84
+ failedReason,
85
+ prev
86
+ });
87
+
88
+ });
89
+
90
+ queueEvents.on( 'stalled', ({ jobId }) => {
91
+
92
+ logger.event( 'job.stalled', {
93
+ jobId,
94
+ queue : queueName
95
+ });
96
+
97
+ });
98
+
99
+ };
100
+
101
+ export { attachQueueEventsLogger, enqueueFromWorker, wrapWorkerHandler };
package/dist/bullmq.js ADDED
@@ -0,0 +1,62 @@
1
+ import {
2
+ currentTraceId,
3
+ withTraceScope
4
+ } from "./chunk-SRV5HFQT.js";
5
+
6
+ // bullmq.js
7
+ import * as Sentry from "@sentry/core";
8
+ var wrapWorkerHandler = (handler) => async (jobData, job) => {
9
+ var _a;
10
+ const traceId = (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId;
11
+ return withTraceScope(traceId, (scope) => {
12
+ if (job == null ? void 0 : job.queueName) {
13
+ scope.setTag("queue", job.queueName);
14
+ }
15
+ ;
16
+ if (job == null ? void 0 : job.name) {
17
+ scope.setTag("jobName", job.name);
18
+ }
19
+ ;
20
+ return handler(jobData, job);
21
+ });
22
+ };
23
+ var enqueueFromWorker = async (queue, name, data, options = {}) => {
24
+ const traceId = currentTraceId();
25
+ return queue.add(
26
+ name,
27
+ {
28
+ ...data,
29
+ __traceId: (data == null ? void 0 : data.__traceId) || traceId
30
+ },
31
+ options
32
+ );
33
+ };
34
+ var attachQueueEventsLogger = (queueEvents, queueName, logger) => {
35
+ if (!(logger == null ? void 0 : logger.event)) return;
36
+ queueEvents.on("completed", ({ jobId, returnvalue, prev }) => {
37
+ logger.event("job.completed", {
38
+ jobId,
39
+ queue: queueName,
40
+ prev
41
+ });
42
+ });
43
+ queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
44
+ logger.event("job.failed", {
45
+ jobId,
46
+ queue: queueName,
47
+ failedReason,
48
+ prev
49
+ });
50
+ });
51
+ queueEvents.on("stalled", ({ jobId }) => {
52
+ logger.event("job.stalled", {
53
+ jobId,
54
+ queue: queueName
55
+ });
56
+ });
57
+ };
58
+ export {
59
+ attachQueueEventsLogger,
60
+ enqueueFromWorker,
61
+ wrapWorkerHandler
62
+ };
@@ -0,0 +1,50 @@
1
+ // index.js
2
+ import * as Sentry from "@sentry/core";
3
+ var attached = false;
4
+ var attachProcessHandlers = () => {
5
+ if (attached) return;
6
+ attached = true;
7
+ process.on("uncaughtException", (error) => {
8
+ Sentry.captureException(error, {
9
+ extra: {
10
+ source: "uncaughtException"
11
+ }
12
+ });
13
+ });
14
+ process.on("unhandledRejection", (reason) => {
15
+ const error = reason instanceof Error ? reason : new Error(String(reason));
16
+ Sentry.captureException(error, {
17
+ extra: {
18
+ source: "unhandledRejection"
19
+ }
20
+ });
21
+ });
22
+ process.on("exit", (code) => {
23
+ console.error("[process exit] code=" + code);
24
+ });
25
+ process.on("beforeExit", (code) => {
26
+ console.error("[process beforeExit] code=" + code + " \u2014 event loop empty");
27
+ });
28
+ const signals = ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT", "SIGUSR2"];
29
+ signals.forEach((signal) => {
30
+ process.on(signal, () => {
31
+ console.error("[process signal] " + signal + " received");
32
+ });
33
+ });
34
+ };
35
+ var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
36
+ if (traceId) scope.setTag("traceId", traceId);
37
+ return fn(scope);
38
+ });
39
+ var currentTraceId = () => {
40
+ var _a, _b;
41
+ const scope = Sentry.getCurrentScope();
42
+ const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
43
+ return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
44
+ };
45
+
46
+ export {
47
+ attachProcessHandlers,
48
+ withTraceScope,
49
+ currentTraceId
50
+ };
@@ -0,0 +1,66 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // express.js
30
+ var express_exports = {};
31
+ __export(express_exports, {
32
+ expressContextMiddleware: () => expressContextMiddleware
33
+ });
34
+ module.exports = __toCommonJS(express_exports);
35
+ var Sentry = __toESM(require("@sentry/core"), 1);
36
+ var expressContextMiddleware = () => (req, res, next) => {
37
+ const scope = Sentry.getCurrentScope();
38
+ if (req == null ? void 0 : req.id) {
39
+ scope.setTag("traceId", req.id);
40
+ }
41
+ ;
42
+ if (req == null ? void 0 : req.user) {
43
+ scope.setUser({
44
+ id: req.user.id || req.user._id,
45
+ email: req.user.email
46
+ });
47
+ }
48
+ ;
49
+ if (req == null ? void 0 : req.organization) {
50
+ scope.setTag("organization", req.organization.id || req.organization._id);
51
+ }
52
+ ;
53
+ if (req == null ? void 0 : req.method) {
54
+ scope.setTag("method", req.method);
55
+ }
56
+ ;
57
+ if (req == null ? void 0 : req.path) {
58
+ scope.setTag("route", req.path);
59
+ }
60
+ ;
61
+ next();
62
+ };
63
+ // Annotate the CommonJS export names for ESM import in node:
64
+ 0 && (module.exports = {
65
+ expressContextMiddleware
66
+ });
@@ -0,0 +1,53 @@
1
+ import * as Sentry from '@sentry/core';
2
+
3
+ // Express middleware that tags the active Sentry scope with the current
4
+ // user/organization/route/method/traceId. Mount AFTER auth resolution (so
5
+ // req.user and req.organization are populated) and AFTER any req.id
6
+ // middleware (so req.id exists to use as the traceId).
7
+ //
8
+ // Every Sentry event captured during this request — and any downstream
9
+ // BullMQ jobs that propagate the traceId via job.data.__traceId — is
10
+ // filterable by these tags. Without this, errors arrive in Sentry as
11
+ // anonymous stack traces.
12
+
13
+ const expressContextMiddleware = () => ( req, res, next ) => {
14
+
15
+ const scope = Sentry.getCurrentScope();
16
+
17
+ if( req?.id ){
18
+
19
+ scope.setTag( 'traceId', req.id );
20
+
21
+ }
22
+ if( req?.user ){
23
+
24
+ scope.setUser({
25
+ id : req.user.id || req.user._id,
26
+ email : req.user.email
27
+ });
28
+
29
+ }
30
+ if( req?.organization ){
31
+
32
+ scope.setTag( 'organization', req.organization.id || req.organization._id );
33
+
34
+ }
35
+ if( req?.method ){
36
+
37
+ scope.setTag( 'method', req.method );
38
+
39
+ }
40
+ // req.route is set by Express only AFTER route matching, which hasn't
41
+ // happened yet at middleware time. Use req.path as a best-effort route
42
+ // tag — it captures the URL hit; aggregation by route pattern is left
43
+ // to a future res.on('finish') refinement.
44
+ if( req?.path ){
45
+
46
+ scope.setTag( 'route', req.path );
47
+
48
+ }
49
+ next();
50
+
51
+ };
52
+
53
+ export { expressContextMiddleware };
@@ -0,0 +1,53 @@
1
+ import * as Sentry from '@sentry/core';
2
+
3
+ // Express middleware that tags the active Sentry scope with the current
4
+ // user/organization/route/method/traceId. Mount AFTER auth resolution (so
5
+ // req.user and req.organization are populated) and AFTER any req.id
6
+ // middleware (so req.id exists to use as the traceId).
7
+ //
8
+ // Every Sentry event captured during this request — and any downstream
9
+ // BullMQ jobs that propagate the traceId via job.data.__traceId — is
10
+ // filterable by these tags. Without this, errors arrive in Sentry as
11
+ // anonymous stack traces.
12
+
13
+ const expressContextMiddleware = () => ( req, res, next ) => {
14
+
15
+ const scope = Sentry.getCurrentScope();
16
+
17
+ if( req?.id ){
18
+
19
+ scope.setTag( 'traceId', req.id );
20
+
21
+ }
22
+ if( req?.user ){
23
+
24
+ scope.setUser({
25
+ id : req.user.id || req.user._id,
26
+ email : req.user.email
27
+ });
28
+
29
+ }
30
+ if( req?.organization ){
31
+
32
+ scope.setTag( 'organization', req.organization.id || req.organization._id );
33
+
34
+ }
35
+ if( req?.method ){
36
+
37
+ scope.setTag( 'method', req.method );
38
+
39
+ }
40
+ // req.route is set by Express only AFTER route matching, which hasn't
41
+ // happened yet at middleware time. Use req.path as a best-effort route
42
+ // tag — it captures the URL hit; aggregation by route pattern is left
43
+ // to a future res.on('finish') refinement.
44
+ if( req?.path ){
45
+
46
+ scope.setTag( 'route', req.path );
47
+
48
+ }
49
+ next();
50
+
51
+ };
52
+
53
+ export { expressContextMiddleware };
@@ -0,0 +1,32 @@
1
+ // express.js
2
+ import * as Sentry from "@sentry/core";
3
+ var expressContextMiddleware = () => (req, res, next) => {
4
+ const scope = Sentry.getCurrentScope();
5
+ if (req == null ? void 0 : req.id) {
6
+ scope.setTag("traceId", req.id);
7
+ }
8
+ ;
9
+ if (req == null ? void 0 : req.user) {
10
+ scope.setUser({
11
+ id: req.user.id || req.user._id,
12
+ email: req.user.email
13
+ });
14
+ }
15
+ ;
16
+ if (req == null ? void 0 : req.organization) {
17
+ scope.setTag("organization", req.organization.id || req.organization._id);
18
+ }
19
+ ;
20
+ if (req == null ? void 0 : req.method) {
21
+ scope.setTag("method", req.method);
22
+ }
23
+ ;
24
+ if (req == null ? void 0 : req.path) {
25
+ scope.setTag("route", req.path);
26
+ }
27
+ ;
28
+ next();
29
+ };
30
+ export {
31
+ expressContextMiddleware
32
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,85 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // index.js
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ attachProcessHandlers: () => attachProcessHandlers,
33
+ currentTraceId: () => currentTraceId,
34
+ withTraceScope: () => withTraceScope
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var Sentry = __toESM(require("@sentry/core"), 1);
38
+ var attached = false;
39
+ var attachProcessHandlers = () => {
40
+ if (attached) return;
41
+ attached = true;
42
+ process.on("uncaughtException", (error) => {
43
+ Sentry.captureException(error, {
44
+ extra: {
45
+ source: "uncaughtException"
46
+ }
47
+ });
48
+ });
49
+ process.on("unhandledRejection", (reason) => {
50
+ const error = reason instanceof Error ? reason : new Error(String(reason));
51
+ Sentry.captureException(error, {
52
+ extra: {
53
+ source: "unhandledRejection"
54
+ }
55
+ });
56
+ });
57
+ process.on("exit", (code) => {
58
+ console.error("[process exit] code=" + code);
59
+ });
60
+ process.on("beforeExit", (code) => {
61
+ console.error("[process beforeExit] code=" + code + " \u2014 event loop empty");
62
+ });
63
+ const signals = ["SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT", "SIGUSR2"];
64
+ signals.forEach((signal) => {
65
+ process.on(signal, () => {
66
+ console.error("[process signal] " + signal + " received");
67
+ });
68
+ });
69
+ };
70
+ var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
71
+ if (traceId) scope.setTag("traceId", traceId);
72
+ return fn(scope);
73
+ });
74
+ var currentTraceId = () => {
75
+ var _a, _b;
76
+ const scope = Sentry.getCurrentScope();
77
+ const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
78
+ return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
79
+ };
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ attachProcessHandlers,
83
+ currentTraceId,
84
+ withTraceScope
85
+ });
@@ -0,0 +1,93 @@
1
+ import * as Sentry from '@sentry/core';
2
+
3
+ // Diagnostic process-level handlers. Surfaces unhandled async errors to
4
+ // Sentry (without these, modern Node defaults silently exit) and logs
5
+ // exit-path signals to stderr so an operator can diagnose crashes.
6
+ // Does NOT trigger shutdown — each service owns its shutdown logic.
7
+ //
8
+ // Idempotent: calling twice is a no-op so accidental double-registration
9
+ // during hot reloads or test setups doesn't stack listeners.
10
+
11
+ let attached = false;
12
+
13
+ const attachProcessHandlers = () => {
14
+
15
+ if( attached ) return;
16
+
17
+ attached = true;
18
+
19
+ process.on( 'uncaughtException', ( error ) => {
20
+
21
+ Sentry.captureException( error, {
22
+ extra : {
23
+ source : 'uncaughtException'
24
+ }
25
+ });
26
+
27
+ });
28
+
29
+ process.on( 'unhandledRejection', ( reason ) => {
30
+
31
+ const error = reason instanceof Error ? reason : new Error( String( reason ) );
32
+
33
+ Sentry.captureException( error, {
34
+ extra : {
35
+ source : 'unhandledRejection'
36
+ }
37
+ });
38
+
39
+ });
40
+
41
+ // Sentry can't flush from here — V8 is tearing down. stderr only.
42
+ process.on( 'exit', ( code ) => {
43
+
44
+ console.error( '[process exit] code=' + code );
45
+
46
+ });
47
+
48
+ process.on( 'beforeExit', ( code ) => {
49
+
50
+ console.error( '[process beforeExit] code=' + code + ' — event loop empty' );
51
+
52
+ });
53
+
54
+ const signals = [ 'SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR2' ];
55
+
56
+ signals.forEach( ( signal ) => {
57
+
58
+ process.on( signal, () => {
59
+
60
+ console.error( '[process signal] ' + signal + ' received' );
61
+
62
+ });
63
+
64
+ } );
65
+
66
+ };
67
+
68
+ // Run fn inside a Sentry scope with traceId tag set. The callback receives
69
+ // the forked scope so callers can attach additional tags without re-reading
70
+ // the active scope. Returns whatever fn returns (including promises).
71
+
72
+ const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
73
+
74
+ if( traceId ) scope.setTag( 'traceId', traceId );
75
+
76
+ return fn( scope );
77
+
78
+ } );
79
+
80
+ // Read the active scope's traceId. Used by helpers that need to forward
81
+ // the current request/job's trace into downstream work (e.g. enqueuing a
82
+ // new BullMQ job from inside a worker handler).
83
+
84
+ const currentTraceId = () => {
85
+
86
+ const scope = Sentry.getCurrentScope();
87
+ const data = scope?.getScopeData?.();
88
+
89
+ return data?.tags?.traceId;
90
+
91
+ };
92
+
93
+ export { attachProcessHandlers, currentTraceId, withTraceScope };
@@ -0,0 +1,93 @@
1
+ import * as Sentry from '@sentry/core';
2
+
3
+ // Diagnostic process-level handlers. Surfaces unhandled async errors to
4
+ // Sentry (without these, modern Node defaults silently exit) and logs
5
+ // exit-path signals to stderr so an operator can diagnose crashes.
6
+ // Does NOT trigger shutdown — each service owns its shutdown logic.
7
+ //
8
+ // Idempotent: calling twice is a no-op so accidental double-registration
9
+ // during hot reloads or test setups doesn't stack listeners.
10
+
11
+ let attached = false;
12
+
13
+ const attachProcessHandlers = () => {
14
+
15
+ if( attached ) return;
16
+
17
+ attached = true;
18
+
19
+ process.on( 'uncaughtException', ( error ) => {
20
+
21
+ Sentry.captureException( error, {
22
+ extra : {
23
+ source : 'uncaughtException'
24
+ }
25
+ });
26
+
27
+ });
28
+
29
+ process.on( 'unhandledRejection', ( reason ) => {
30
+
31
+ const error = reason instanceof Error ? reason : new Error( String( reason ) );
32
+
33
+ Sentry.captureException( error, {
34
+ extra : {
35
+ source : 'unhandledRejection'
36
+ }
37
+ });
38
+
39
+ });
40
+
41
+ // Sentry can't flush from here — V8 is tearing down. stderr only.
42
+ process.on( 'exit', ( code ) => {
43
+
44
+ console.error( '[process exit] code=' + code );
45
+
46
+ });
47
+
48
+ process.on( 'beforeExit', ( code ) => {
49
+
50
+ console.error( '[process beforeExit] code=' + code + ' — event loop empty' );
51
+
52
+ });
53
+
54
+ const signals = [ 'SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGUSR2' ];
55
+
56
+ signals.forEach( ( signal ) => {
57
+
58
+ process.on( signal, () => {
59
+
60
+ console.error( '[process signal] ' + signal + ' received' );
61
+
62
+ });
63
+
64
+ } );
65
+
66
+ };
67
+
68
+ // Run fn inside a Sentry scope with traceId tag set. The callback receives
69
+ // the forked scope so callers can attach additional tags without re-reading
70
+ // the active scope. Returns whatever fn returns (including promises).
71
+
72
+ const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
73
+
74
+ if( traceId ) scope.setTag( 'traceId', traceId );
75
+
76
+ return fn( scope );
77
+
78
+ } );
79
+
80
+ // Read the active scope's traceId. Used by helpers that need to forward
81
+ // the current request/job's trace into downstream work (e.g. enqueuing a
82
+ // new BullMQ job from inside a worker handler).
83
+
84
+ const currentTraceId = () => {
85
+
86
+ const scope = Sentry.getCurrentScope();
87
+ const data = scope?.getScopeData?.();
88
+
89
+ return data?.tags?.traceId;
90
+
91
+ };
92
+
93
+ export { attachProcessHandlers, currentTraceId, withTraceScope };
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ attachProcessHandlers,
3
+ currentTraceId,
4
+ withTraceScope
5
+ } from "./chunk-SRV5HFQT.js";
6
+ export {
7
+ attachProcessHandlers,
8
+ currentTraceId,
9
+ withTraceScope
10
+ };
@@ -0,0 +1,39 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // stream.js
20
+ var stream_exports = {};
21
+ __export(stream_exports, {
22
+ enqueueWithTrace: () => enqueueWithTrace
23
+ });
24
+ module.exports = __toCommonJS(stream_exports);
25
+ var enqueueWithTrace = async (queue, name, data, resumeToken, options = {}) => {
26
+ const traceId = (resumeToken == null ? void 0 : resumeToken._data) || String(resumeToken);
27
+ return queue.add(
28
+ name,
29
+ {
30
+ ...data,
31
+ __traceId: traceId
32
+ },
33
+ options
34
+ );
35
+ };
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ enqueueWithTrace
39
+ });
@@ -0,0 +1,28 @@
1
+ // Enqueue a BullMQ job from a change-stream handler, forwarding the
2
+ // MongoDB change-event resume token as the __traceId. The resume token
3
+ // (_id._data) is identical for every cursor observing the same oplog
4
+ // event, so all sync replicas compute the same traceId — combined with
5
+ // the BullMQ jobId derived from the same token, this gives both per-event
6
+ // dedup (jobId) AND per-event trace correlation (traceId) from a single
7
+ // source of truth.
8
+ //
9
+ // Callers retain control of the jobId via `options` — the resume-token-as-
10
+ // jobId convention lives in stream.js, not here, because it depends on the
11
+ // caller's choice of collection/operation namespace.
12
+
13
+ const enqueueWithTrace = async ( queue, name, data, resumeToken, options = {} ) => {
14
+
15
+ const traceId = resumeToken?._data || String( resumeToken );
16
+
17
+ return queue.add(
18
+ name,
19
+ {
20
+ ...data,
21
+ __traceId : traceId
22
+ },
23
+ options
24
+ );
25
+
26
+ };
27
+
28
+ export { enqueueWithTrace };
@@ -0,0 +1,28 @@
1
+ // Enqueue a BullMQ job from a change-stream handler, forwarding the
2
+ // MongoDB change-event resume token as the __traceId. The resume token
3
+ // (_id._data) is identical for every cursor observing the same oplog
4
+ // event, so all sync replicas compute the same traceId — combined with
5
+ // the BullMQ jobId derived from the same token, this gives both per-event
6
+ // dedup (jobId) AND per-event trace correlation (traceId) from a single
7
+ // source of truth.
8
+ //
9
+ // Callers retain control of the jobId via `options` — the resume-token-as-
10
+ // jobId convention lives in stream.js, not here, because it depends on the
11
+ // caller's choice of collection/operation namespace.
12
+
13
+ const enqueueWithTrace = async ( queue, name, data, resumeToken, options = {} ) => {
14
+
15
+ const traceId = resumeToken?._data || String( resumeToken );
16
+
17
+ return queue.add(
18
+ name,
19
+ {
20
+ ...data,
21
+ __traceId : traceId
22
+ },
23
+ options
24
+ );
25
+
26
+ };
27
+
28
+ export { enqueueWithTrace };
package/dist/stream.js ADDED
@@ -0,0 +1,15 @@
1
+ // stream.js
2
+ var enqueueWithTrace = async (queue, name, data, resumeToken, options = {}) => {
3
+ const traceId = (resumeToken == null ? void 0 : resumeToken._data) || String(resumeToken);
4
+ return queue.add(
5
+ name,
6
+ {
7
+ ...data,
8
+ __traceId: traceId
9
+ },
10
+ options
11
+ );
12
+ };
13
+ export {
14
+ enqueueWithTrace
15
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "type": "module",
3
+ "dependencies": {
4
+ "tsup": "8.5.1",
5
+ "typescript": "5.9.3"
6
+ },
7
+ "peerDependencies": {
8
+ "@sentry/core": ">=10"
9
+ },
10
+ "peerDependenciesMeta": {
11
+ "@sentry/core": {
12
+ "optional": false
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@sentry/core": "10.27.0"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./express": {
25
+ "types": "./dist/express.d.ts",
26
+ "import": "./dist/express.js",
27
+ "require": "./dist/express.cjs"
28
+ },
29
+ "./bullmq": {
30
+ "types": "./dist/bullmq.d.ts",
31
+ "import": "./dist/bullmq.js",
32
+ "require": "./dist/bullmq.cjs"
33
+ },
34
+ "./stream": {
35
+ "types": "./dist/stream.d.ts",
36
+ "import": "./dist/stream.js",
37
+ "require": "./dist/stream.cjs"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "license": "ISC",
44
+ "main": "dist/index.cjs",
45
+ "module": "dist/index.js",
46
+ "name": "@drawbridge/drawbridge-telemetry",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "sync": ". \"$HOME/.nvm/nvm.sh\" && nvm use && npm prune && npm install",
52
+ "build": "tsup && npm publish"
53
+ },
54
+ "types": "dist/index.d.ts",
55
+ "version": "0.0.1"
56
+ }