@drawbridge/drawbridge-telemetry 0.0.7 → 0.0.9

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/dist/bullmq.cjs CHANGED
@@ -35,17 +35,18 @@ __export(bullmq_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(bullmq_exports);
37
37
  var import_crypto = require("crypto");
38
+ var Sentry2 = __toESM(require("@sentry/node"), 1);
38
39
 
39
40
  // index.js
40
41
  var Sentry = __toESM(require("@sentry/node"), 1);
41
42
  var import_pino = __toESM(require("pino"), 1);
42
- var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
43
+ var withTraceScope = (traceId, fn) => Sentry.withIsolationScope((scope) => {
43
44
  if (traceId) scope.setTag("traceId", traceId);
44
45
  return fn(scope);
45
46
  });
46
47
  var currentTraceId = () => {
47
48
  var _a, _b;
48
- const scope = Sentry.getCurrentScope();
49
+ const scope = Sentry.getIsolationScope();
49
50
  const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
50
51
  return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
51
52
  };
@@ -77,28 +78,66 @@ var enqueueFromWorker = async (queue, name, data, options = {}) => {
77
78
  options
78
79
  );
79
80
  };
80
- var attachQueueEventsLogger = (queueEvents, queueName, logger2) => {
81
- if (!(logger2 == null ? void 0 : logger2.event)) return;
82
- queueEvents.on("completed", ({ jobId, returnvalue, prev }) => {
83
- logger2.event("job.completed", {
84
- jobId,
85
- queue: queueName,
86
- prev
87
- });
81
+ var attachQueueEventsLogger = (queue, queueEvents, logger2) => {
82
+ if (!(logger2 == null ? void 0 : logger2.emit)) return;
83
+ const queueName = queue == null ? void 0 : queue.name;
84
+ const emitInScope = (name, data, traceId) => Sentry2.withIsolationScope((scope) => {
85
+ if (traceId) scope.setTag("traceId", traceId);
86
+ if (queueName) scope.setTag("queue", queueName);
87
+ return logger2.emit(name, data);
88
88
  });
89
- queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
90
- logger2.event("job.failed", {
91
- jobId,
92
- queue: queueName,
93
- failedReason,
94
- prev
95
- });
89
+ queueEvents.on("completed", async ({ jobId }) => {
90
+ var _a;
91
+ try {
92
+ const job = await queue.getJob(jobId).catch(() => null);
93
+ const duration = (job == null ? void 0 : job.finishedOn) && (job == null ? void 0 : job.processedOn) ? job.finishedOn - job.processedOn : null;
94
+ await emitInScope(
95
+ "job.completed",
96
+ {
97
+ jobId,
98
+ queue: queueName,
99
+ name: job == null ? void 0 : job.name,
100
+ duration,
101
+ attemptsMade: job == null ? void 0 : job.attemptsMade
102
+ },
103
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
104
+ );
105
+ } catch {
106
+ }
107
+ });
108
+ queueEvents.on("failed", async ({ jobId, failedReason }) => {
109
+ var _a;
110
+ try {
111
+ const job = await queue.getJob(jobId).catch(() => null);
112
+ await emitInScope(
113
+ "job.failed",
114
+ {
115
+ jobId,
116
+ queue: queueName,
117
+ name: job == null ? void 0 : job.name,
118
+ failedReason,
119
+ attemptsMade: job == null ? void 0 : job.attemptsMade
120
+ },
121
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
122
+ );
123
+ } catch {
124
+ }
96
125
  });
97
- queueEvents.on("stalled", ({ jobId }) => {
98
- logger2.event("job.stalled", {
99
- jobId,
100
- queue: queueName
101
- });
126
+ queueEvents.on("stalled", async ({ jobId }) => {
127
+ var _a;
128
+ try {
129
+ const job = await queue.getJob(jobId).catch(() => null);
130
+ await emitInScope(
131
+ "job.stalled",
132
+ {
133
+ jobId,
134
+ queue: queueName,
135
+ name: job == null ? void 0 : job.name
136
+ },
137
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
138
+ );
139
+ } catch {
140
+ }
102
141
  });
103
142
  };
104
143
  // Annotate the CommonJS export names for ESM import in node:
package/dist/bullmq.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import * as Sentry from '@sentry/node';
2
3
  import { currentTraceId, withTraceScope } from './index.cjs';
3
- import '@sentry/node';
4
4
  import 'pino';
5
5
 
6
6
  // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
@@ -61,42 +61,108 @@ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
61
61
 
62
62
  };
63
63
 
64
- // Attach a QueueEvents listener that emits structured `job.completed` and
65
- // `job.failed` records to the active Sentry logger. Use one per queue.
66
- // Stage 2 only Stage 1 ships the helper but it's a no-op until the
67
- // unified logger lands.
64
+ // Attach a QueueEvents listener that emits `job.completed`, `job.failed`,
65
+ // and `job.stalled` to the unified logger. Called once per (queue, events)
66
+ // pair, typically right after both are constructed.
67
+ //
68
+ // The Queue handle is required because BullMQ's QueueEvents payload doesn't
69
+ // include duration, attempts, or job name on its own — we look those up via
70
+ // queue.getJob( jobId ). The lookup is best-effort: jobs that have already
71
+ // been swept by removeOnComplete / removeOnFail return null, in which case
72
+ // the emit goes out with the fields we do have.
73
+ //
74
+ // duration is measured from worker pickup (processedOn) to finish
75
+ // (finishedOn), i.e. execution time. Queue-wait time (timestamp to
76
+ // processedOn) is not included; if that becomes interesting it can be
77
+ // added as a separate field.
78
+
79
+ const attachQueueEventsLogger = ( queue, queueEvents, logger ) => {
80
+
81
+ if( ! logger?.emit ) return;
82
+
83
+ const queueName = queue?.name;
68
84
 
69
- const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
85
+ // QueueEvents listeners fire as plain event-emitter callbacks they
86
+ // don't run inside any wrapWorkerHandler scope, so without this
87
+ // withIsolationScope wrapper the emit's envelope traceId would be
88
+ // null. Wrap each handler so the emitted job.* row carries the job's
89
+ // own __traceId (set by enqueueWithTrace / enqueueFromWorker at
90
+ // enqueue time).
91
+
92
+ const emitInScope = ( name, data, traceId ) => Sentry.withIsolationScope( ( scope ) => {
93
+
94
+ if( traceId ) scope.setTag( 'traceId', traceId );
95
+ if( queueName ) scope.setTag( 'queue', queueName );
96
+
97
+ return logger.emit( name, data );
98
+
99
+ } );
70
100
 
71
- if( ! logger?.event ) return;
101
+ queueEvents.on( 'completed', async ({ jobId }) => {
72
102
 
73
- queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
103
+ try {
74
104
 
75
- logger.event( 'job.completed', {
76
- jobId,
77
- queue : queueName,
78
- prev
79
- });
105
+ const job = await queue.getJob( jobId ).catch( () => null );
106
+
107
+ const duration = ( job?.finishedOn && job?.processedOn )
108
+ ? job.finishedOn - job.processedOn
109
+ : null;
110
+
111
+ await emitInScope(
112
+ 'job.completed',
113
+ {
114
+ jobId,
115
+ queue : queueName,
116
+ name : job?.name,
117
+ duration,
118
+ attemptsMade : job?.attemptsMade
119
+ },
120
+ job?.data?.__traceId
121
+ );
122
+
123
+ } catch {}
80
124
 
81
125
  });
82
126
 
83
- queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
127
+ queueEvents.on( 'failed', async ({ jobId, failedReason }) => {
128
+
129
+ try {
130
+
131
+ const job = await queue.getJob( jobId ).catch( () => null );
84
132
 
85
- logger.event( 'job.failed', {
86
- jobId,
87
- queue : queueName,
88
- failedReason,
89
- prev
90
- });
133
+ await emitInScope(
134
+ 'job.failed',
135
+ {
136
+ jobId,
137
+ queue : queueName,
138
+ name : job?.name,
139
+ failedReason,
140
+ attemptsMade : job?.attemptsMade
141
+ },
142
+ job?.data?.__traceId
143
+ );
144
+
145
+ } catch {}
91
146
 
92
147
  });
93
148
 
94
- queueEvents.on( 'stalled', ({ jobId }) => {
149
+ queueEvents.on( 'stalled', async ({ jobId }) => {
150
+
151
+ try {
152
+
153
+ const job = await queue.getJob( jobId ).catch( () => null );
154
+
155
+ await emitInScope(
156
+ 'job.stalled',
157
+ {
158
+ jobId,
159
+ queue : queueName,
160
+ name : job?.name
161
+ },
162
+ job?.data?.__traceId
163
+ );
95
164
 
96
- logger.event( 'job.stalled', {
97
- jobId,
98
- queue : queueName
99
- });
165
+ } catch {}
100
166
 
101
167
  });
102
168
 
package/dist/bullmq.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import * as Sentry from '@sentry/node';
2
3
  import { currentTraceId, withTraceScope } from './index.js';
3
- import '@sentry/node';
4
4
  import 'pino';
5
5
 
6
6
  // Wrap a BullMQ worker handler so it runs inside a Sentry scope tagged
@@ -61,42 +61,108 @@ const enqueueFromWorker = async ( queue, name, data, options = {} ) => {
61
61
 
62
62
  };
63
63
 
64
- // Attach a QueueEvents listener that emits structured `job.completed` and
65
- // `job.failed` records to the active Sentry logger. Use one per queue.
66
- // Stage 2 only Stage 1 ships the helper but it's a no-op until the
67
- // unified logger lands.
64
+ // Attach a QueueEvents listener that emits `job.completed`, `job.failed`,
65
+ // and `job.stalled` to the unified logger. Called once per (queue, events)
66
+ // pair, typically right after both are constructed.
67
+ //
68
+ // The Queue handle is required because BullMQ's QueueEvents payload doesn't
69
+ // include duration, attempts, or job name on its own — we look those up via
70
+ // queue.getJob( jobId ). The lookup is best-effort: jobs that have already
71
+ // been swept by removeOnComplete / removeOnFail return null, in which case
72
+ // the emit goes out with the fields we do have.
73
+ //
74
+ // duration is measured from worker pickup (processedOn) to finish
75
+ // (finishedOn), i.e. execution time. Queue-wait time (timestamp to
76
+ // processedOn) is not included; if that becomes interesting it can be
77
+ // added as a separate field.
78
+
79
+ const attachQueueEventsLogger = ( queue, queueEvents, logger ) => {
80
+
81
+ if( ! logger?.emit ) return;
82
+
83
+ const queueName = queue?.name;
68
84
 
69
- const attachQueueEventsLogger = ( queueEvents, queueName, logger ) => {
85
+ // QueueEvents listeners fire as plain event-emitter callbacks they
86
+ // don't run inside any wrapWorkerHandler scope, so without this
87
+ // withIsolationScope wrapper the emit's envelope traceId would be
88
+ // null. Wrap each handler so the emitted job.* row carries the job's
89
+ // own __traceId (set by enqueueWithTrace / enqueueFromWorker at
90
+ // enqueue time).
91
+
92
+ const emitInScope = ( name, data, traceId ) => Sentry.withIsolationScope( ( scope ) => {
93
+
94
+ if( traceId ) scope.setTag( 'traceId', traceId );
95
+ if( queueName ) scope.setTag( 'queue', queueName );
96
+
97
+ return logger.emit( name, data );
98
+
99
+ } );
70
100
 
71
- if( ! logger?.event ) return;
101
+ queueEvents.on( 'completed', async ({ jobId }) => {
72
102
 
73
- queueEvents.on( 'completed', ({ jobId, returnvalue, prev }) => {
103
+ try {
74
104
 
75
- logger.event( 'job.completed', {
76
- jobId,
77
- queue : queueName,
78
- prev
79
- });
105
+ const job = await queue.getJob( jobId ).catch( () => null );
106
+
107
+ const duration = ( job?.finishedOn && job?.processedOn )
108
+ ? job.finishedOn - job.processedOn
109
+ : null;
110
+
111
+ await emitInScope(
112
+ 'job.completed',
113
+ {
114
+ jobId,
115
+ queue : queueName,
116
+ name : job?.name,
117
+ duration,
118
+ attemptsMade : job?.attemptsMade
119
+ },
120
+ job?.data?.__traceId
121
+ );
122
+
123
+ } catch {}
80
124
 
81
125
  });
82
126
 
83
- queueEvents.on( 'failed', ({ jobId, failedReason, prev }) => {
127
+ queueEvents.on( 'failed', async ({ jobId, failedReason }) => {
128
+
129
+ try {
130
+
131
+ const job = await queue.getJob( jobId ).catch( () => null );
84
132
 
85
- logger.event( 'job.failed', {
86
- jobId,
87
- queue : queueName,
88
- failedReason,
89
- prev
90
- });
133
+ await emitInScope(
134
+ 'job.failed',
135
+ {
136
+ jobId,
137
+ queue : queueName,
138
+ name : job?.name,
139
+ failedReason,
140
+ attemptsMade : job?.attemptsMade
141
+ },
142
+ job?.data?.__traceId
143
+ );
144
+
145
+ } catch {}
91
146
 
92
147
  });
93
148
 
94
- queueEvents.on( 'stalled', ({ jobId }) => {
149
+ queueEvents.on( 'stalled', async ({ jobId }) => {
150
+
151
+ try {
152
+
153
+ const job = await queue.getJob( jobId ).catch( () => null );
154
+
155
+ await emitInScope(
156
+ 'job.stalled',
157
+ {
158
+ jobId,
159
+ queue : queueName,
160
+ name : job?.name
161
+ },
162
+ job?.data?.__traceId
163
+ );
95
164
 
96
- logger.event( 'job.stalled', {
97
- jobId,
98
- queue : queueName
99
- });
165
+ } catch {}
100
166
 
101
167
  });
102
168
 
package/dist/bullmq.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  currentTraceId,
3
3
  withTraceScope
4
- } from "./chunk-CODZHH5I.js";
4
+ } from "./chunk-WUUTBLMK.js";
5
5
 
6
6
  // bullmq.js
7
7
  import { randomUUID } from "crypto";
8
+ import * as Sentry from "@sentry/node";
8
9
  var wrapWorkerHandler = (handler) => async (job) => {
9
10
  var _a;
10
11
  const traceId = ((_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId) || randomUUID();
@@ -31,28 +32,66 @@ var enqueueFromWorker = async (queue, name, data, options = {}) => {
31
32
  options
32
33
  );
33
34
  };
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
- });
35
+ var attachQueueEventsLogger = (queue, queueEvents, logger) => {
36
+ if (!(logger == null ? void 0 : logger.emit)) return;
37
+ const queueName = queue == null ? void 0 : queue.name;
38
+ const emitInScope = (name, data, traceId) => Sentry.withIsolationScope((scope) => {
39
+ if (traceId) scope.setTag("traceId", traceId);
40
+ if (queueName) scope.setTag("queue", queueName);
41
+ return logger.emit(name, data);
42
42
  });
43
- queueEvents.on("failed", ({ jobId, failedReason, prev }) => {
44
- logger.event("job.failed", {
45
- jobId,
46
- queue: queueName,
47
- failedReason,
48
- prev
49
- });
43
+ queueEvents.on("completed", async ({ jobId }) => {
44
+ var _a;
45
+ try {
46
+ const job = await queue.getJob(jobId).catch(() => null);
47
+ const duration = (job == null ? void 0 : job.finishedOn) && (job == null ? void 0 : job.processedOn) ? job.finishedOn - job.processedOn : null;
48
+ await emitInScope(
49
+ "job.completed",
50
+ {
51
+ jobId,
52
+ queue: queueName,
53
+ name: job == null ? void 0 : job.name,
54
+ duration,
55
+ attemptsMade: job == null ? void 0 : job.attemptsMade
56
+ },
57
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
58
+ );
59
+ } catch {
60
+ }
61
+ });
62
+ queueEvents.on("failed", async ({ jobId, failedReason }) => {
63
+ var _a;
64
+ try {
65
+ const job = await queue.getJob(jobId).catch(() => null);
66
+ await emitInScope(
67
+ "job.failed",
68
+ {
69
+ jobId,
70
+ queue: queueName,
71
+ name: job == null ? void 0 : job.name,
72
+ failedReason,
73
+ attemptsMade: job == null ? void 0 : job.attemptsMade
74
+ },
75
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
76
+ );
77
+ } catch {
78
+ }
50
79
  });
51
- queueEvents.on("stalled", ({ jobId }) => {
52
- logger.event("job.stalled", {
53
- jobId,
54
- queue: queueName
55
- });
80
+ queueEvents.on("stalled", async ({ jobId }) => {
81
+ var _a;
82
+ try {
83
+ const job = await queue.getJob(jobId).catch(() => null);
84
+ await emitInScope(
85
+ "job.stalled",
86
+ {
87
+ jobId,
88
+ queue: queueName,
89
+ name: job == null ? void 0 : job.name
90
+ },
91
+ (_a = job == null ? void 0 : job.data) == null ? void 0 : _a.__traceId
92
+ );
93
+ } catch {
94
+ }
56
95
  });
57
96
  };
58
97
  export {
@@ -46,12 +46,12 @@ var createLogger = ({ eventWriter, level = "info" } = {}) => {
46
46
  }
47
47
  };
48
48
  const emit = async (name, data = {}) => {
49
- var _a, _b, _c, _d;
49
+ var _a, _b, _c, _d, _e;
50
50
  const scope = Sentry.getIsolationScope();
51
51
  const scopeData = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
52
- const traceId = (_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId;
53
- const userId = (_c = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _c.id;
54
- const organizationId = (_d = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _d.organization;
52
+ const traceId = ((_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId) || ((_c = data == null ? void 0 : data.telemetry) == null ? void 0 : _c.id) || null;
53
+ const userId = ((_d = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _d.id) || (data == null ? void 0 : data.userId) || (data == null ? void 0 : data.user) || null;
54
+ const organizationId = ((_e = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _e.organization) || (data == null ? void 0 : data.organizationId) || (data == null ? void 0 : data.organization) || null;
55
55
  log.info(
56
56
  { event: name, traceId, userId, organizationId, ...data },
57
57
  "event:" + name
@@ -99,13 +99,13 @@ var attachProcessHandlers = () => {
99
99
  });
100
100
  });
101
101
  };
102
- var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
102
+ var withTraceScope = (traceId, fn) => Sentry.withIsolationScope((scope) => {
103
103
  if (traceId) scope.setTag("traceId", traceId);
104
104
  return fn(scope);
105
105
  });
106
106
  var currentTraceId = () => {
107
107
  var _a, _b;
108
- const scope = Sentry.getCurrentScope();
108
+ const scope = Sentry.getIsolationScope();
109
109
  const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
110
110
  return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
111
111
  };
package/dist/index.cjs CHANGED
@@ -87,12 +87,12 @@ var createLogger = ({ eventWriter, level = "info" } = {}) => {
87
87
  }
88
88
  };
89
89
  const emit = async (name, data = {}) => {
90
- var _a, _b, _c, _d;
90
+ var _a, _b, _c, _d, _e;
91
91
  const scope = Sentry.getIsolationScope();
92
92
  const scopeData = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
93
- const traceId = (_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId;
94
- const userId = (_c = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _c.id;
95
- const organizationId = (_d = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _d.organization;
93
+ const traceId = ((_b = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _b.traceId) || ((_c = data == null ? void 0 : data.telemetry) == null ? void 0 : _c.id) || null;
94
+ const userId = ((_d = scopeData == null ? void 0 : scopeData.user) == null ? void 0 : _d.id) || (data == null ? void 0 : data.userId) || (data == null ? void 0 : data.user) || null;
95
+ const organizationId = ((_e = scopeData == null ? void 0 : scopeData.tags) == null ? void 0 : _e.organization) || (data == null ? void 0 : data.organizationId) || (data == null ? void 0 : data.organization) || null;
96
96
  log.info(
97
97
  { event: name, traceId, userId, organizationId, ...data },
98
98
  "event:" + name
@@ -140,13 +140,13 @@ var attachProcessHandlers = () => {
140
140
  });
141
141
  });
142
142
  };
143
- var withTraceScope = (traceId, fn) => Sentry.withScope((scope) => {
143
+ var withTraceScope = (traceId, fn) => Sentry.withIsolationScope((scope) => {
144
144
  if (traceId) scope.setTag("traceId", traceId);
145
145
  return fn(scope);
146
146
  });
147
147
  var currentTraceId = () => {
148
148
  var _a, _b;
149
- const scope = Sentry.getCurrentScope();
149
+ const scope = Sentry.getIsolationScope();
150
150
  const data = (_a = scope == null ? void 0 : scope.getScopeData) == null ? void 0 : _a.call(scope);
151
151
  return (_b = data == null ? void 0 : data.tags) == null ? void 0 : _b.traceId;
152
152
  };
package/dist/index.d.cts CHANGED
@@ -115,9 +115,26 @@ const createLogger = ({ eventWriter, level = 'info' } = {}) => {
115
115
  const scope = Sentry.getIsolationScope();
116
116
  const scopeData = scope?.getScopeData?.();
117
117
 
118
- const traceId = scopeData?.tags?.traceId;
119
- const userId = scopeData?.user?.id;
120
- const organizationId = scopeData?.tags?.organization;
118
+ // Envelope fields prefer the active Sentry isolation scope (set by
119
+ // expressContextMiddleware in Express requests, by withTraceScope in
120
+ // BullMQ workers). Change-stream-driven emits frequently don't have
121
+ // an org/user on the scope but DO carry them in the data payload —
122
+ // fall back to those so the envelope stays populated and admin
123
+ // queries by `organizationId`/`userId` still match.
124
+
125
+ const traceId = scopeData?.tags?.traceId
126
+ || data?.telemetry?.id
127
+ || null;
128
+
129
+ const userId = scopeData?.user?.id
130
+ || data?.userId
131
+ || data?.user
132
+ || null;
133
+
134
+ const organizationId = scopeData?.tags?.organization
135
+ || data?.organizationId
136
+ || data?.organization
137
+ || null;
121
138
 
122
139
  log.info(
123
140
  { event : name, traceId, userId, organizationId, ...data },
@@ -210,7 +227,16 @@ const attachProcessHandlers = () => {
210
227
  // the forked scope so callers can attach additional tags without re-reading
211
228
  // the active scope. Returns whatever fn returns (including promises).
212
229
 
213
- const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
230
+ // Runs `fn` inside a forked isolation scope tagged with `traceId`.
231
+ // Important: must use withIsolationScope, NOT withScope. `Sentry.withScope`
232
+ // forks the *current* scope, which is invisible to consumers that read
233
+ // from `Sentry.getIsolationScope()` — including our own logger.emit and
234
+ // the pinoIntegration that ships log lines with isolation-scope tags
235
+ // attached. Using the wrong scope was the original cause of every
236
+ // telemetry row showing traceId: null even when wrapWorkerHandler had
237
+ // "set" the tag.
238
+
239
+ const withTraceScope = ( traceId, fn ) => Sentry.withIsolationScope( ( scope ) => {
214
240
 
215
241
  if( traceId ) scope.setTag( 'traceId', traceId );
216
242
 
@@ -224,7 +250,12 @@ const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
224
250
 
225
251
  const currentTraceId = () => {
226
252
 
227
- const scope = Sentry.getCurrentScope();
253
+ // Read from the isolation scope that's where withTraceScope and
254
+ // expressContextMiddleware both write traceId. Falling back to
255
+ // current scope would only pick up tags set inside an explicit
256
+ // Sentry.withScope block, which nothing in this codebase does.
257
+
258
+ const scope = Sentry.getIsolationScope();
228
259
  const data = scope?.getScopeData?.();
229
260
 
230
261
  return data?.tags?.traceId;
package/dist/index.d.ts CHANGED
@@ -115,9 +115,26 @@ const createLogger = ({ eventWriter, level = 'info' } = {}) => {
115
115
  const scope = Sentry.getIsolationScope();
116
116
  const scopeData = scope?.getScopeData?.();
117
117
 
118
- const traceId = scopeData?.tags?.traceId;
119
- const userId = scopeData?.user?.id;
120
- const organizationId = scopeData?.tags?.organization;
118
+ // Envelope fields prefer the active Sentry isolation scope (set by
119
+ // expressContextMiddleware in Express requests, by withTraceScope in
120
+ // BullMQ workers). Change-stream-driven emits frequently don't have
121
+ // an org/user on the scope but DO carry them in the data payload —
122
+ // fall back to those so the envelope stays populated and admin
123
+ // queries by `organizationId`/`userId` still match.
124
+
125
+ const traceId = scopeData?.tags?.traceId
126
+ || data?.telemetry?.id
127
+ || null;
128
+
129
+ const userId = scopeData?.user?.id
130
+ || data?.userId
131
+ || data?.user
132
+ || null;
133
+
134
+ const organizationId = scopeData?.tags?.organization
135
+ || data?.organizationId
136
+ || data?.organization
137
+ || null;
121
138
 
122
139
  log.info(
123
140
  { event : name, traceId, userId, organizationId, ...data },
@@ -210,7 +227,16 @@ const attachProcessHandlers = () => {
210
227
  // the forked scope so callers can attach additional tags without re-reading
211
228
  // the active scope. Returns whatever fn returns (including promises).
212
229
 
213
- const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
230
+ // Runs `fn` inside a forked isolation scope tagged with `traceId`.
231
+ // Important: must use withIsolationScope, NOT withScope. `Sentry.withScope`
232
+ // forks the *current* scope, which is invisible to consumers that read
233
+ // from `Sentry.getIsolationScope()` — including our own logger.emit and
234
+ // the pinoIntegration that ships log lines with isolation-scope tags
235
+ // attached. Using the wrong scope was the original cause of every
236
+ // telemetry row showing traceId: null even when wrapWorkerHandler had
237
+ // "set" the tag.
238
+
239
+ const withTraceScope = ( traceId, fn ) => Sentry.withIsolationScope( ( scope ) => {
214
240
 
215
241
  if( traceId ) scope.setTag( 'traceId', traceId );
216
242
 
@@ -224,7 +250,12 @@ const withTraceScope = ( traceId, fn ) => Sentry.withScope( ( scope ) => {
224
250
 
225
251
  const currentTraceId = () => {
226
252
 
227
- const scope = Sentry.getCurrentScope();
253
+ // Read from the isolation scope that's where withTraceScope and
254
+ // expressContextMiddleware both write traceId. Falling back to
255
+ // current scope would only pick up tags set inside an explicit
256
+ // Sentry.withScope block, which nothing in this codebase does.
257
+
258
+ const scope = Sentry.getIsolationScope();
228
259
  const data = scope?.getScopeData?.();
229
260
 
230
261
  return data?.tags?.traceId;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  init,
9
9
  logger,
10
10
  withTraceScope
11
- } from "./chunk-CODZHH5I.js";
11
+ } from "./chunk-WUUTBLMK.js";
12
12
  export {
13
13
  Sentry,
14
14
  attachProcessHandlers,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "dependencies": {
4
- "@drawbridge/drawbridge-agents": "0.0.4",
4
+ "@drawbridge/drawbridge-agents": "0.0.5",
5
5
  "@sentry/node": "10.27.0",
6
6
  "pino": "10.3.1",
7
7
  "pino-pretty": "13.1.3"
@@ -60,5 +60,5 @@
60
60
  "build": "tsup && npm publish"
61
61
  },
62
62
  "types": "dist/index.d.ts",
63
- "version": "0.0.7"
63
+ "version": "0.0.9"
64
64
  }