@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.
- package/dist/client.d.ts +286 -0
- package/dist/client.js +681 -0
- package/dist/client.js.map +1 -0
- package/dist/context.d.ts +126 -0
- package/dist/context.js +170 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +135 -0
- package/dist/errors.js +180 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +314 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express.d.ts +77 -0
- package/dist/integrations/express.js +87 -0
- package/dist/integrations/express.js.map +1 -0
- package/dist/integrations/http.d.ts +129 -0
- package/dist/integrations/http.js +465 -0
- package/dist/integrations/http.js.map +1 -0
- package/dist/integrations/metrics.d.ts +110 -0
- package/dist/integrations/metrics.js +313 -0
- package/dist/integrations/metrics.js.map +1 -0
- package/dist/integrations/next.d.ts +252 -0
- package/dist/integrations/next.js +480 -0
- package/dist/integrations/next.js.map +1 -0
- package/dist/integrations/pg.d.ts +169 -0
- package/dist/integrations/pg.js +616 -0
- package/dist/integrations/pg.js.map +1 -0
- package/dist/integrations/pino.d.ts +52 -0
- package/dist/integrations/pino.js +153 -0
- package/dist/integrations/pino.js.map +1 -0
- package/dist/integrations/redis.d.ts +190 -0
- package/dist/integrations/redis.js +597 -0
- package/dist/integrations/redis.js.map +1 -0
- package/dist/integrations/winston.d.ts +48 -0
- package/dist/integrations/winston.js +99 -0
- package/dist/integrations/winston.js.map +1 -0
- package/dist/logger.d.ts +148 -0
- package/dist/logger.js +162 -0
- package/dist/logger.js.map +1 -0
- package/dist/span.d.ts +192 -0
- package/dist/span.js +197 -0
- package/dist/span.js.map +1 -0
- package/dist/transport.d.ts +246 -0
- package/dist/transport.js +654 -0
- package/dist/transport.js.map +1 -0
- package/dist/utils/headers.d.ts +60 -0
- package/dist/utils/headers.js +93 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/id.d.ts +23 -0
- package/dist/utils/id.js +36 -0
- package/dist/utils/id.js.map +1 -0
- 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
|