@justanalyticsapp/node 0.1.0

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.
Files changed (52) hide show
  1. package/dist/client.d.ts +286 -0
  2. package/dist/client.js +681 -0
  3. package/dist/client.js.map +1 -0
  4. package/dist/context.d.ts +126 -0
  5. package/dist/context.js +170 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/errors.d.ts +135 -0
  8. package/dist/errors.js +180 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/index.d.ts +301 -0
  11. package/dist/index.js +314 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/integrations/express.d.ts +77 -0
  14. package/dist/integrations/express.js +87 -0
  15. package/dist/integrations/express.js.map +1 -0
  16. package/dist/integrations/http.d.ts +129 -0
  17. package/dist/integrations/http.js +465 -0
  18. package/dist/integrations/http.js.map +1 -0
  19. package/dist/integrations/metrics.d.ts +110 -0
  20. package/dist/integrations/metrics.js +313 -0
  21. package/dist/integrations/metrics.js.map +1 -0
  22. package/dist/integrations/next.d.ts +252 -0
  23. package/dist/integrations/next.js +480 -0
  24. package/dist/integrations/next.js.map +1 -0
  25. package/dist/integrations/pg.d.ts +169 -0
  26. package/dist/integrations/pg.js +616 -0
  27. package/dist/integrations/pg.js.map +1 -0
  28. package/dist/integrations/pino.d.ts +52 -0
  29. package/dist/integrations/pino.js +153 -0
  30. package/dist/integrations/pino.js.map +1 -0
  31. package/dist/integrations/redis.d.ts +190 -0
  32. package/dist/integrations/redis.js +597 -0
  33. package/dist/integrations/redis.js.map +1 -0
  34. package/dist/integrations/winston.d.ts +48 -0
  35. package/dist/integrations/winston.js +99 -0
  36. package/dist/integrations/winston.js.map +1 -0
  37. package/dist/logger.d.ts +148 -0
  38. package/dist/logger.js +162 -0
  39. package/dist/logger.js.map +1 -0
  40. package/dist/span.d.ts +192 -0
  41. package/dist/span.js +197 -0
  42. package/dist/span.js.map +1 -0
  43. package/dist/transport.d.ts +246 -0
  44. package/dist/transport.js +654 -0
  45. package/dist/transport.js.map +1 -0
  46. package/dist/utils/headers.d.ts +60 -0
  47. package/dist/utils/headers.js +93 -0
  48. package/dist/utils/headers.js.map +1 -0
  49. package/dist/utils/id.d.ts +23 -0
  50. package/dist/utils/id.js +36 -0
  51. package/dist/utils/id.js.map +1 -0
  52. package/package.json +65 -0
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * @file packages/node-sdk/src/integrations/express.ts
4
+ * @description Express middleware for route pattern extraction in JustAnalytics spans.
5
+ *
6
+ * Implements Story 036 - HTTP Auto-Instrumentation
7
+ *
8
+ * When the HTTP integration creates a server span for an incoming request,
9
+ * the operation name defaults to `{method} {raw URL}` (e.g., `GET /api/users/42`).
10
+ * This middleware updates the operation name to use Express route patterns
11
+ * (e.g., `GET /api/users/:id`) for more meaningful span names.
12
+ *
13
+ * The middleware uses `res.on('finish')` to read `req.route` after Express
14
+ * completes route matching, ensuring it works regardless of middleware ordering.
15
+ *
16
+ * Usage:
17
+ * ```typescript
18
+ * import JA from '@justanalyticsapp/node';
19
+ * import express from 'express';
20
+ *
21
+ * const app = express();
22
+ * app.use(JA.expressMiddleware());
23
+ *
24
+ * app.get('/api/users/:id', (req, res) => {
25
+ * // Span operation name will be updated to "GET /api/users/:id"
26
+ * res.json({ id: req.params.id });
27
+ * });
28
+ * ```
29
+ *
30
+ * References:
31
+ * - Express routing: req.route is populated after route matching
32
+ * - Story 035 - Node.js SDK Core (Span class, context module)
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.expressMiddleware = expressMiddleware;
36
+ const context_1 = require("../context");
37
+ /**
38
+ * Express middleware that extracts `req.route.path` and updates the
39
+ * active span's operation name to use the route pattern instead
40
+ * of the raw URL.
41
+ *
42
+ * Must be registered via `app.use()` so it runs for all routes.
43
+ * The route pattern extraction happens in `res.on('finish')`,
44
+ * which fires after route matching is complete.
45
+ *
46
+ * If the SDK is not initialized or no active span exists, the
47
+ * middleware is a no-op and simply calls `next()`.
48
+ *
49
+ * @returns Express-compatible middleware function: `(req, res, next) => void`
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import JA from '@justanalyticsapp/node';
54
+ * import express from 'express';
55
+ *
56
+ * JA.init({ siteId: '...', apiKey: '...', serviceName: 'api' });
57
+ *
58
+ * const app = express();
59
+ * app.use(JA.expressMiddleware());
60
+ *
61
+ * app.get('/api/users/:id', handler);
62
+ * // Span operationName: "GET /api/users/:id" (instead of "GET /api/users/42")
63
+ * ```
64
+ */
65
+ function expressMiddleware() {
66
+ return function jaExpressMiddleware(req, res, next) {
67
+ // Use res.on('finish') to extract the route after matching
68
+ res.on('finish', () => {
69
+ try {
70
+ const span = (0, context_1.getActiveSpan)();
71
+ if (!span)
72
+ return;
73
+ const route = req.route?.path;
74
+ if (route) {
75
+ const method = (req.method || 'GET').toUpperCase();
76
+ span.updateOperationName(`${method} ${route}`);
77
+ span.setAttribute('http.route', route);
78
+ }
79
+ }
80
+ catch {
81
+ // Never crash the request pipeline
82
+ }
83
+ });
84
+ next();
85
+ };
86
+ }
87
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/integrations/express.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;;AAkDH,8CA6BC;AA7ED,wCAA2C;AAoB3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,SAAgB,iBAAiB;IAK/B,OAAO,SAAS,mBAAmB,CACjC,GAAmB,EACnB,GAAoB,EACpB,IAAyB;QAEzB,2DAA2D;QAC3D,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACpB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,IAAA,uBAAa,GAAE,CAAC;gBAC7B,IAAI,CAAC,IAAI;oBAAE,OAAO;gBAElB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC;gBAC9B,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;oBACnD,IAAI,CAAC,mBAAmB,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC,CAAC;oBAC/C,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,mCAAmC;YACrC,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @file packages/node-sdk/src/integrations/http.ts
3
+ * @description HTTP auto-instrumentation integration for the JustAnalytics Node.js SDK.
4
+ *
5
+ * Implements Story 036 - HTTP Auto-Instrumentation
6
+ *
7
+ * Monkey-patches Node.js built-in `http` and `https` modules to automatically
8
+ * create spans for incoming and outgoing HTTP traffic. This is the industry-standard
9
+ * approach used by OpenTelemetry, Datadog dd-trace, and Sentry for Node.js
10
+ * auto-instrumentation.
11
+ *
12
+ * - **Outgoing requests:** Wraps `http.request`, `http.get`, `https.request`,
13
+ * `https.get` to create client spans, inject `traceparent` headers, and capture
14
+ * response status codes.
15
+ *
16
+ * - **Incoming requests:** Patches `http.Server.prototype.emit` to intercept
17
+ * the `'request'` event, creating server spans, reading `traceparent` headers,
18
+ * and propagating trace context via AsyncLocalStorage.
19
+ *
20
+ * References:
21
+ * - W3C Trace Context specification (traceparent header format)
22
+ * - OpenTelemetry @opentelemetry/instrumentation-http (similar approach)
23
+ * - Story 035 - Node.js SDK Core (Span, context, transport)
24
+ */
25
+ import { Span } from '../span';
26
+ /**
27
+ * Configuration for the HTTP auto-instrumentation integration.
28
+ */
29
+ export interface HttpIntegrationOptions {
30
+ /** Enable/disable the integration (default: true) */
31
+ enabled?: boolean;
32
+ /**
33
+ * URL patterns to exclude from instrumentation.
34
+ * - string: matched via url.includes(pattern)
35
+ * - RegExp: matched via pattern.test(url)
36
+ * Appended to default exclusions unless ignoreDefaults is true.
37
+ */
38
+ ignoreUrls?: Array<string | RegExp>;
39
+ /**
40
+ * When true, do not apply the default URL exclusion patterns
41
+ * ('/health', '/ready', '/live', '/favicon.ico', serverUrl).
42
+ * (default: false)
43
+ */
44
+ ignoreDefaults?: boolean;
45
+ /** Instrument incoming HTTP requests (default: true) */
46
+ traceIncoming?: boolean;
47
+ /** Instrument outgoing HTTP requests (default: true) */
48
+ traceOutgoing?: boolean;
49
+ }
50
+ /**
51
+ * HttpIntegration monkey-patches http.request/https.request to
52
+ * auto-create spans for incoming and outgoing HTTP traffic.
53
+ *
54
+ * Incoming requests: reads traceparent, creates root server span,
55
+ * propagates context via AsyncLocalStorage.
56
+ *
57
+ * Outgoing requests: injects traceparent header, creates client span
58
+ * as a child of the active span.
59
+ */
60
+ export declare class HttpIntegration {
61
+ private _enabled;
62
+ private _options;
63
+ private _serviceName;
64
+ private _serverUrl;
65
+ private _onSpanEnd;
66
+ private _originalHttpRequest;
67
+ private _originalHttpGet;
68
+ private _originalHttpsRequest;
69
+ private _originalHttpsGet;
70
+ private _originalServerEmit;
71
+ /**
72
+ * Create a new HttpIntegration.
73
+ *
74
+ * @param serviceName - The service name for span attribution
75
+ * @param serverUrl - The JustAnalytics server URL (excluded from tracing)
76
+ * @param options - Integration configuration options
77
+ * @param onSpanEnd - Callback invoked when a span ends (enqueues to transport)
78
+ */
79
+ constructor(serviceName: string, serverUrl: string, options: HttpIntegrationOptions | undefined, onSpanEnd: (span: Span) => void);
80
+ /**
81
+ * Activate the integration: patch http/https modules.
82
+ *
83
+ * Calling enable() when already enabled is a no-op (idempotent).
84
+ */
85
+ enable(): void;
86
+ /**
87
+ * Deactivate: restore original http/https functions.
88
+ *
89
+ * Calling disable() when not enabled is a no-op (idempotent).
90
+ */
91
+ disable(): void;
92
+ /**
93
+ * Restore all patched functions to their originals.
94
+ */
95
+ private _restoreOriginals;
96
+ /**
97
+ * Check if a URL matches any exclusion pattern.
98
+ *
99
+ * @param url - The URL string to check
100
+ * @returns true if the URL should be ignored (not instrumented)
101
+ */
102
+ private _shouldIgnore;
103
+ /**
104
+ * Wrap an outgoing `http.request` or `https.request` to create client spans.
105
+ *
106
+ * @param original - The original request function
107
+ * @param protocol - 'http' or 'https'
108
+ * @returns A wrapped version of the request function
109
+ */
110
+ private _wrapOutgoingRequest;
111
+ /**
112
+ * Wrap an outgoing `http.get` or `https.get` to create client spans.
113
+ *
114
+ * `http.get` is like `http.request` but auto-calls `req.end()`.
115
+ *
116
+ * @param original - The original get function
117
+ * @param protocol - 'http' or 'https'
118
+ * @returns A wrapped version of the get function
119
+ */
120
+ private _wrapOutgoingGet;
121
+ /**
122
+ * Create a server span for an incoming HTTP request.
123
+ *
124
+ * @param req - The incoming HTTP request
125
+ * @param res - The server response
126
+ * @returns The created Span, or null if instrumentation should be skipped
127
+ */
128
+ private _instrumentIncoming;
129
+ }
@@ -0,0 +1,465 @@
1
+ "use strict";
2
+ /**
3
+ * @file packages/node-sdk/src/integrations/http.ts
4
+ * @description HTTP auto-instrumentation integration for the JustAnalytics Node.js SDK.
5
+ *
6
+ * Implements Story 036 - HTTP Auto-Instrumentation
7
+ *
8
+ * Monkey-patches Node.js built-in `http` and `https` modules to automatically
9
+ * create spans for incoming and outgoing HTTP traffic. This is the industry-standard
10
+ * approach used by OpenTelemetry, Datadog dd-trace, and Sentry for Node.js
11
+ * auto-instrumentation.
12
+ *
13
+ * - **Outgoing requests:** Wraps `http.request`, `http.get`, `https.request`,
14
+ * `https.get` to create client spans, inject `traceparent` headers, and capture
15
+ * response status codes.
16
+ *
17
+ * - **Incoming requests:** Patches `http.Server.prototype.emit` to intercept
18
+ * the `'request'` event, creating server spans, reading `traceparent` headers,
19
+ * and propagating trace context via AsyncLocalStorage.
20
+ *
21
+ * References:
22
+ * - W3C Trace Context specification (traceparent header format)
23
+ * - OpenTelemetry @opentelemetry/instrumentation-http (similar approach)
24
+ * - Story 035 - Node.js SDK Core (Span, context, transport)
25
+ */
26
+ var __importDefault = (this && this.__importDefault) || function (mod) {
27
+ return (mod && mod.__esModule) ? mod : { "default": mod };
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.HttpIntegration = void 0;
31
+ const http_1 = __importDefault(require("http"));
32
+ const https_1 = __importDefault(require("https"));
33
+ const url_1 = require("url");
34
+ const span_1 = require("../span");
35
+ const headers_1 = require("../utils/headers");
36
+ const context_1 = require("../context");
37
+ const id_1 = require("../utils/id");
38
+ /** Default URL patterns to exclude from instrumentation */
39
+ const DEFAULT_IGNORE_URLS = [
40
+ '/health',
41
+ '/ready',
42
+ '/live',
43
+ '/favicon.ico',
44
+ ];
45
+ /**
46
+ * Escape special RegExp characters in a string.
47
+ *
48
+ * @param str - String to escape
49
+ * @returns Escaped string safe for use in a RegExp
50
+ */
51
+ function escapeRegex(str) {
52
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ }
54
+ /**
55
+ * Normalize the various overloaded argument forms of http.request / http.get
56
+ * into a consistent { url, options, callback } shape.
57
+ *
58
+ * Node.js supports three call signatures:
59
+ * - request(url: string | URL, options?, callback?)
60
+ * - request(options, callback?)
61
+ *
62
+ * @param args - The arguments passed to http.request / http.get
63
+ * @param protocol - 'http' or 'https' for URL construction
64
+ * @returns Normalized { urlString, options, callback }
65
+ */
66
+ function normalizeRequestArgs(args, protocol) {
67
+ let urlString;
68
+ let options;
69
+ let callback;
70
+ if (typeof args[0] === 'string' || args[0] instanceof url_1.URL) {
71
+ // request(url, options?, callback?)
72
+ const urlArg = args[0] instanceof url_1.URL ? args[0] : new url_1.URL(args[0]);
73
+ urlString = urlArg.toString();
74
+ if (typeof args[1] === 'function') {
75
+ options = {};
76
+ callback = args[1];
77
+ }
78
+ else if (typeof args[1] === 'object' && args[1] !== null) {
79
+ options = args[1];
80
+ callback = typeof args[2] === 'function'
81
+ ? args[2]
82
+ : undefined;
83
+ }
84
+ else {
85
+ options = {};
86
+ callback = undefined;
87
+ }
88
+ // Merge URL into options if not already set
89
+ if (!options.hostname && !options.host) {
90
+ options.hostname = urlArg.hostname;
91
+ options.port = urlArg.port || undefined;
92
+ options.path = urlArg.pathname + urlArg.search;
93
+ options.protocol = urlArg.protocol;
94
+ }
95
+ }
96
+ else if (typeof args[0] === 'object' && args[0] !== null) {
97
+ // request(options, callback?)
98
+ options = args[0];
99
+ callback = typeof args[1] === 'function'
100
+ ? args[1]
101
+ : undefined;
102
+ // Build URL string from options
103
+ const hostname = options.hostname || options.host || 'localhost';
104
+ const cleanHost = hostname.replace(/:\d+$/, '');
105
+ const port = options.port;
106
+ const path = options.path || '/';
107
+ const proto = options.protocol || `${protocol}:`;
108
+ const defaultPort = proto === 'https:' ? 443 : 80;
109
+ const portStr = port && Number(port) !== defaultPort ? `:${port}` : '';
110
+ urlString = `${proto}//${cleanHost}${portStr}${path}`;
111
+ }
112
+ else {
113
+ // Fallback: unknown call pattern
114
+ options = {};
115
+ callback = undefined;
116
+ urlString = `${protocol}://unknown/`;
117
+ }
118
+ return { urlString, options, callback };
119
+ }
120
+ /**
121
+ * HttpIntegration monkey-patches http.request/https.request to
122
+ * auto-create spans for incoming and outgoing HTTP traffic.
123
+ *
124
+ * Incoming requests: reads traceparent, creates root server span,
125
+ * propagates context via AsyncLocalStorage.
126
+ *
127
+ * Outgoing requests: injects traceparent header, creates client span
128
+ * as a child of the active span.
129
+ */
130
+ class HttpIntegration {
131
+ /**
132
+ * Create a new HttpIntegration.
133
+ *
134
+ * @param serviceName - The service name for span attribution
135
+ * @param serverUrl - The JustAnalytics server URL (excluded from tracing)
136
+ * @param options - Integration configuration options
137
+ * @param onSpanEnd - Callback invoked when a span ends (enqueues to transport)
138
+ */
139
+ constructor(serviceName, serverUrl, options, onSpanEnd) {
140
+ this._enabled = false;
141
+ // Store original functions for restoration
142
+ this._originalHttpRequest = null;
143
+ this._originalHttpGet = null;
144
+ this._originalHttpsRequest = null;
145
+ this._originalHttpsGet = null;
146
+ this._originalServerEmit = null;
147
+ this._serviceName = serviceName;
148
+ this._serverUrl = serverUrl;
149
+ this._onSpanEnd = onSpanEnd;
150
+ // Resolve options with defaults
151
+ const opts = options || {};
152
+ const ignoreDefaults = opts.ignoreDefaults === true;
153
+ const customIgnoreUrls = opts.ignoreUrls || [];
154
+ // Build the combined ignore list
155
+ let ignoreUrls;
156
+ if (ignoreDefaults) {
157
+ ignoreUrls = [...customIgnoreUrls];
158
+ }
159
+ else {
160
+ // Build server URL exclusion pattern
161
+ const serverUrlPatterns = [];
162
+ try {
163
+ const parsed = new url_1.URL(serverUrl);
164
+ serverUrlPatterns.push(new RegExp(`^https?://${escapeRegex(parsed.host)}`));
165
+ }
166
+ catch {
167
+ // If serverUrl is malformed, skip it
168
+ }
169
+ ignoreUrls = [...DEFAULT_IGNORE_URLS, ...serverUrlPatterns, ...customIgnoreUrls];
170
+ }
171
+ this._options = {
172
+ enabled: opts.enabled !== false,
173
+ ignoreUrls,
174
+ ignoreDefaults,
175
+ traceIncoming: opts.traceIncoming !== false,
176
+ traceOutgoing: opts.traceOutgoing !== false,
177
+ };
178
+ }
179
+ /**
180
+ * Activate the integration: patch http/https modules.
181
+ *
182
+ * Calling enable() when already enabled is a no-op (idempotent).
183
+ */
184
+ enable() {
185
+ if (this._enabled)
186
+ return;
187
+ if (!this._options.enabled)
188
+ return;
189
+ try {
190
+ // Patch outgoing requests
191
+ if (this._options.traceOutgoing) {
192
+ this._originalHttpRequest = http_1.default.request;
193
+ this._originalHttpGet = http_1.default.get;
194
+ this._originalHttpsRequest = https_1.default.request;
195
+ this._originalHttpsGet = https_1.default.get;
196
+ http_1.default.request = this._wrapOutgoingRequest(this._originalHttpRequest, 'http');
197
+ http_1.default.get = this._wrapOutgoingGet(this._originalHttpGet, 'http');
198
+ https_1.default.request = this._wrapOutgoingRequest(this._originalHttpsRequest, 'https');
199
+ https_1.default.get = this._wrapOutgoingGet(this._originalHttpsGet, 'https');
200
+ }
201
+ // Patch incoming requests via http.Server.prototype.emit
202
+ if (this._options.traceIncoming) {
203
+ this._originalServerEmit = http_1.default.Server.prototype.emit;
204
+ const integration = this;
205
+ const originalEmit = this._originalServerEmit;
206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
+ http_1.default.Server.prototype.emit = function patchedEmit(event, ...args) {
208
+ if (event === 'request') {
209
+ const req = args[0];
210
+ const res = args[1];
211
+ const urlString = req.url || '/';
212
+ // Check exclusion
213
+ if (!integration._shouldIgnore(urlString)) {
214
+ const span = integration._instrumentIncoming(req, res);
215
+ if (span) {
216
+ // Run the rest of the request pipeline in span context
217
+ const childContext = (0, context_1.createChildContext)({
218
+ activeSpan: span,
219
+ traceId: span.traceId,
220
+ });
221
+ return (0, context_1.runWithContext)(childContext, () => {
222
+ return originalEmit.apply(this, [event, ...args]);
223
+ });
224
+ }
225
+ }
226
+ }
227
+ return originalEmit.apply(this, [event, ...args]);
228
+ };
229
+ }
230
+ this._enabled = true;
231
+ }
232
+ catch (error) {
233
+ // Monkey-patching failed; fall back to no-op
234
+ this._restoreOriginals();
235
+ if (typeof console !== 'undefined') {
236
+ console.warn('[JustAnalytics] Failed to enable HTTP integration:', error instanceof Error ? error.message : String(error));
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * Deactivate: restore original http/https functions.
242
+ *
243
+ * Calling disable() when not enabled is a no-op (idempotent).
244
+ */
245
+ disable() {
246
+ if (!this._enabled)
247
+ return;
248
+ this._restoreOriginals();
249
+ this._enabled = false;
250
+ }
251
+ /**
252
+ * Restore all patched functions to their originals.
253
+ */
254
+ _restoreOriginals() {
255
+ if (this._originalHttpRequest) {
256
+ http_1.default.request = this._originalHttpRequest;
257
+ this._originalHttpRequest = null;
258
+ }
259
+ if (this._originalHttpGet) {
260
+ http_1.default.get = this._originalHttpGet;
261
+ this._originalHttpGet = null;
262
+ }
263
+ if (this._originalHttpsRequest) {
264
+ https_1.default.request = this._originalHttpsRequest;
265
+ this._originalHttpsRequest = null;
266
+ }
267
+ if (this._originalHttpsGet) {
268
+ https_1.default.get = this._originalHttpsGet;
269
+ this._originalHttpsGet = null;
270
+ }
271
+ if (this._originalServerEmit) {
272
+ http_1.default.Server.prototype.emit = this._originalServerEmit;
273
+ this._originalServerEmit = null;
274
+ }
275
+ }
276
+ /**
277
+ * Check if a URL matches any exclusion pattern.
278
+ *
279
+ * @param url - The URL string to check
280
+ * @returns true if the URL should be ignored (not instrumented)
281
+ */
282
+ _shouldIgnore(url) {
283
+ for (const pattern of this._options.ignoreUrls) {
284
+ if (typeof pattern === 'string') {
285
+ if (url.includes(pattern))
286
+ return true;
287
+ }
288
+ else if (pattern instanceof RegExp) {
289
+ if (pattern.test(url))
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+ /**
296
+ * Wrap an outgoing `http.request` or `https.request` to create client spans.
297
+ *
298
+ * @param original - The original request function
299
+ * @param protocol - 'http' or 'https'
300
+ * @returns A wrapped version of the request function
301
+ */
302
+ _wrapOutgoingRequest(original, protocol) {
303
+ const integration = this;
304
+ return function patchedRequest(...args) {
305
+ try {
306
+ const { urlString, options, callback } = normalizeRequestArgs(args, protocol);
307
+ // Check if URL should be ignored
308
+ if (integration._shouldIgnore(urlString)) {
309
+ return original.apply(this, args);
310
+ }
311
+ // Get active span context
312
+ const parentSpan = (0, context_1.getActiveSpan)();
313
+ const traceId = parentSpan?.traceId ?? (0, id_1.generateTraceId)();
314
+ const parentSpanId = parentSpan?.id ?? null;
315
+ const method = (options.method || 'GET').toUpperCase();
316
+ const hostname = options.hostname || options.host || 'unknown';
317
+ const cleanHostname = hostname.replace(/:\d+$/, '');
318
+ const path = options.path || '/';
319
+ // Create client span
320
+ const span = new span_1.Span({
321
+ operationName: `${method} ${cleanHostname}${path}`,
322
+ serviceName: integration._serviceName,
323
+ kind: 'client',
324
+ traceId,
325
+ parentSpanId,
326
+ attributes: {
327
+ 'http.method': method,
328
+ 'http.url': urlString,
329
+ 'http.host': cleanHostname,
330
+ },
331
+ });
332
+ // Inject traceparent header
333
+ if (!options.headers || Array.isArray(options.headers)) {
334
+ options.headers = {};
335
+ }
336
+ options.headers['traceparent'] = (0, headers_1.serializeTraceparent)(span.traceId, span.id);
337
+ // Build new args with modified options
338
+ // We always call original(options, wrappedCallback) to ensure headers are applied
339
+ const wrappedCallback = (res) => {
340
+ const statusCode = res.statusCode || 0;
341
+ span.setAttribute('http.status_code', statusCode);
342
+ if (statusCode >= 400) {
343
+ span.setStatus('error', `HTTP ${statusCode}`);
344
+ }
345
+ span.end();
346
+ integration._onSpanEnd(span);
347
+ if (callback) {
348
+ callback(res);
349
+ }
350
+ };
351
+ let req;
352
+ try {
353
+ req = original.call(this, options, wrappedCallback);
354
+ }
355
+ catch (err) {
356
+ // If the original call throws, end span with error
357
+ span.setStatus('error', err instanceof Error ? err.message : String(err));
358
+ span.end();
359
+ integration._onSpanEnd(span);
360
+ throw err;
361
+ }
362
+ req.on('error', (err) => {
363
+ if (!span.isEnded) {
364
+ span.setStatus('error', err.message);
365
+ span.end();
366
+ integration._onSpanEnd(span);
367
+ }
368
+ });
369
+ return req;
370
+ }
371
+ catch (error) {
372
+ // If our instrumentation code fails, fall through to original
373
+ return original.apply(this, args);
374
+ }
375
+ };
376
+ }
377
+ /**
378
+ * Wrap an outgoing `http.get` or `https.get` to create client spans.
379
+ *
380
+ * `http.get` is like `http.request` but auto-calls `req.end()`.
381
+ *
382
+ * @param original - The original get function
383
+ * @param protocol - 'http' or 'https'
384
+ * @returns A wrapped version of the get function
385
+ */
386
+ _wrapOutgoingGet(original, protocol) {
387
+ const integration = this;
388
+ // Use the already-patched request for get
389
+ const patchedRequest = this._wrapOutgoingRequest(protocol === 'http' ? (this._originalHttpRequest || http_1.default.request) : (this._originalHttpsRequest || https_1.default.request), protocol);
390
+ return function patchedGet(...args) {
391
+ const req = patchedRequest.apply(this, args);
392
+ req.end();
393
+ return req;
394
+ };
395
+ }
396
+ /**
397
+ * Create a server span for an incoming HTTP request.
398
+ *
399
+ * @param req - The incoming HTTP request
400
+ * @param res - The server response
401
+ * @returns The created Span, or null if instrumentation should be skipped
402
+ */
403
+ _instrumentIncoming(req, res) {
404
+ try {
405
+ const urlString = req.url || '/';
406
+ const method = (req.method || 'GET').toUpperCase();
407
+ // Parse traceparent from incoming headers
408
+ const traceparentHeader = req.headers['traceparent'];
409
+ let traceId;
410
+ let parentSpanId;
411
+ if (traceparentHeader) {
412
+ const parsed = (0, headers_1.parseTraceparent)(traceparentHeader);
413
+ if (parsed) {
414
+ traceId = parsed.traceId;
415
+ parentSpanId = parsed.parentSpanId;
416
+ }
417
+ else {
418
+ // Invalid traceparent -- generate new trace
419
+ traceId = (0, id_1.generateTraceId)();
420
+ parentSpanId = null;
421
+ }
422
+ }
423
+ else {
424
+ traceId = (0, id_1.generateTraceId)();
425
+ parentSpanId = null;
426
+ }
427
+ // Create server span
428
+ const span = new span_1.Span({
429
+ operationName: `${method} ${urlString}`,
430
+ serviceName: this._serviceName,
431
+ kind: 'server',
432
+ traceId,
433
+ parentSpanId,
434
+ attributes: {
435
+ 'http.method': method,
436
+ 'http.url': urlString,
437
+ 'http.host': req.headers.host || 'unknown',
438
+ 'http.user_agent': req.headers['user-agent'] || '',
439
+ },
440
+ });
441
+ const integration = this;
442
+ // End span when response finishes
443
+ res.on('finish', () => {
444
+ try {
445
+ span.setAttribute('http.status_code', res.statusCode);
446
+ if (res.statusCode >= 500) {
447
+ span.setStatus('error', `HTTP ${res.statusCode}`);
448
+ }
449
+ span.end();
450
+ integration._onSpanEnd(span);
451
+ }
452
+ catch {
453
+ // Never crash the request pipeline
454
+ }
455
+ });
456
+ return span;
457
+ }
458
+ catch {
459
+ // Instrumentation failure -- do not crash
460
+ return null;
461
+ }
462
+ }
463
+ }
464
+ exports.HttpIntegration = HttpIntegration;
465
+ //# sourceMappingURL=http.js.map