@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
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