@sentienguard/apm 1.0.22-debug.1 → 1.0.23

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 CHANGED
@@ -1,141 +1,177 @@
1
- # @sentienguard/apm
2
-
3
- Minimal, production-safe APM SDK for Node.js applications. Zero-config setup with automatic instrumentation.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @sentienguard/apm
9
- ```
10
-
11
- ## Quick Start
12
-
13
- ```js
14
- // If using dotenv, import it first
15
- import 'dotenv/config';
16
-
17
- // Then import the SDK (before other app modules for best instrumentation coverage)
18
- import '@sentienguard/apm';
19
-
20
- // Your app code
21
- import express from 'express';
22
- const app = express();
23
- // ...
24
- ```
25
-
26
- Set environment variables:
27
-
28
- ```bash
29
- SENTIENGUARD_APM_KEY=your-app-key
30
- SENTIENGUARD_SERVICE=my-api
31
- ```
32
-
33
- That's it. The SDK automatically instruments your application and sends metrics to SentienGuard.
34
-
35
- ## Configuration
36
-
37
- All configuration is via environment variables:
38
-
39
- | Variable | Required | Default | Description |
40
- |----------|----------|---------|-------------|
41
- | `SENTIENGUARD_APM_KEY` | Yes | - | Your application's APM key |
42
- | `SENTIENGUARD_SERVICE` | Yes | - | Service name (e.g., `orders-api`) |
43
- | `SENTIENGUARD_ENV` | No | `production` | Environment (`production`, `staging`, `development`) |
44
- | `SENTIENGUARD_ENDPOINT` | No | `https://sentienguard-dev.the-algo.com/api/v1` | SentienGuard backend URL |
45
- | `SENTIENGUARD_FLUSH_INTERVAL` | No | `10` | Metrics flush interval in seconds |
46
-
47
- > **Note:** If `SENTIENGUARD_APM_KEY` or `SENTIENGUARD_SERVICE` is missing, the SDK disables itself silently without affecting your application.
48
-
49
- ## What Gets Tracked
50
-
51
- - **HTTP Requests** - Incoming requests with method, route, status, and latency
52
- - **Dependencies** - Outgoing HTTP/HTTPS calls to external services
53
- - **Errors** - Uncaught exceptions and unhandled rejections
54
-
55
- ## Framework Integration
56
-
57
- ### Express
58
-
59
- For better route extraction, add the middleware:
60
-
61
- ```js
62
- import 'dotenv/config'; // if using dotenv
63
- import '@sentienguard/apm';
64
- import { expressMiddleware, expressErrorMiddleware } from '@sentienguard/apm';
65
- import express from 'express';
66
-
67
- const app = express();
68
-
69
- // Add early in middleware chain
70
- app.use(expressMiddleware());
71
-
72
- // Your routes
73
- app.get('/users/:id', (req, res) => { ... });
74
-
75
- // Add error middleware last
76
- app.use(expressErrorMiddleware());
77
- ```
78
-
79
- ### Fastify
80
-
81
- ```js
82
- import 'dotenv/config'; // if using dotenv
83
- import '@sentienguard/apm';
84
- import { fastifyPlugin, fastifyErrorHandler } from '@sentienguard/apm';
85
- import Fastify from 'fastify';
86
-
87
- const app = Fastify();
88
-
89
- // Register plugin
90
- app.register(fastifyPlugin);
91
-
92
- // Add error handler
93
- app.setErrorHandler(fastifyErrorHandler);
94
- ```
95
-
96
- ## API Reference
97
-
98
- ### Functions
99
-
100
- ```js
101
- import {
102
- shutdown, // Graceful shutdown (flushes pending metrics)
103
- getStatus, // Get SDK status and stats
104
- flush, // Force flush metrics now
105
- isEnabled // Check if SDK is enabled
106
- } from '@sentienguard/apm';
107
- ```
108
-
109
- ### Graceful Shutdown
110
-
111
- The SDK automatically handles `SIGTERM` and `SIGINT` signals. For manual shutdown:
112
-
113
- ```js
114
- import { shutdown } from '@sentienguard/apm';
115
-
116
- process.on('exit', async () => {
117
- await shutdown();
118
- });
119
- ```
120
-
121
- ### Check Status
122
-
123
- ```js
124
- import { getStatus } from '@sentienguard/apm';
125
-
126
- console.log(getStatus());
127
- // {
128
- // enabled: true,
129
- // initialized: true,
130
- // config: { service: 'my-api', environment: 'production', flushInterval: 10 },
131
- // stats: { requests: 150, dependencies: 45, errors: 2 }
132
- // }
133
- ```
134
-
135
- ## Requirements
136
-
137
- - Node.js >= 16.0.0
138
-
139
- ## License
140
-
141
- MIT
1
+ # @sentienguard/apm
2
+
3
+ Minimal, production-safe APM SDK for Node.js applications. Zero-config setup with automatic instrumentation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sentienguard/apm
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```js
14
+ // If using dotenv, import it first
15
+ import 'dotenv/config';
16
+
17
+ // Then import the SDK (before other app modules for best instrumentation coverage)
18
+ import '@sentienguard/apm';
19
+
20
+ // Your app code
21
+ import express from 'express';
22
+ const app = express();
23
+ // ...
24
+ ```
25
+
26
+ Set environment variables:
27
+
28
+ ```bash
29
+ SENTIENGUARD_APM_KEY=your-app-key
30
+ SENTIENGUARD_SERVICE=my-api
31
+ ```
32
+
33
+ That's it. The SDK automatically instruments your application and sends metrics to SentienGuard.
34
+
35
+ ## Configuration
36
+
37
+ All configuration is via environment variables:
38
+
39
+ | Variable | Required | Default | Description |
40
+ |----------|----------|---------|-------------|
41
+ | `SENTIENGUARD_APM_KEY` | Yes | - | Your application's APM key |
42
+ | `SENTIENGUARD_SERVICE` | Yes | - | Service name (e.g., `orders-api`) |
43
+ | `SENTIENGUARD_ENV` | No | `production` | Environment (`production`, `staging`, `development`) |
44
+ | `SENTIENGUARD_ENDPOINT` | No | `https://sentienguard-dev.the-algo.com/api/v1` | SentienGuard backend URL |
45
+ | `SENTIENGUARD_FLUSH_INTERVAL` | No | `10` | Metrics flush interval in seconds |
46
+
47
+ > **Note:** If `SENTIENGUARD_APM_KEY` or `SENTIENGUARD_SERVICE` is missing, the SDK disables itself silently without affecting your application.
48
+
49
+ ## What Gets Tracked
50
+
51
+ - **HTTP Requests** - Incoming requests with method, route, status, and latency
52
+ - **Dependencies** - Outgoing HTTP/HTTPS calls to external services
53
+ - **Errors** - Uncaught exceptions and unhandled rejections
54
+
55
+ ## Framework Integration
56
+
57
+ ### Express
58
+
59
+ For better route extraction, add the middleware:
60
+
61
+ ```js
62
+ import 'dotenv/config'; // if using dotenv
63
+ import '@sentienguard/apm';
64
+ import { expressMiddleware, expressErrorMiddleware } from '@sentienguard/apm';
65
+ import express from 'express';
66
+
67
+ const app = express();
68
+
69
+ // Add early in middleware chain
70
+ app.use(expressMiddleware());
71
+
72
+ // Your routes
73
+ app.get('/users/:id', (req, res) => { ... });
74
+
75
+ // Add error middleware last
76
+ app.use(expressErrorMiddleware());
77
+ ```
78
+
79
+ ### Fastify
80
+
81
+ ```js
82
+ import 'dotenv/config'; // if using dotenv
83
+ import '@sentienguard/apm';
84
+ import { fastifyPlugin, fastifyErrorHandler } from '@sentienguard/apm';
85
+ import Fastify from 'fastify';
86
+
87
+ const app = Fastify();
88
+
89
+ // Register plugin
90
+ app.register(fastifyPlugin);
91
+
92
+ // Add error handler
93
+ app.setErrorHandler(fastifyErrorHandler);
94
+ ```
95
+
96
+ ## API Reference
97
+
98
+ ### Functions
99
+
100
+ ```js
101
+ import {
102
+ shutdown, // Graceful shutdown (flushes pending metrics)
103
+ getStatus, // Get SDK status and stats
104
+ flush, // Force flush metrics now
105
+ isEnabled // Check if SDK is enabled
106
+ } from '@sentienguard/apm';
107
+ ```
108
+
109
+ ### Graceful Shutdown
110
+
111
+ The SDK automatically handles `SIGTERM` and `SIGINT` signals. For manual shutdown:
112
+
113
+ ```js
114
+ import { shutdown } from '@sentienguard/apm';
115
+
116
+ process.on('exit', async () => {
117
+ await shutdown();
118
+ });
119
+ ```
120
+
121
+ ### Check Status
122
+
123
+ ```js
124
+ import { getStatus } from '@sentienguard/apm';
125
+
126
+ console.log(getStatus());
127
+ // {
128
+ // enabled: true,
129
+ // initialized: true,
130
+ // config: { service: 'my-api', environment: 'production', flushInterval: 10 },
131
+ // stats: { requests: 150, dependencies: 45, errors: 2 }
132
+ // }
133
+ ```
134
+
135
+ ## Browser / Frontend Monitoring (RUM)
136
+
137
+ For React, Vue, or other browser apps, bundlers automatically use the browser build.
138
+ Call `init()` explicitly in your app entry (client-side only):
139
+
140
+ ```js
141
+ import SentienGuard from '@sentienguard/apm';
142
+
143
+ SentienGuard.init({
144
+ apiKey: 'your-apm-key',
145
+ service: 'my-frontend',
146
+ endpoint: 'https://your-backend.com/api/v1/apm/ingest',
147
+ environment: 'production'
148
+ });
149
+ ```
150
+
151
+ ### What Gets Tracked (Browser)
152
+
153
+ - **Outgoing fetch/XHR** — API calls from the browser with latency and errors
154
+ - **Core Web Vitals** — LCP, FID, INP, CLS
155
+ - **Page load timing** — TTFB, DOM ready, full load
156
+ - **JS errors** — Unhandled exceptions and promise rejections
157
+ - **SPA routes** — Client-side navigation via History API
158
+
159
+ ### Browser Configuration
160
+
161
+ | Option | Required | Default | Description |
162
+ |--------|----------|---------|-------------|
163
+ | `apiKey` | Yes | - | APM application key |
164
+ | `service` | Yes | - | Frontend service name |
165
+ | `endpoint` | No | SentienGuard dev ingest URL | Full ingest URL |
166
+ | `environment` | No | `production` | Environment name |
167
+ | `flushInterval` | No | `10` | Flush interval in seconds |
168
+ | `debug` | No | `false` | Enable SDK debug logging |
169
+
170
+ ## Requirements
171
+
172
+ - Node.js >= 16.0.0 (server SDK)
173
+ - Modern browser with PerformanceObserver support (browser SDK)
174
+
175
+ ## License
176
+
177
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentienguard/apm",
3
- "version": "1.0.22-debug.1",
3
+ "version": "1.0.23",
4
4
  "description": "SentienGuard APM SDK - Minimal, production-safe application performance monitoring",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -139,6 +139,29 @@ export class BrowserMetricsAggregator {
139
139
  });
140
140
  }
141
141
 
142
+ const webVitals = Object.keys(this.webVitals).length > 0
143
+ ? { ...this.webVitals }
144
+ : undefined;
145
+
146
+ const jsErrors = this.jsErrors > 0 ? this.jsErrors : undefined;
147
+
148
+ const routeChanges = this.routeChanges.length > 0
149
+ ? this.routeChanges.map(({ from, to, timestamp }) => ({ from, to, timestamp }))
150
+ : undefined;
151
+
152
+ const pageLoads = this.pageLoads.length > 0
153
+ ? this.pageLoads.map((timing) => ({
154
+ dns: Math.round(timing.dns || 0),
155
+ tcp: Math.round(timing.tcp || 0),
156
+ tls: Math.round(timing.tls || 0),
157
+ ttfb: Math.round(timing.ttfb || 0),
158
+ download: Math.round(timing.download || 0),
159
+ domInteractive: Math.round(timing.domInteractive || 0),
160
+ domContentLoaded: Math.round(timing.domContentLoaded || 0),
161
+ load: Math.round(timing.load || 0)
162
+ }))
163
+ : undefined;
164
+
142
165
  const payload = {
143
166
  interval: `${config.flushInterval}s`,
144
167
  service: config.service,
@@ -147,6 +170,11 @@ export class BrowserMetricsAggregator {
147
170
  dependencies: []
148
171
  };
149
172
 
173
+ if (webVitals) payload.webVitals = webVitals;
174
+ if (jsErrors) payload.jsErrors = jsErrors;
175
+ if (routeChanges) payload.routeChanges = routeChanges;
176
+ if (pageLoads) payload.pageLoads = pageLoads;
177
+
150
178
  this.reset();
151
179
  return payload;
152
180
  }
@@ -87,7 +87,16 @@ export async function flush() {
87
87
  if (!aggregator.hasData()) return;
88
88
 
89
89
  const payload = aggregator.flush();
90
- debug(`Flushing ${payload.requests.length} request metrics`);
90
+ const frontendExtras = [
91
+ payload.webVitals ? 'vitals' : null,
92
+ payload.jsErrors ? 'errors' : null,
93
+ payload.routeChanges?.length ? 'routes' : null,
94
+ payload.pageLoads?.length ? 'pageLoads' : null
95
+ ].filter(Boolean);
96
+ debug(
97
+ `Flushing ${payload.requests.length} request metrics` +
98
+ (frontendExtras.length ? ` + ${frontendExtras.join(', ')}` : '')
99
+ );
91
100
 
92
101
  try {
93
102
  const startTime = performance.now();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bun + OpenTelemetry: @opentelemetry/instrumentation-http does not hook Bun's HTTP stack,
3
+ * so NodeSDK starts but produces no spans. Emit spans via the OTel API so they still reach
4
+ * BatchSpanProcessor → SentienGuardTraceSpanExporter (same as Node).
5
+ */
6
+
7
+ import { context, trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';
8
+ import { isTracingActive } from './tracing.js';
9
+
10
+ const TRACER_NAME = '@sentienguard/apm';
11
+
12
+ export function shouldEmitBunManualSpans() {
13
+ return typeof globalThis.Bun !== 'undefined' && isTracingActive();
14
+ }
15
+
16
+ export function getBunTracer() {
17
+ return trace.getTracer(TRACER_NAME);
18
+ }
19
+
20
+ export { context, trace, SpanKind, SpanStatusCode };
@@ -11,9 +11,10 @@
11
11
 
12
12
  import http from 'http';
13
13
  import https from 'https';
14
- import { context, propagation } from '@opentelemetry/api';
14
+ import { context, propagation, trace, SpanKind, SpanStatusCode } from '@opentelemetry/api';
15
15
  import { getAggregator } from './aggregator.js';
16
16
  import { debug, getConfig } from './config.js';
17
+ import { shouldEmitBunManualSpans, getBunTracer } from './bunManualSpans.js';
17
18
 
18
19
  let isInstrumented = false;
19
20
  let originalHttpRequest = null;
@@ -178,6 +179,22 @@ function shouldExcludeUrl(u) {
178
179
  return shouldExclude(u.hostname, u);
179
180
  }
180
181
 
182
+ /** Full URL string for outgoing http.request options (for trace attributes). */
183
+ function safeOutgoingUrl(options) {
184
+ try {
185
+ if (typeof options === 'string') return options;
186
+ if (!options || typeof options !== 'object') return '';
187
+ const protocol = options.protocol || 'http:';
188
+ const hostRaw = options.hostname || options.host || '';
189
+ const host = String(hostRaw).trim().split(':')[0];
190
+ const path = options.path || options.pathname || '/';
191
+ if (!host) return '';
192
+ return `${protocol}//${host}${path}`;
193
+ } catch {
194
+ return '';
195
+ }
196
+ }
197
+
181
198
  function injectTraceHeaders(init) {
182
199
  const headers = new Headers((init && init.headers) || {});
183
200
  try {
@@ -215,6 +232,45 @@ export function instrumentFetch() {
215
232
  const peerLabel = resolveOutgoingPeerLabel(hostname, u ? u.toString() : '');
216
233
  const depType = getDependencyType(hostname, u?.pathname || '/');
217
234
 
235
+ if (shouldEmitBunManualSpans()) {
236
+ const tracer = getBunTracer();
237
+ const method = (init && init.method && String(init.method).toUpperCase()) || 'GET';
238
+ const span = tracer.startSpan(`HTTP ${method}`, { kind: SpanKind.CLIENT });
239
+ span.setAttribute('http.method', method);
240
+ if (hostname) span.setAttribute('net.peer.name', hostname);
241
+ if (u?.href) span.setAttribute('http.url', u.href);
242
+ const ctx = trace.setSpan(context.active(), span);
243
+ return context.with(ctx, async () => {
244
+ try {
245
+ const res = await originalFetch.call(this, input, injectTraceHeaders(init));
246
+ const endTime = process.hrtime.bigint();
247
+ const latencyMs = Number(endTime - startTime) / 1e6;
248
+ const isError = (res?.status || 0) >= 400;
249
+ span.setAttribute('http.status_code', res?.status || 0);
250
+ span.setStatus(
251
+ isError ? { code: SpanStatusCode.ERROR } : { code: SpanStatusCode.OK }
252
+ );
253
+ getAggregator().recordDependency(peerLabel, depType, latencyMs, isError);
254
+ debug(`Service call: ${caller} -> ${peerLabel} ${latencyMs.toFixed(2)}ms (${depType}) ${res?.status}`);
255
+ return res;
256
+ } catch (err) {
257
+ const endTime = process.hrtime.bigint();
258
+ const latencyMs = Number(endTime - startTime) / 1e6;
259
+ span.recordException(err);
260
+ span.setStatus({ code: SpanStatusCode.ERROR });
261
+ getAggregator().recordDependency(peerLabel, depType, latencyMs, true);
262
+ debug(`Service call: ${caller} -> ${peerLabel} ${latencyMs.toFixed(2)}ms (${depType}) error`);
263
+ throw err;
264
+ } finally {
265
+ try {
266
+ span.end();
267
+ } catch {
268
+ /* ignore */
269
+ }
270
+ }
271
+ });
272
+ }
273
+
218
274
  try {
219
275
  const res = await originalFetch.call(this, input, injectTraceHeaders(init));
220
276
  const endTime = process.hrtime.bigint();
@@ -270,11 +326,52 @@ function wrapRequest(original, protocol) {
270
326
  const depType = getDependencyType(hostname, path);
271
327
  const caller = getConfig().service || 'unknown';
272
328
 
273
- // Call original
274
- const req = original.apply(this, arguments);
329
+ let clientSpan = null;
330
+ let clientSpanEnded = false;
331
+ const finishClientSpan = (statusCode, networkError) => {
332
+ if (!clientSpan || clientSpanEnded) return;
333
+ clientSpanEnded = true;
334
+ try {
335
+ if (statusCode > 0) {
336
+ clientSpan.setAttribute('http.status_code', statusCode);
337
+ }
338
+ clientSpan.setStatus(
339
+ networkError || statusCode >= 400
340
+ ? { code: SpanStatusCode.ERROR }
341
+ : { code: SpanStatusCode.OK }
342
+ );
343
+ clientSpan.end();
344
+ } catch {
345
+ /* ignore */
346
+ }
347
+ };
348
+
349
+ if (shouldEmitBunManualSpans()) {
350
+ try {
351
+ const tracer = getBunTracer();
352
+ const method =
353
+ typeof options === 'string'
354
+ ? 'GET'
355
+ : String(options?.method || 'GET').toUpperCase();
356
+ clientSpan = tracer.startSpan(`HTTP ${method}`, { kind: SpanKind.CLIENT });
357
+ clientSpan.setAttribute('http.method', method);
358
+ if (hostname) clientSpan.setAttribute('net.peer.name', hostname);
359
+ const urlStr = safeOutgoingUrl(options);
360
+ if (urlStr) clientSpan.setAttribute('http.url', urlStr);
361
+ } catch {
362
+ clientSpan = null;
363
+ }
364
+ }
365
+
366
+ const req = clientSpan
367
+ ? context.with(trace.setSpan(context.active(), clientSpan), () =>
368
+ original.apply(this, arguments)
369
+ )
370
+ : original.apply(this, arguments);
275
371
 
276
372
  // Track response
277
373
  req.on('response', (res) => {
374
+ finishClientSpan(res.statusCode || 0, false);
278
375
  const endTime = process.hrtime.bigint();
279
376
  const latencyMs = Number(endTime - startTime) / 1e6;
280
377
  const isError = res.statusCode >= 400;
@@ -289,6 +386,7 @@ function wrapRequest(original, protocol) {
289
386
 
290
387
  // Track errors
291
388
  req.on('error', () => {
389
+ finishClientSpan(0, true);
292
390
  const endTime = process.hrtime.bigint();
293
391
  const latencyMs = Number(endTime - startTime) / 1e6;
294
392
 
package/src/index.js CHANGED
@@ -80,12 +80,6 @@ function initialize() {
80
80
  instrumentDependencies();
81
81
  }
82
82
 
83
- // #region agent log
84
- try {
85
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'post-fix',hypothesisId:'A,B',location:'index.js:initialize',message:'SDK init runtime + instrumentation flow (Bun-aware)',data:{isBun:isBunRuntime,bunVersion:globalThis.Bun?.version||null,nodeVersion:process.versions?.node||null,tracingOn,legacyHttpInstalled:!tracingOn||isBunRuntime,service:config.service,endpoint:config.endpoint,argv0:process.argv0,execPath:process.execPath},timestamp:Date.now()}));
86
- } catch {}
87
- // #endregion
88
-
89
83
  // Auto-instrument MongoDB if available
90
84
  autoInstrumentMongoDB();
91
85
 
@@ -16,6 +16,14 @@ import { extractRoute } from './normalizer.js';
16
16
  import { getAggregator } from './aggregator.js';
17
17
  import { debug } from './config.js';
18
18
  import { isTracingActive } from './tracing.js';
19
+ import {
20
+ context,
21
+ trace,
22
+ SpanKind,
23
+ SpanStatusCode,
24
+ shouldEmitBunManualSpans,
25
+ getBunTracer
26
+ } from './bunManualSpans.js';
19
27
 
20
28
  let isInstrumented = false;
21
29
  let originalHttpCreateServer = null;
@@ -28,6 +36,19 @@ function wrapRequestHandler(handler) {
28
36
  return function wrappedHandler(req, res) {
29
37
  const startTime = process.hrtime.bigint();
30
38
 
39
+ let span = null;
40
+ let spanEnded = false;
41
+ if (shouldEmitBunManualSpans()) {
42
+ try {
43
+ span = getBunTracer().startSpan('HTTP', { kind: SpanKind.SERVER });
44
+ span.setAttribute('http.method', req.method || 'GET');
45
+ const rawUrl = req.url || '/';
46
+ span.setAttribute('http.target', typeof rawUrl === 'string' ? rawUrl : String(rawUrl));
47
+ } catch {
48
+ span = null;
49
+ }
50
+ }
51
+
31
52
  // Store original end method
32
53
  const originalEnd = res.end;
33
54
 
@@ -46,13 +67,52 @@ function wrapRequestHandler(handler) {
46
67
  const aggregator = getAggregator();
47
68
  aggregator.recordRequest(method, route, latencyMs, isError);
48
69
 
70
+ if (span && !spanEnded) {
71
+ spanEnded = true;
72
+ try {
73
+ span.setAttribute('http.route', route);
74
+ span.setAttribute('http.status_code', statusCode);
75
+ span.updateName(`${method} ${route}`);
76
+ span.setStatus(
77
+ statusCode >= 400
78
+ ? { code: SpanStatusCode.ERROR }
79
+ : { code: SpanStatusCode.OK }
80
+ );
81
+ } catch {
82
+ /* ignore */
83
+ }
84
+ try {
85
+ span.end();
86
+ } catch {
87
+ /* ignore */
88
+ }
89
+ }
90
+
49
91
  debug(`Request: ${method} ${route} ${statusCode} ${latencyMs.toFixed(2)}ms`);
50
92
 
51
93
  // Call original end
52
94
  return originalEnd.apply(this, args);
53
95
  };
54
96
 
55
- // Call original handler
97
+ if (span) {
98
+ try {
99
+ const ctx = trace.setSpan(context.active(), span);
100
+ return context.with(ctx, () => handler.call(this, req, res));
101
+ } catch (err) {
102
+ if (!spanEnded) {
103
+ spanEnded = true;
104
+ try {
105
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err?.message });
106
+ span.recordException(err);
107
+ span.end();
108
+ } catch {
109
+ /* ignore */
110
+ }
111
+ }
112
+ throw err;
113
+ }
114
+ }
115
+
56
116
  return handler.call(this, req, res);
57
117
  };
58
118
  }
package/src/mongodb.js CHANGED
@@ -106,12 +106,6 @@ function handleCommandStarted(event) {
106
106
 
107
107
  const commandName = event.commandName;
108
108
 
109
- // #region agent log
110
- try {
111
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-cmd',hypothesisId:'C',location:'mongodb.js:handleCommandStarted',message:'commandStarted event fired',data:{commandName,databaseName:event.databaseName,ignored:IGNORED_COMMANDS.has(commandName),requestId:String(event.requestId||'')},timestamp:Date.now()}));
112
- } catch {}
113
- // #endregion
114
-
115
109
  // Skip ignored commands
116
110
  if (IGNORED_COMMANDS.has(commandName)) {
117
111
  return;
@@ -269,42 +263,26 @@ function collectPoolStats() {
269
263
  * also populate require.cache via the CJS interop layer.
270
264
  */
271
265
  function tryDetectMongoose() {
272
- let cacheKeyCount = 0;
273
- let mongooseKeyMatched = null;
274
- let cacheError = null;
275
266
  try {
276
267
  // Strategy 1: Scan require.cache for mongoose
277
268
  const cache = require.cache || {};
278
269
  const cacheKeys = Object.keys(cache);
279
- cacheKeyCount = cacheKeys.length;
280
270
  const mongooseKey = cacheKeys.find(
281
271
  k => /[\\/]mongoose[\\/](?:lib[\\/])?index\.js$/.test(k) &&
282
272
  !k.includes('node_modules/mongoose/node_modules')
283
273
  );
284
- mongooseKeyMatched = mongooseKey || null;
285
274
  if (mongooseKey && cache[mongooseKey]?.exports) {
286
275
  const mod = cache[mongooseKey].exports;
287
276
  // Verify it's actually mongoose (has connection property)
288
277
  if (mod.connection) {
289
278
  debug('Detected mongoose via require.cache');
290
- // #region agent log
291
- try {
292
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-detect',hypothesisId:'D',location:'mongodb.js:tryDetectMongoose',message:'mongoose detected via require.cache',data:{isBun:typeof globalThis.Bun!=='undefined',cacheKeyCount,matchedKey:mongooseKey,readyState:mod.connection?.readyState??null},timestamp:Date.now()}));
293
- } catch {}
294
- // #endregion
295
279
  return mod;
296
280
  }
297
281
  }
298
282
  } catch (e) {
299
- cacheError = e?.message || String(e);
283
+ // require.cache not available (unlikely in Node.js)
300
284
  }
301
285
 
302
- // #region agent log
303
- try {
304
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-detect',hypothesisId:'D',location:'mongodb.js:tryDetectMongoose',message:'mongoose NOT detected (require.cache scan)',data:{isBun:typeof globalThis.Bun!=='undefined',hasRequire:typeof require!=='undefined',hasRequireCache:typeof require!=='undefined' && !!require.cache,cacheKeyCount,mongooseKeyMatched,cacheError,globalThisMongoose:!!globalThis.mongoose},timestamp:Date.now()}));
305
- } catch {}
306
- // #endregion
307
-
308
286
  // Strategy 2: Fallback — check globalThis (in case user set it manually)
309
287
  try {
310
288
  if (globalThis.mongoose?.connection) {
@@ -328,14 +306,6 @@ function ensureMonitorCommands(client) {
328
306
  client.options?.monitorCommands ||
329
307
  client.s?.options?.monitorCommands;
330
308
 
331
- // #region agent log
332
- try {
333
- const optsRoot = client.options ? Object.keys(client.options).slice(0, 30) : null;
334
- const optsS = client.s?.options ? Object.keys(client.s.options).slice(0, 30) : null;
335
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'mongo-init',hypothesisId:'C',location:'mongodb.js:ensureMonitorCommands',message:'monitorCommands inspection on live MongoClient',data:{alreadyEnabled:!!alreadyEnabled,hasClientOptions:!!client.options,hasClientSOptions:!!client.s?.options,clientOptionsFrozen:client.options ? Object.isFrozen(client.options) : null,clientSOptionsFrozen:client.s?.options ? Object.isFrozen(client.s.options) : null,optsRootKeys:optsRoot,optsSKeys:optsS},timestamp:Date.now()}));
336
- } catch {}
337
- // #endregion
338
-
339
309
  if (alreadyEnabled) {
340
310
  debug('monitorCommands already enabled');
341
311
  return true;
@@ -389,11 +359,6 @@ function wrapMongooseConnectForMonitorCommands(mongoose) {
389
359
  mongoose.connect = function sentienguardConnect(uri, options, ...rest) {
390
360
  const opts = { ...(options || {}), monitorCommands: true };
391
361
  debug('Injecting monitorCommands:true into mongoose.connect()');
392
- // #region agent log
393
- try {
394
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'post-fix',hypothesisId:'C',location:'mongodb.js:wrapMongooseConnect',message:'mongoose.connect called - injected monitorCommands:true',data:{hadOptions:!!options,userMonitorCommands:options?.monitorCommands??null},timestamp:Date.now()}));
395
- } catch {}
396
- // #endregion
397
362
  return origConnect(uri, opts, ...rest);
398
363
  };
399
364
  }
@@ -146,12 +146,6 @@ export function classifyAndRecordSpan(span) {
146
146
  let error = isErrorSpan(span);
147
147
  const kind = span.kind;
148
148
 
149
- // #region agent log
150
- try {
151
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'span',hypothesisId:'A,E',location:'spanExporter.js:classifyAndRecordSpan',message:'span received by exporter',data:{kind,name:span.name,latencyMs:Math.round(latency),httpMethod:attr(attrs,HTTP_METHOD)||null,httpRoute:attr(attrs,HTTP_ROUTE)||null,httpTarget:attr(attrs,HTTP_TARGET)||null,httpStatus:attr(attrs,HTTP_STATUS)||null,dbSystem:attr(attrs,DB_SYSTEM)||null,dbCollection:attr(attrs,DB_COLLECTION)||null,dbOperation:attr(attrs,DB_OPERATION)||null,peerName:attr(attrs,NET_PEER_NAME)||null,error},timestamp:Date.now()}));
152
- } catch {}
153
- // #endregion
154
-
155
149
  if (kind === SpanKind.SERVER) {
156
150
  const method = attr(attrs, HTTP_METHOD) || 'GET';
157
151
  let route = attr(attrs, HTTP_ROUTE) || attr(attrs, HTTP_TARGET) || '/';
@@ -95,6 +95,27 @@ function shouldSampleTraceId(traceId, sampleRate) {
95
95
  }
96
96
  }
97
97
 
98
+ function getParentSpanId(span) {
99
+ try {
100
+ const direct = span?.parentSpanId;
101
+ if (typeof direct === 'string' && direct) return direct;
102
+
103
+ const psc = span?.parentSpanContext;
104
+ if (psc && typeof psc === 'object') {
105
+ const id = psc.spanId;
106
+ if (typeof id === 'string' && id) return id;
107
+ }
108
+ if (typeof psc === 'function') {
109
+ const ctx = psc();
110
+ const id = ctx?.spanId;
111
+ if (typeof id === 'string' && id) return id;
112
+ }
113
+ } catch {
114
+ // ignore
115
+ }
116
+ return null;
117
+ }
118
+
98
119
  function serializeSpan(span) {
99
120
  const ctx = span?.spanContext?.();
100
121
  if (!ctx?.traceId || !ctx?.spanId) return null;
@@ -103,7 +124,7 @@ function serializeSpan(span) {
103
124
  const endNano = hrTimeToUnixNanoString(span.endTime);
104
125
  if (!startNano || !endNano) return null;
105
126
 
106
- const parentSpanId = span?.parentSpanId || span?.parentSpanContext?.spanId || null;
127
+ const parentSpanId = getParentSpanId(span);
107
128
  const status = statusForSpan(span);
108
129
 
109
130
  const durationMs =
package/src/tracing.js CHANGED
@@ -193,11 +193,6 @@ export function startTracing() {
193
193
  sdk.start();
194
194
  tracingActive = true;
195
195
  debug('OpenTelemetry tracing started (W3C Trace Context + HTTP/Express)');
196
- // #region agent log
197
- try {
198
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'init',hypothesisId:'A',location:'tracing.js:startTracing',message:'NodeSDK.start() returned without error',data:{isBun:typeof globalThis.Bun!=='undefined',sdkType:sdk?.constructor?.name||null,instrumentationsCount:4,sampler:'AlwaysOn'},timestamp:Date.now()}));
199
- } catch {}
200
- // #endregion
201
196
  return true;
202
197
  } catch (err) {
203
198
  const msg = err instanceof Error ? err.message : String(err);
package/src/transport.js CHANGED
@@ -112,14 +112,6 @@ async function flush() {
112
112
 
113
113
  const aggregator = getAggregator();
114
114
 
115
- // #region agent log
116
- try {
117
- const stats = aggregator.getStats();
118
- const pool = aggregator.mongodbPoolStats || null;
119
- console.log('[SG-APM-DBG]', JSON.stringify({sessionId:'ecc573',runId:'flush',hypothesisId:'A,B,C,D,E',location:'transport.js:flush',message:'flush() invoked - aggregator state',data:{hasData:aggregator.hasData(),...stats,poolUpdated:!!pool?.lastUpdated,poolActive:pool?.active??null,poolIdle:pool?.idle??null,poolTotal:pool?.total??null,isBun:typeof globalThis.Bun!=='undefined'},timestamp:Date.now()}));
120
- } catch {}
121
- // #endregion
122
-
123
115
  // Skip if no data
124
116
  if (!aggregator.hasData()) {
125
117
  debug('No data to flush');