@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
package/dist/client.js
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file packages/node-sdk/src/client.ts
|
|
4
|
+
* @description JustAnalyticsClient - the core SDK class managing configuration,
|
|
5
|
+
* lifecycle, span creation, error capture, context, and transport.
|
|
6
|
+
*
|
|
7
|
+
* Implements Story 035 - Node.js SDK Core
|
|
8
|
+
* Updated for Story 041 - Server-Side Error Tracking via SDK
|
|
9
|
+
* Updated for Story 046 - SDK Log Integration
|
|
10
|
+
* Updated for Story 048 - Infrastructure Metrics Collection
|
|
11
|
+
*
|
|
12
|
+
* The client is the central orchestrator:
|
|
13
|
+
* - Validates and stores configuration from `init()`
|
|
14
|
+
* - Creates spans via `startSpan()` and runs callbacks in AsyncLocalStorage context
|
|
15
|
+
* - Captures errors via `captureException()` and `captureMessage()`
|
|
16
|
+
* - Manages the BatchTransport for periodic flushing
|
|
17
|
+
* - Registers process exit handlers for graceful shutdown
|
|
18
|
+
* - Registers process error handlers for uncaughtException/unhandledRejection
|
|
19
|
+
* - Provides `setUser()`, `setTag()`, `getActiveSpan()`, `getTraceId()` context APIs
|
|
20
|
+
*
|
|
21
|
+
* When `enabled: false`, all methods become no-ops (spans are not created,
|
|
22
|
+
* errors are not captured, transport does not flush). This allows toggling
|
|
23
|
+
* the SDK off in environments where tracing is not desired.
|
|
24
|
+
*
|
|
25
|
+
* References:
|
|
26
|
+
* - src/app/api/ingest/spans/route.ts (target endpoint)
|
|
27
|
+
* - src/app/api/ingest/errors/route.ts (error ingestion endpoint)
|
|
28
|
+
* - Expansion Roadmap (architecture decisions, SDK design)
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.JustAnalyticsClient = void 0;
|
|
32
|
+
const span_1 = require("./span");
|
|
33
|
+
const context_1 = require("./context");
|
|
34
|
+
const transport_1 = require("./transport");
|
|
35
|
+
const id_1 = require("./utils/id");
|
|
36
|
+
const errors_1 = require("./errors");
|
|
37
|
+
const logger_1 = require("./logger");
|
|
38
|
+
const http_1 = require("./integrations/http");
|
|
39
|
+
const pg_1 = require("./integrations/pg");
|
|
40
|
+
const redis_1 = require("./integrations/redis");
|
|
41
|
+
const metrics_1 = require("./integrations/metrics");
|
|
42
|
+
const express_1 = require("./integrations/express");
|
|
43
|
+
/** Default JustAnalytics server URL */
|
|
44
|
+
const DEFAULT_SERVER_URL = 'https://justanalytics.up.railway.app';
|
|
45
|
+
/**
|
|
46
|
+
* JustAnalyticsClient manages the SDK lifecycle, span creation, error capture,
|
|
47
|
+
* context propagation, and batched transport.
|
|
48
|
+
*/
|
|
49
|
+
class JustAnalyticsClient {
|
|
50
|
+
constructor() {
|
|
51
|
+
this._initialized = false;
|
|
52
|
+
this._enabled = true;
|
|
53
|
+
this._serviceName = '';
|
|
54
|
+
this._debug = false;
|
|
55
|
+
this._transport = null;
|
|
56
|
+
this._logger = null;
|
|
57
|
+
this._httpIntegration = null;
|
|
58
|
+
this._pgIntegration = null;
|
|
59
|
+
this._redisIntegration = null;
|
|
60
|
+
this._metricsIntegration = null;
|
|
61
|
+
this._exitHandlersRegistered = false;
|
|
62
|
+
// Store references to bound handlers so we can remove them
|
|
63
|
+
this._boundBeforeExitHandler = null;
|
|
64
|
+
this._boundSigtermHandler = null;
|
|
65
|
+
this._boundSigintHandler = null;
|
|
66
|
+
// Process error handlers (Story 041)
|
|
67
|
+
this._boundUncaughtExceptionHandler = null;
|
|
68
|
+
this._boundUnhandledRejectionHandler = null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get a shared no-op Logger instance.
|
|
72
|
+
* Used when the SDK is not initialized or has been closed.
|
|
73
|
+
* All methods on this logger are no-ops (enabled: false, transport: null).
|
|
74
|
+
*/
|
|
75
|
+
static getNoopLogger() {
|
|
76
|
+
if (!JustAnalyticsClient._noopLogger) {
|
|
77
|
+
JustAnalyticsClient._noopLogger = new logger_1.Logger({
|
|
78
|
+
serviceName: 'unknown',
|
|
79
|
+
transport: null,
|
|
80
|
+
debug: false,
|
|
81
|
+
enabled: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return JustAnalyticsClient._noopLogger;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Initialize the SDK with the given options.
|
|
88
|
+
*
|
|
89
|
+
* Must be called before any other method. Can only be called once;
|
|
90
|
+
* subsequent calls log a warning and are ignored (idempotent guard).
|
|
91
|
+
*
|
|
92
|
+
* @param options - SDK configuration
|
|
93
|
+
* @throws Error if required fields (siteId, apiKey, serviceName) are missing
|
|
94
|
+
*/
|
|
95
|
+
init(options) {
|
|
96
|
+
if (this._initialized) {
|
|
97
|
+
console.warn('[JustAnalytics] Warning: init() already called. Ignoring.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Validate required fields
|
|
101
|
+
if (!options.siteId) {
|
|
102
|
+
throw new Error('[JustAnalytics] init() requires "siteId". Get it from your JustAnalytics dashboard.');
|
|
103
|
+
}
|
|
104
|
+
if (!options.apiKey) {
|
|
105
|
+
throw new Error('[JustAnalytics] init() requires "apiKey". Create one in your JustAnalytics site settings.');
|
|
106
|
+
}
|
|
107
|
+
if (!options.serviceName) {
|
|
108
|
+
throw new Error('[JustAnalytics] init() requires "serviceName" (e.g. "api-server", "worker").');
|
|
109
|
+
}
|
|
110
|
+
this._enabled = options.enabled !== false;
|
|
111
|
+
this._serviceName = options.serviceName;
|
|
112
|
+
this._environment = options.environment;
|
|
113
|
+
this._release = options.release;
|
|
114
|
+
this._debug = options.debug === true;
|
|
115
|
+
const serverUrl = options.serverUrl ||
|
|
116
|
+
process.env.JUSTANALYTICS_URL ||
|
|
117
|
+
DEFAULT_SERVER_URL;
|
|
118
|
+
if (this._debug) {
|
|
119
|
+
const keyPrefix = options.apiKey.substring(0, 10);
|
|
120
|
+
console.debug(`[JustAnalytics] Initializing SDK: service=${options.serviceName}, ` +
|
|
121
|
+
`server=${serverUrl}, apiKey=${keyPrefix}..., enabled=${this._enabled}`);
|
|
122
|
+
}
|
|
123
|
+
if (this._enabled) {
|
|
124
|
+
this._transport = new transport_1.BatchTransport({
|
|
125
|
+
serverUrl,
|
|
126
|
+
apiKey: options.apiKey,
|
|
127
|
+
flushIntervalMs: options.flushIntervalMs ?? 2000,
|
|
128
|
+
maxBatchSize: options.maxBatchSize ?? 100,
|
|
129
|
+
debug: this._debug,
|
|
130
|
+
});
|
|
131
|
+
this._transport.start();
|
|
132
|
+
this.registerExitHandlers();
|
|
133
|
+
// Create Logger instance with transport connected (Story 046)
|
|
134
|
+
this._logger = new logger_1.Logger({
|
|
135
|
+
serviceName: this._serviceName,
|
|
136
|
+
transport: this._transport,
|
|
137
|
+
debug: this._debug,
|
|
138
|
+
enabled: this._enabled,
|
|
139
|
+
environment: this._environment,
|
|
140
|
+
release: this._release,
|
|
141
|
+
});
|
|
142
|
+
// Initialize HTTP auto-instrumentation integration
|
|
143
|
+
const httpConfig = options.integrations?.http;
|
|
144
|
+
if (httpConfig !== false) {
|
|
145
|
+
const httpOptions = typeof httpConfig === 'object' ? httpConfig : undefined;
|
|
146
|
+
this._httpIntegration = new http_1.HttpIntegration(this._serviceName, serverUrl, httpOptions, (span) => this.enqueueSpan(span));
|
|
147
|
+
this._httpIntegration.enable();
|
|
148
|
+
}
|
|
149
|
+
// Initialize PostgreSQL auto-instrumentation integration
|
|
150
|
+
const pgConfig = options.integrations?.pg;
|
|
151
|
+
if (pgConfig !== false) {
|
|
152
|
+
const pgOptions = typeof pgConfig === 'object' ? pgConfig : undefined;
|
|
153
|
+
this._pgIntegration = new pg_1.PgIntegration(this._serviceName, pgOptions, (span) => this.enqueueSpan(span));
|
|
154
|
+
this._pgIntegration.enable();
|
|
155
|
+
}
|
|
156
|
+
// Initialize Redis auto-instrumentation integration
|
|
157
|
+
const redisConfig = options.integrations?.redis;
|
|
158
|
+
if (redisConfig !== false) {
|
|
159
|
+
const redisOptions = typeof redisConfig === 'object' ? redisConfig : undefined;
|
|
160
|
+
this._redisIntegration = new redis_1.RedisIntegration(this._serviceName, redisOptions, (span) => this.enqueueSpan(span));
|
|
161
|
+
this._redisIntegration.enable();
|
|
162
|
+
}
|
|
163
|
+
// Initialize infrastructure metrics integration (Story 048)
|
|
164
|
+
const metricsConfig = options.integrations?.metrics;
|
|
165
|
+
if (metricsConfig !== false) {
|
|
166
|
+
const metricsOptions = typeof metricsConfig === 'object' ? metricsConfig : undefined;
|
|
167
|
+
this._metricsIntegration = new metrics_1.MetricsIntegration(this._serviceName, this._transport, this._debug, this._environment, metricsOptions);
|
|
168
|
+
this._metricsIntegration.enable();
|
|
169
|
+
}
|
|
170
|
+
// Register process error handlers (Story 041)
|
|
171
|
+
this.registerProcessErrorHandlers(options);
|
|
172
|
+
}
|
|
173
|
+
this._initialized = true;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether `init()` has been called successfully.
|
|
177
|
+
*/
|
|
178
|
+
isInitialized() {
|
|
179
|
+
return this._initialized;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get the Logger instance.
|
|
183
|
+
*
|
|
184
|
+
* Returns a no-op Logger if init() has not been called yet,
|
|
185
|
+
* if the SDK is disabled, or after close() has been called.
|
|
186
|
+
* Never returns null -- always safe to call methods on the result.
|
|
187
|
+
*/
|
|
188
|
+
get logger() {
|
|
189
|
+
return this._logger || JustAnalyticsClient.getNoopLogger();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create and execute a span.
|
|
193
|
+
*
|
|
194
|
+
* Creates a new Span, runs the callback inside an AsyncLocalStorage context
|
|
195
|
+
* with the span as the active span, and ends the span when the callback
|
|
196
|
+
* completes (sync or async).
|
|
197
|
+
*
|
|
198
|
+
* If the callback throws, the span is ended with `status: 'error'` and the
|
|
199
|
+
* error is re-thrown.
|
|
200
|
+
*
|
|
201
|
+
* Supports two signatures:
|
|
202
|
+
* - `startSpan(name, callback)` -- default options
|
|
203
|
+
* - `startSpan(name, options, callback)` -- with SpanOptions
|
|
204
|
+
*
|
|
205
|
+
* @param name - Operation name for the span
|
|
206
|
+
* @param callbackOrOptions - Either the callback or SpanOptions
|
|
207
|
+
* @param maybeCallback - The callback (if options were provided)
|
|
208
|
+
* @returns The return value of the callback
|
|
209
|
+
*/
|
|
210
|
+
startSpan(name, callbackOrOptions, maybeCallback) {
|
|
211
|
+
// Parse overloaded arguments
|
|
212
|
+
let options;
|
|
213
|
+
let callback;
|
|
214
|
+
if (typeof callbackOrOptions === 'function') {
|
|
215
|
+
callback = callbackOrOptions;
|
|
216
|
+
options = undefined;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
options = callbackOrOptions;
|
|
220
|
+
if (!maybeCallback) {
|
|
221
|
+
throw new Error('[JustAnalytics] startSpan() requires a callback function.');
|
|
222
|
+
}
|
|
223
|
+
callback = maybeCallback;
|
|
224
|
+
}
|
|
225
|
+
// No-op if not initialized or disabled
|
|
226
|
+
if (!this._initialized || !this._enabled) {
|
|
227
|
+
// Create a dummy span for the callback, but don't enqueue it
|
|
228
|
+
const dummySpan = new span_1.Span({
|
|
229
|
+
operationName: name,
|
|
230
|
+
serviceName: this._serviceName || 'unknown',
|
|
231
|
+
kind: options?.kind || 'internal',
|
|
232
|
+
traceId: (0, id_1.generateTraceId)(),
|
|
233
|
+
parentSpanId: null,
|
|
234
|
+
});
|
|
235
|
+
return callback(dummySpan);
|
|
236
|
+
}
|
|
237
|
+
// Determine parent span and trace ID
|
|
238
|
+
let parentSpan = options?.parentSpan || (0, context_1.getActiveSpan)();
|
|
239
|
+
let traceId;
|
|
240
|
+
let parentSpanId;
|
|
241
|
+
if (parentSpan) {
|
|
242
|
+
traceId = parentSpan.traceId;
|
|
243
|
+
parentSpanId = parentSpan.id;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
traceId = (0, id_1.generateTraceId)();
|
|
247
|
+
parentSpanId = null;
|
|
248
|
+
}
|
|
249
|
+
// Merge context tags into initial attributes
|
|
250
|
+
const contextTags = this.getContextTags();
|
|
251
|
+
const initialAttributes = {
|
|
252
|
+
...contextTags,
|
|
253
|
+
...(options?.attributes || {}),
|
|
254
|
+
};
|
|
255
|
+
// Add environment and release as attributes if set
|
|
256
|
+
if (this._environment) {
|
|
257
|
+
initialAttributes['environment'] = this._environment;
|
|
258
|
+
}
|
|
259
|
+
if (this._release) {
|
|
260
|
+
initialAttributes['release'] = this._release;
|
|
261
|
+
}
|
|
262
|
+
// Create the span
|
|
263
|
+
const span = new span_1.Span({
|
|
264
|
+
operationName: name,
|
|
265
|
+
serviceName: this._serviceName,
|
|
266
|
+
kind: options?.kind || 'internal',
|
|
267
|
+
traceId,
|
|
268
|
+
parentSpanId,
|
|
269
|
+
attributes: initialAttributes,
|
|
270
|
+
});
|
|
271
|
+
// Create child context with this span as active
|
|
272
|
+
const childContext = (0, context_1.createChildContext)({
|
|
273
|
+
activeSpan: span,
|
|
274
|
+
traceId,
|
|
275
|
+
});
|
|
276
|
+
// Run callback in context
|
|
277
|
+
return (0, context_1.runWithContext)(childContext, () => {
|
|
278
|
+
try {
|
|
279
|
+
const result = callback(span);
|
|
280
|
+
// Handle async callbacks
|
|
281
|
+
if (result instanceof Promise) {
|
|
282
|
+
return result.then((value) => {
|
|
283
|
+
if (!span.isEnded) {
|
|
284
|
+
span.end();
|
|
285
|
+
this.enqueueSpan(span);
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}, (error) => {
|
|
289
|
+
if (!span.isEnded) {
|
|
290
|
+
span.setStatus('error', error instanceof Error ? error.message : String(error));
|
|
291
|
+
span.end();
|
|
292
|
+
this.enqueueSpan(span);
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Sync callback completed successfully
|
|
298
|
+
if (!span.isEnded) {
|
|
299
|
+
span.end();
|
|
300
|
+
this.enqueueSpan(span);
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
// Sync callback threw an error
|
|
306
|
+
if (!span.isEnded) {
|
|
307
|
+
span.setStatus('error', error instanceof Error ? error.message : String(error));
|
|
308
|
+
span.end();
|
|
309
|
+
this.enqueueSpan(span);
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Capture an exception and send it to JustAnalytics.
|
|
317
|
+
*
|
|
318
|
+
* Automatically attaches the current traceId, spanId, user context,
|
|
319
|
+
* and tags from the AsyncLocalStorage context.
|
|
320
|
+
*
|
|
321
|
+
* @param error - Error object (or any value, which will be coerced)
|
|
322
|
+
* @param options - Optional tags, extra, user, level, fingerprint
|
|
323
|
+
* @returns A unique eventId string, or empty string if SDK is disabled
|
|
324
|
+
*/
|
|
325
|
+
captureException(error, options) {
|
|
326
|
+
if (!this._initialized || !this._enabled)
|
|
327
|
+
return '';
|
|
328
|
+
try {
|
|
329
|
+
const payload = (0, errors_1.buildErrorPayload)(error, options, 'manual', true, this._serviceName, this._environment, this._release, 'error');
|
|
330
|
+
this.enqueueError(payload);
|
|
331
|
+
return payload.eventId;
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (this._debug) {
|
|
335
|
+
console.debug('[JustAnalytics] captureException() internal error:', err);
|
|
336
|
+
}
|
|
337
|
+
return '';
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Capture a message and send it to JustAnalytics.
|
|
342
|
+
*
|
|
343
|
+
* @param message - The message string
|
|
344
|
+
* @param levelOrOptions - Severity level or CaptureOptions
|
|
345
|
+
* @param options - CaptureOptions (if level was provided as second arg)
|
|
346
|
+
* @returns A unique eventId string, or empty string if SDK is disabled
|
|
347
|
+
*/
|
|
348
|
+
captureMessage(message, levelOrOptions, options) {
|
|
349
|
+
if (!this._initialized || !this._enabled)
|
|
350
|
+
return '';
|
|
351
|
+
try {
|
|
352
|
+
let resolvedLevel = 'info';
|
|
353
|
+
let resolvedOptions;
|
|
354
|
+
if (typeof levelOrOptions === 'string') {
|
|
355
|
+
resolvedLevel = levelOrOptions;
|
|
356
|
+
resolvedOptions = options;
|
|
357
|
+
}
|
|
358
|
+
else if (typeof levelOrOptions === 'object') {
|
|
359
|
+
resolvedOptions = levelOrOptions;
|
|
360
|
+
resolvedLevel = levelOrOptions.level || 'info';
|
|
361
|
+
}
|
|
362
|
+
const payload = (0, errors_1.buildErrorPayload)(message, { ...resolvedOptions, level: resolvedLevel }, 'manual', true, this._serviceName, this._environment, this._release, resolvedLevel);
|
|
363
|
+
// Override type to 'Message' for captureMessage
|
|
364
|
+
payload.error.type = 'Message';
|
|
365
|
+
this.enqueueError(payload);
|
|
366
|
+
return payload.eventId;
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
if (this._debug) {
|
|
370
|
+
console.debug('[JustAnalytics] captureMessage() internal error:', err);
|
|
371
|
+
}
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Set user context for the current async scope.
|
|
377
|
+
*
|
|
378
|
+
* The user context is available to all spans created within this scope.
|
|
379
|
+
* Uses copy-on-write semantics via AsyncLocalStorage.
|
|
380
|
+
*
|
|
381
|
+
* @param user - User context (id, email, username)
|
|
382
|
+
*/
|
|
383
|
+
setUser(user) {
|
|
384
|
+
if (!this._initialized || !this._enabled)
|
|
385
|
+
return;
|
|
386
|
+
(0, context_1.setUserInContext)(user);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Set a tag for the current async scope.
|
|
390
|
+
*
|
|
391
|
+
* Tags are attached as attributes on all spans created within this scope.
|
|
392
|
+
* Uses copy-on-write semantics via AsyncLocalStorage.
|
|
393
|
+
*
|
|
394
|
+
* @param key - Tag key
|
|
395
|
+
* @param value - Tag value
|
|
396
|
+
*/
|
|
397
|
+
setTag(key, value) {
|
|
398
|
+
if (!this._initialized || !this._enabled)
|
|
399
|
+
return;
|
|
400
|
+
(0, context_1.setTagInContext)(key, value);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Get the currently active span from AsyncLocalStorage.
|
|
404
|
+
*
|
|
405
|
+
* @returns The active Span, or null if not in a traced context
|
|
406
|
+
*/
|
|
407
|
+
getActiveSpan() {
|
|
408
|
+
if (!this._initialized || !this._enabled)
|
|
409
|
+
return null;
|
|
410
|
+
return (0, context_1.getActiveSpan)();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get the current trace ID from AsyncLocalStorage.
|
|
414
|
+
*
|
|
415
|
+
* @returns The trace ID string, or null if not in a traced context
|
|
416
|
+
*/
|
|
417
|
+
getTraceId() {
|
|
418
|
+
if (!this._initialized || !this._enabled)
|
|
419
|
+
return null;
|
|
420
|
+
return (0, context_1.getActiveTraceId)();
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get an Express middleware function that updates server span operation
|
|
424
|
+
* names with the matched Express route pattern.
|
|
425
|
+
*
|
|
426
|
+
* @returns Express-compatible middleware: `(req, res, next) => void`
|
|
427
|
+
*/
|
|
428
|
+
expressMiddleware() {
|
|
429
|
+
return (0, express_1.expressMiddleware)();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Record a custom infrastructure metric.
|
|
433
|
+
*
|
|
434
|
+
* Constructs a MetricPayload with the provided name, value, and tags,
|
|
435
|
+
* plus serviceName, timestamp, and default tags (hostname, environment).
|
|
436
|
+
*
|
|
437
|
+
* @param metricName - Metric name using dot notation (e.g., 'custom.queue_size')
|
|
438
|
+
* @param value - Numeric value
|
|
439
|
+
* @param tags - Optional additional tags
|
|
440
|
+
*/
|
|
441
|
+
recordMetric(metricName, value, tags) {
|
|
442
|
+
if (!this._initialized || !this._enabled)
|
|
443
|
+
return;
|
|
444
|
+
try {
|
|
445
|
+
const payload = {
|
|
446
|
+
metricName,
|
|
447
|
+
value,
|
|
448
|
+
serviceName: this._serviceName,
|
|
449
|
+
timestamp: new Date().toISOString(),
|
|
450
|
+
tags: {
|
|
451
|
+
hostname: require('os').hostname(),
|
|
452
|
+
...(this._environment ? { environment: this._environment } : {}),
|
|
453
|
+
...(tags || {}),
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
this._transport?.enqueueMetric(payload);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
if (this._debug) {
|
|
460
|
+
console.debug('[JustAnalytics] recordMetric() internal error:', err);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Manually flush all pending spans and errors to the server.
|
|
466
|
+
*
|
|
467
|
+
* @returns Promise that resolves when the flush completes
|
|
468
|
+
*/
|
|
469
|
+
async flush() {
|
|
470
|
+
if (!this._transport)
|
|
471
|
+
return;
|
|
472
|
+
await this._transport.flush();
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Shut down the SDK: flush remaining data, stop timers, deregister handlers.
|
|
476
|
+
*
|
|
477
|
+
* After calling `close()`, the SDK cannot be used again until `init()` is called.
|
|
478
|
+
*/
|
|
479
|
+
async close() {
|
|
480
|
+
if (!this._initialized)
|
|
481
|
+
return;
|
|
482
|
+
if (this._debug) {
|
|
483
|
+
console.debug('[JustAnalytics] Closing SDK...');
|
|
484
|
+
}
|
|
485
|
+
// Disable HTTP integration (restore original functions)
|
|
486
|
+
if (this._httpIntegration) {
|
|
487
|
+
this._httpIntegration.disable();
|
|
488
|
+
this._httpIntegration = null;
|
|
489
|
+
}
|
|
490
|
+
// Disable PostgreSQL integration (restore original functions)
|
|
491
|
+
if (this._pgIntegration) {
|
|
492
|
+
this._pgIntegration.disable();
|
|
493
|
+
this._pgIntegration = null;
|
|
494
|
+
}
|
|
495
|
+
// Disable Redis integration (restore original functions)
|
|
496
|
+
if (this._redisIntegration) {
|
|
497
|
+
this._redisIntegration.disable();
|
|
498
|
+
this._redisIntegration = null;
|
|
499
|
+
}
|
|
500
|
+
// Disable metrics integration (Story 048)
|
|
501
|
+
if (this._metricsIntegration) {
|
|
502
|
+
this._metricsIntegration.disable();
|
|
503
|
+
this._metricsIntegration = null;
|
|
504
|
+
}
|
|
505
|
+
// Flush remaining spans, errors, logs, and metrics
|
|
506
|
+
if (this._transport) {
|
|
507
|
+
await this._transport.flush();
|
|
508
|
+
this._transport.stop();
|
|
509
|
+
this._transport = null;
|
|
510
|
+
}
|
|
511
|
+
// Set logger to null after flush (Story 046)
|
|
512
|
+
// The getter will return the no-op logger after this
|
|
513
|
+
this._logger = null;
|
|
514
|
+
this.deregisterExitHandlers();
|
|
515
|
+
this.deregisterProcessErrorHandlers();
|
|
516
|
+
this._initialized = false;
|
|
517
|
+
if (this._debug) {
|
|
518
|
+
console.debug('[JustAnalytics] SDK closed.');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Enqueue an ended span for transport.
|
|
523
|
+
*
|
|
524
|
+
* @param span - The ended span to enqueue
|
|
525
|
+
*/
|
|
526
|
+
enqueueSpan(span) {
|
|
527
|
+
if (!this._transport)
|
|
528
|
+
return;
|
|
529
|
+
this._transport.enqueue(span.toJSON());
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Enqueue an error event payload for transport.
|
|
533
|
+
*
|
|
534
|
+
* @param payload - The error event payload to enqueue
|
|
535
|
+
*/
|
|
536
|
+
enqueueError(payload) {
|
|
537
|
+
if (!this._transport)
|
|
538
|
+
return;
|
|
539
|
+
this._transport.enqueueError(payload);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Get tags from the current AsyncLocalStorage context.
|
|
543
|
+
*
|
|
544
|
+
* @returns Current context tags, or empty object if none
|
|
545
|
+
*/
|
|
546
|
+
getContextTags() {
|
|
547
|
+
return (0, context_1.getTags)();
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Register process error handlers for uncaught exceptions and unhandled rejections.
|
|
551
|
+
*
|
|
552
|
+
* These handlers capture the error via the SDK and ensure it is sent to the server.
|
|
553
|
+
* For uncaughtException, the process exits after a 2-second safety timeout.
|
|
554
|
+
* For unhandledRejection, the error is batched (process continues).
|
|
555
|
+
*
|
|
556
|
+
* @param options - SDK configuration options
|
|
557
|
+
*/
|
|
558
|
+
registerProcessErrorHandlers(options) {
|
|
559
|
+
// Register uncaught exception handler
|
|
560
|
+
if (options.enableUncaughtExceptionHandler !== false) {
|
|
561
|
+
this._boundUncaughtExceptionHandler = (error) => {
|
|
562
|
+
try {
|
|
563
|
+
const payload = (0, errors_1.buildErrorPayload)(error, { level: 'fatal', tags: { 'error.mechanism': 'uncaughtException' } }, 'uncaughtException', false, this._serviceName, this._environment, this._release, 'fatal');
|
|
564
|
+
// Send immediately (not batched) since process is about to exit
|
|
565
|
+
if (this._transport) {
|
|
566
|
+
this._transport.sendErrorImmediate(payload);
|
|
567
|
+
// Also flush any pending spans and errors
|
|
568
|
+
this._transport.flush().catch(() => { });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// Never throw from the handler
|
|
573
|
+
}
|
|
574
|
+
// Safety net: exit after 2 seconds regardless
|
|
575
|
+
const exitTimer = setTimeout(() => process.exit(1), 2000);
|
|
576
|
+
if (typeof exitTimer.unref === 'function') {
|
|
577
|
+
exitTimer.unref();
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
process.on('uncaughtException', this._boundUncaughtExceptionHandler);
|
|
581
|
+
}
|
|
582
|
+
// Register unhandled rejection handler
|
|
583
|
+
if (options.enableUnhandledRejectionHandler !== false) {
|
|
584
|
+
this._boundUnhandledRejectionHandler = (reason) => {
|
|
585
|
+
try {
|
|
586
|
+
const payload = (0, errors_1.buildErrorPayload)(reason, { level: 'error', tags: { 'error.mechanism': 'unhandledRejection' } }, 'unhandledRejection', false, this._serviceName, this._environment, this._release, 'error');
|
|
587
|
+
this.enqueueError(payload);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// Never throw from the handler
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
process.on('unhandledRejection', this._boundUnhandledRejectionHandler);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Deregister process error handlers.
|
|
598
|
+
*
|
|
599
|
+
* Called by `close()` to clean up uncaughtException and unhandledRejection listeners.
|
|
600
|
+
*/
|
|
601
|
+
deregisterProcessErrorHandlers() {
|
|
602
|
+
if (this._boundUncaughtExceptionHandler) {
|
|
603
|
+
process.removeListener('uncaughtException', this._boundUncaughtExceptionHandler);
|
|
604
|
+
this._boundUncaughtExceptionHandler = null;
|
|
605
|
+
}
|
|
606
|
+
if (this._boundUnhandledRejectionHandler) {
|
|
607
|
+
process.removeListener('unhandledRejection', this._boundUnhandledRejectionHandler);
|
|
608
|
+
this._boundUnhandledRejectionHandler = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Register process exit handlers to flush remaining spans on shutdown.
|
|
613
|
+
*
|
|
614
|
+
* Handlers are registered only once (tracked by `_exitHandlersRegistered`).
|
|
615
|
+
*/
|
|
616
|
+
registerExitHandlers() {
|
|
617
|
+
if (this._exitHandlersRegistered)
|
|
618
|
+
return;
|
|
619
|
+
this._boundBeforeExitHandler = () => {
|
|
620
|
+
if (this._transport && this._transport.pendingCount > 0) {
|
|
621
|
+
if (this._debug) {
|
|
622
|
+
console.debug(`[JustAnalytics] Process beforeExit: flushing ${this._transport.pendingCount} pending span(s)...`);
|
|
623
|
+
}
|
|
624
|
+
// beforeExit allows async work that will re-trigger the event
|
|
625
|
+
this._transport.flush().catch(() => { });
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
this._boundSigtermHandler = () => {
|
|
629
|
+
if (this._debug) {
|
|
630
|
+
console.debug('[JustAnalytics] Received SIGTERM, flushing...');
|
|
631
|
+
}
|
|
632
|
+
if (this._transport) {
|
|
633
|
+
this._transport.flush().then(() => process.exit(0), () => process.exit(0));
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
process.exit(0);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
this._boundSigintHandler = () => {
|
|
640
|
+
if (this._debug) {
|
|
641
|
+
console.debug('[JustAnalytics] Received SIGINT, flushing...');
|
|
642
|
+
}
|
|
643
|
+
if (this._transport) {
|
|
644
|
+
this._transport.flush().then(() => process.exit(0), () => process.exit(0));
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
process.on('beforeExit', this._boundBeforeExitHandler);
|
|
651
|
+
process.on('SIGTERM', this._boundSigtermHandler);
|
|
652
|
+
process.on('SIGINT', this._boundSigintHandler);
|
|
653
|
+
this._exitHandlersRegistered = true;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Deregister process exit handlers.
|
|
657
|
+
*
|
|
658
|
+
* Called by `close()` to clean up all registered handlers.
|
|
659
|
+
*/
|
|
660
|
+
deregisterExitHandlers() {
|
|
661
|
+
if (!this._exitHandlersRegistered)
|
|
662
|
+
return;
|
|
663
|
+
if (this._boundBeforeExitHandler) {
|
|
664
|
+
process.removeListener('beforeExit', this._boundBeforeExitHandler);
|
|
665
|
+
this._boundBeforeExitHandler = null;
|
|
666
|
+
}
|
|
667
|
+
if (this._boundSigtermHandler) {
|
|
668
|
+
process.removeListener('SIGTERM', this._boundSigtermHandler);
|
|
669
|
+
this._boundSigtermHandler = null;
|
|
670
|
+
}
|
|
671
|
+
if (this._boundSigintHandler) {
|
|
672
|
+
process.removeListener('SIGINT', this._boundSigintHandler);
|
|
673
|
+
this._boundSigintHandler = null;
|
|
674
|
+
}
|
|
675
|
+
this._exitHandlersRegistered = false;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
exports.JustAnalyticsClient = JustAnalyticsClient;
|
|
679
|
+
// Lazy no-op logger for pre-init access (Story 046)
|
|
680
|
+
JustAnalyticsClient._noopLogger = null;
|
|
681
|
+
//# sourceMappingURL=client.js.map
|