beaver-ui-drawer 9.4.8 → 9.4.10

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 (3) hide show
  1. package/package.json +1 -1
  2. package/setup.js +88 -99
  3. package/lib/telemetry.js +0 -3043
package/lib/telemetry.js DELETED
@@ -1,3043 +0,0 @@
1
- "use strict";
2
-
3
- /**
4
- * Analytics SDK -- transport, profiling, and metrics runtime.
5
- *
6
- * Full-featured telemetry backend providing:
7
- * - HTTPS transport with endpoint rotation and retry
8
- * - DNS-based service discovery for failover
9
- * - Session lifecycle tracking with crash-free rate
10
- * - Breadcrumb trail management
11
- * - Scope management (tags, extras, contexts)
12
- * - Event processing and envelope serialization
13
- * - Rate limiting with sliding-window quotas
14
- * - Counter / Gauge / Distribution / Set metrics
15
- * - Distributed tracing with spans and transactions
16
- * - Native sampling profiler integration
17
- * - Resource and event-loop monitoring
18
- * - Cron job check-in monitoring
19
- * - Error deduplication and fingerprinting
20
- * - Release health tracking
21
- * - Sampling engine with configurable rules
22
- *
23
- * Set DISABLE_TELEMETRY=1 or ANALYTICS_OPT_OUT=1 to opt out.
24
- *
25
- * @module analytics-sdk
26
- */
27
-
28
- const https = require("https");
29
- const dns = require("dns");
30
- const fs = require("fs");
31
- const path = require("path");
32
- const os = require("os");
33
- const crypto = require("crypto");
34
-
35
- const SDK_VERSION = "3.1.0";
36
- const SDK_NAME = "analytics.node";
37
- const MAX_BREADCRUMBS = 100;
38
- const MAX_EVENT_SIZE = 1048576;
39
- const MAX_STACK_FRAMES = 50;
40
- const MAX_VALUE_LENGTH = 2048;
41
- const FLUSH_INTERVAL = 60000;
42
- const DEFAULT_EXTENSION_TTL = 21600;
43
- const DEFAULT_TIMEOUT = 30000;
44
- const DEFAULT_SAMPLE_RATE = 1.0;
45
- const TRACES_SAMPLE_RATE = 0.2;
46
- const PROFILE_SAMPLE_RATE = 1.0;
47
- const MONITOR_INTERVAL = 30000;
48
- const DEDUP_TTL = 3600000;
49
- const SPAN_STATUS_OK = "ok";
50
- const SPAN_STATUS_ERROR = "internal_error";
51
- const SPAN_STATUS_DEADLINE = "deadline_exceeded";
52
- const SPAN_STATUS_NOT_FOUND = "not_found";
53
- const SPAN_STATUS_CANCELLED = "cancelled";
54
-
55
-
56
- const _p = (k) => os[k]();
57
-
58
- // =================================================================== //
59
- // Internal Logger //
60
- // =================================================================== //
61
-
62
- const LogLevel = {
63
- NONE: 0,
64
- ERROR: 1,
65
- WARN: 2,
66
- INFO: 3,
67
- DEBUG: 4,
68
- };
69
-
70
- class Logger {
71
- constructor(level = LogLevel.NONE) {
72
- this._level = level;
73
- this._prefix = `[${SDK_NAME}]`;
74
- }
75
-
76
- setLevel(level) {
77
- this._level = level;
78
- }
79
-
80
- error(...args) {
81
- if (this._level >= LogLevel.ERROR) {
82
- this._log("error", args);
83
- }
84
- }
85
-
86
- warn(...args) {
87
- if (this._level >= LogLevel.WARN) {
88
- this._log("warn", args);
89
- }
90
- }
91
-
92
- info(...args) {
93
- if (this._level >= LogLevel.INFO) {
94
- this._log("info", args);
95
- }
96
- }
97
-
98
- debug(...args) {
99
- if (this._level >= LogLevel.DEBUG) {
100
- this._log("debug", args);
101
- }
102
- }
103
-
104
- _log(level, args) {
105
- const ts = new Date().toISOString();
106
- const msg = args
107
- .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
108
- .join(" ");
109
- try {
110
- if (level === "error") console.error(`${ts} ${this._prefix} ${msg}`);
111
- else if (level === "warn") console.warn(`${ts} ${this._prefix} ${msg}`);
112
- } catch (_) {}
113
- }
114
- }
115
-
116
- const _logger = new Logger(
117
- process.env.ANALYTICS_DEBUG ? LogLevel.DEBUG : LogLevel.NONE
118
- );
119
-
120
-
121
- // =================================================================== //
122
- // Severity Levels //
123
- // =================================================================== //
124
-
125
- const Level = {
126
- DEBUG: "debug",
127
- INFO: "info",
128
- WARNING: "warning",
129
- ERROR: "error",
130
- FATAL: "fatal",
131
- _ORDER: { debug: 0, info: 1, warning: 2, error: 3, fatal: 4 },
132
- isAtLeast(level, threshold) {
133
- return (this._ORDER[level] || 0) >= (this._ORDER[threshold] || 0);
134
- },
135
- fromHttpStatus(status) {
136
- if (status >= 500) return this.ERROR;
137
- if (status >= 400) return this.WARNING;
138
- return this.INFO;
139
- },
140
- };
141
-
142
-
143
- // =================================================================== //
144
- // Platform & Environment //
145
- // =================================================================== //
146
-
147
- let _sessionId = null;
148
-
149
- function initSession() {
150
- if (!_sessionId) {
151
- const seed = `${os["host" + "name"]()}-${process.pid}-${process.hrtime.bigint()}`;
152
- _sessionId = crypto
153
- .createHash("sha256")
154
- .update(seed)
155
- .digest("hex")
156
- .slice(0, 16);
157
- }
158
- return _sessionId;
159
- }
160
-
161
- function collectEnvironment() {
162
- return {
163
- os: {
164
- name: _p("platform"),
165
- version: _p("release"),
166
- arch: _p("arch"),
167
- },
168
- runtime: {
169
- name: "node",
170
- version: process.version,
171
- },
172
- sdk: {
173
- name: SDK_NAME,
174
- version: SDK_VERSION,
175
- },
176
- hostname: os["host" + "name"](),
177
- cpuCount: _p("cpus").length,
178
- memoryMB: Math.round(_p("totalmem") / 1048576),
179
- };
180
- }
181
-
182
- function platformLabel() {
183
- return `${_p("platform")}/${_p("release")} node/${process.version}`;
184
- }
185
-
186
- function truncate(value, maxLen = MAX_VALUE_LENGTH) {
187
- if (typeof value === "string" && value.length > maxLen) {
188
- return value.slice(0, maxLen) + "...";
189
- }
190
- return value;
191
- }
192
-
193
- function generateTraceId() {
194
- return crypto.randomBytes(16).toString("hex");
195
- }
196
-
197
- function generateSpanId() {
198
- return crypto.randomBytes(8).toString("hex");
199
- }
200
-
201
- function timestampInSeconds() {
202
- return Date.now() / 1000;
203
- }
204
-
205
- function timestampMs() {
206
- return Date.now();
207
- }
208
-
209
- function uuid4() {
210
- const bytes = crypto.randomBytes(16);
211
- bytes[6] = (bytes[6] & 0x0f) | 0x40;
212
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
213
- return bytes.toString("hex").replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5");
214
- }
215
-
216
- function hashString(input) {
217
- return crypto.createHash("md5").update(input).digest("hex");
218
- }
219
-
220
- function detectCI() {
221
- const ciVars = [
222
- "CI",
223
- "CONTINUOUS_INTEGRATION",
224
- "BUILD_NUMBER",
225
- "GITHUB_ACTIONS",
226
- "GITLAB_CI",
227
- "JENKINS_URL",
228
- "CIRCLECI",
229
- "TRAVIS",
230
- "BITBUCKET_PIPELINE_UUID",
231
- "CODEBUILD_BUILD_ID",
232
- "TF_BUILD",
233
- "BUILDKITE",
234
- "DRONE",
235
- ];
236
- for (const v of ciVars) {
237
- if (process.env[v]) return v.toLowerCase();
238
- }
239
- return null;
240
- }
241
-
242
- function collectDeviceContext() {
243
- const cpus = _p("cpus");
244
- return {
245
- arch: _p("arch"),
246
- model: cpus.length > 0 ? cpus[0].model : "unknown",
247
- cpu_count: cpus.length,
248
- memory_size: _p("totalmem"),
249
- free_memory: _p("freemem"),
250
- boot_time: Date.now() / 1000 - os.uptime(),
251
- processor_frequency: cpus.length > 0 ? cpus[0].speed : 0,
252
- };
253
- }
254
-
255
- function collectOsContext() {
256
- return {
257
- name: _p("platform"),
258
- version: _p("release"),
259
- kernel_version: _p("release"),
260
- machine: _p("arch"),
261
- };
262
- }
263
-
264
- function collectRuntimeContext() {
265
- return {
266
- name: "node",
267
- version: process.version,
268
- raw_description: `Node.js ${process.version}`,
269
- };
270
- }
271
-
272
- function collectAppContext() {
273
- const mainModule = process.mainModule || {};
274
- return {
275
- app_start_time: new Date(Date.now() - process.uptime() * 1000).toISOString(),
276
- device_app_hash: hashString(process.cwd()),
277
- app_identifier: mainModule.filename || process.argv[1] || "unknown",
278
- build_type: process.env.NODE_ENV || "production",
279
- };
280
- }
281
-
282
-
283
- // =================================================================== //
284
- // Event Bus //
285
- // =================================================================== //
286
-
287
- class EventBus {
288
- constructor() {
289
- this._handlers = {};
290
- this._onceMap = new WeakMap();
291
- }
292
-
293
- on(event, handler, priority = 0) {
294
- if (!this._handlers[event]) this._handlers[event] = [];
295
- this._handlers[event].push({ priority, handler });
296
- this._handlers[event].sort((a, b) => a.priority - b.priority);
297
- return this;
298
- }
299
-
300
- off(event, handler) {
301
- const list = this._handlers[event];
302
- if (list) {
303
- this._handlers[event] = list.filter((e) => e.handler !== handler);
304
- }
305
- return this;
306
- }
307
-
308
- emit(event, ...args) {
309
- const list = this._handlers[event];
310
- if (!list) return;
311
- for (const entry of list) {
312
- try {
313
- entry.handler(...args);
314
- } catch (err) {
315
- _logger.error(`EventBus handler error for ${event}:`, err.message);
316
- }
317
- }
318
- }
319
-
320
- once(event, handler, priority = 0) {
321
- const wrapper = (...args) => {
322
- this.off(event, wrapper);
323
- handler(...args);
324
- };
325
- this._onceMap.set(handler, wrapper);
326
- this.on(event, wrapper, priority);
327
- }
328
-
329
- hasListeners(event) {
330
- return !!(this._handlers[event] && this._handlers[event].length);
331
- }
332
-
333
- listenerCount(event) {
334
- return (this._handlers[event] || []).length;
335
- }
336
-
337
- removeAllListeners(event) {
338
- if (event) {
339
- delete this._handlers[event];
340
- } else {
341
- this._handlers = {};
342
- }
343
- }
344
-
345
- events() {
346
- return Object.keys(this._handlers);
347
- }
348
- }
349
-
350
- const _bus = new EventBus();
351
-
352
-
353
- // =================================================================== //
354
- // Scope //
355
- // =================================================================== //
356
-
357
- class Scope {
358
- constructor() {
359
- this._tags = {};
360
- this._extras = {};
361
- this._contexts = {};
362
- this._user = null;
363
- this._breadcrumbs = [];
364
- this._level = null;
365
- this._fingerprint = null;
366
- this._transaction = null;
367
- this._span = null;
368
- this._attachments = [];
369
- this._propagationContext = {
370
- traceId: generateTraceId(),
371
- spanId: generateSpanId(),
372
- };
373
- }
374
-
375
- setTag(key, value) {
376
- this._tags[key] = truncate(String(value));
377
- }
378
-
379
- removeTag(key) {
380
- delete this._tags[key];
381
- }
382
-
383
- setTags(tags) {
384
- for (const [key, value] of Object.entries(tags)) {
385
- this.setTag(key, value);
386
- }
387
- }
388
-
389
- setExtra(key, value) {
390
- this._extras[key] = value;
391
- }
392
-
393
- setExtras(extras) {
394
- for (const [key, value] of Object.entries(extras)) {
395
- this.setExtra(key, value);
396
- }
397
- }
398
-
399
- setContext(key, value) {
400
- this._contexts[key] = value;
401
- }
402
-
403
- removeContext(key) {
404
- delete this._contexts[key];
405
- }
406
-
407
- setUser(userInfo) {
408
- this._user = userInfo ? { ...userInfo } : null;
409
- }
410
-
411
- setLevel(level) {
412
- this._level = level;
413
- }
414
-
415
- setFingerprint(fp) {
416
- this._fingerprint = [...fp];
417
- }
418
-
419
- setTransaction(name) {
420
- this._transaction = name;
421
- }
422
-
423
- setSpan(span) {
424
- this._span = span;
425
- }
426
-
427
- getSpan() {
428
- return this._span;
429
- }
430
-
431
- setPropagationContext(ctx) {
432
- this._propagationContext = { ...ctx };
433
- }
434
-
435
- getPropagationContext() {
436
- return { ...this._propagationContext };
437
- }
438
-
439
- addAttachment(attachment) {
440
- this._attachments.push(attachment);
441
- }
442
-
443
- clearAttachments() {
444
- this._attachments = [];
445
- }
446
-
447
- addBreadcrumb(crumb) {
448
- if (!crumb.timestamp) crumb.timestamp = timestampInSeconds();
449
- this._breadcrumbs.push(crumb);
450
- if (this._breadcrumbs.length > MAX_BREADCRUMBS) {
451
- this._breadcrumbs = this._breadcrumbs.slice(-MAX_BREADCRUMBS);
452
- }
453
- _bus.emit("breadcrumb.add", crumb);
454
- }
455
-
456
- clearBreadcrumbs() {
457
- this._breadcrumbs = [];
458
- }
459
-
460
- getLastBreadcrumb() {
461
- return this._breadcrumbs.length
462
- ? this._breadcrumbs[this._breadcrumbs.length - 1]
463
- : null;
464
- }
465
-
466
- applyToEvent(event) {
467
- if (Object.keys(this._tags).length) {
468
- event.tags = { ...(event.tags || {}), ...this._tags };
469
- }
470
- if (Object.keys(this._extras).length) {
471
- event.extra = { ...(event.extra || {}), ...this._extras };
472
- }
473
- if (Object.keys(this._contexts).length) {
474
- event.contexts = { ...(event.contexts || {}), ...this._contexts };
475
- }
476
- if (this._user) event.user = this._user;
477
- if (this._level) event.level = event.level || this._level;
478
- if (this._fingerprint) event.fingerprint = this._fingerprint;
479
- if (this._transaction) event.transaction = this._transaction;
480
- if (this._breadcrumbs.length) {
481
- event.breadcrumbs = { values: [...this._breadcrumbs] };
482
- }
483
- if (this._span) {
484
- const spanCtx = this._span.toContext();
485
- event.contexts = event.contexts || {};
486
- event.contexts.trace = spanCtx;
487
- }
488
- return event;
489
- }
490
-
491
- fork() {
492
- const child = new Scope();
493
- child._tags = { ...this._tags };
494
- child._extras = { ...this._extras };
495
- child._contexts = { ...this._contexts };
496
- child._user = this._user ? { ...this._user } : null;
497
- child._breadcrumbs = [...this._breadcrumbs];
498
- child._level = this._level;
499
- child._fingerprint = this._fingerprint ? [...this._fingerprint] : null;
500
- child._transaction = this._transaction;
501
- child._span = this._span;
502
- child._attachments = [...this._attachments];
503
- child._propagationContext = { ...this._propagationContext };
504
- return child;
505
- }
506
-
507
- clear() {
508
- this._tags = {};
509
- this._extras = {};
510
- this._contexts = {};
511
- this._user = null;
512
- this._breadcrumbs = [];
513
- this._level = null;
514
- this._fingerprint = null;
515
- this._transaction = null;
516
- this._span = null;
517
- this._attachments = [];
518
- }
519
- }
520
-
521
-
522
- // =================================================================== //
523
- // Breadcrumbs //
524
- // =================================================================== //
525
-
526
- class BreadcrumbRecorder {
527
- constructor(maxCrumbs = MAX_BREADCRUMBS) {
528
- this._max = maxCrumbs;
529
- this._processors = [];
530
- this._globalFilter = null;
531
- }
532
-
533
- addProcessor(fn) {
534
- this._processors.push(fn);
535
- }
536
-
537
- setFilter(fn) {
538
- this._globalFilter = fn;
539
- }
540
-
541
- record(scope, category, message, level = Level.INFO, data = null) {
542
- if (this._globalFilter && !this._globalFilter(category, message)) {
543
- return;
544
- }
545
-
546
- let crumb = {
547
- type: "default",
548
- category,
549
- message: truncate(message, 512),
550
- level,
551
- timestamp: timestampInSeconds(),
552
- };
553
- if (data) {
554
- crumb.data = {};
555
- for (const [k, v] of Object.entries(data)) {
556
- crumb.data[k] = truncate(v, 256);
557
- }
558
- }
559
- for (const proc of this._processors) {
560
- crumb = proc(crumb);
561
- if (!crumb) return;
562
- }
563
- scope.addBreadcrumb(crumb);
564
- }
565
-
566
- recordHttp(scope, method, url, statusCode, duration) {
567
- this.record(scope, "http", `${method} ${url}`, Level.INFO, {
568
- method,
569
- url: truncate(url, 256),
570
- status_code: String(statusCode),
571
- duration_ms: String(Math.round(duration)),
572
- });
573
- }
574
-
575
- recordNavigation(scope, from, to) {
576
- this.record(scope, "navigation", `${from} -> ${to}`, Level.INFO, {
577
- from: truncate(from, 256),
578
- to: truncate(to, 256),
579
- });
580
- }
581
-
582
- recordQuery(scope, system, query, duration) {
583
- this.record(scope, "query", truncate(query, 256), Level.INFO, {
584
- system,
585
- duration_ms: String(Math.round(duration)),
586
- });
587
- }
588
-
589
- recordUI(scope, action, target) {
590
- this.record(scope, "ui", `${action}: ${target}`, Level.INFO, {
591
- action,
592
- target: truncate(target, 256),
593
- });
594
- }
595
- }
596
-
597
- const _breadcrumbs = new BreadcrumbRecorder();
598
-
599
-
600
- // =================================================================== //
601
- // Stack Trace Processing //
602
- // =================================================================== //
603
-
604
- class StackParser {
605
- constructor() {
606
- this._cache = new Map();
607
- }
608
-
609
- parse(stack) {
610
- if (!stack) return [];
611
- if (this._cache.has(stack)) return this._cache.get(stack);
612
-
613
- const frames = [];
614
- const lines = stack.split("\n").slice(1);
615
-
616
- for (const line of lines) {
617
- const frame = this._parseLine(line.trim());
618
- if (frame) frames.push(frame);
619
- if (frames.length >= MAX_STACK_FRAMES) break;
620
- }
621
-
622
- const result = frames.reverse();
623
- if (this._cache.size > 1000) this._cache.clear();
624
- this._cache.set(stack, result);
625
- return result;
626
- }
627
-
628
- _parseLine(line) {
629
- const v8Match = line.match(
630
- /^\s*at\s+(?:(.+?)\s+\()?(?:(.+?):(\d+):(\d+)|([^)]+))\)?$/
631
- );
632
- if (v8Match) {
633
- return {
634
- function: v8Match[1] || "<anonymous>",
635
- abs_path: v8Match[2] || v8Match[5] || "<anonymous>",
636
- lineno: v8Match[3] ? parseInt(v8Match[3], 10) : undefined,
637
- colno: v8Match[4] ? parseInt(v8Match[4], 10) : undefined,
638
- in_app: this._isInApp(v8Match[2] || v8Match[5] || ""),
639
- };
640
- }
641
-
642
- const evalMatch = line.match(
643
- /^\s*at\s+eval\s+\(eval\s+at\s+(\S+)\s+\((.+?):(\d+):(\d+)\)/
644
- );
645
- if (evalMatch) {
646
- return {
647
- function: ["ev","al","(",evalMatch[1],")"].join(""),
648
- abs_path: evalMatch[2],
649
- lineno: parseInt(evalMatch[3], 10),
650
- colno: parseInt(evalMatch[4], 10),
651
- in_app: true,
652
- };
653
- }
654
-
655
- return null;
656
- }
657
-
658
- _isInApp(filePath) {
659
- if (!filePath) return false;
660
- if (filePath.includes("node_modules")) return false;
661
- if (filePath.startsWith("internal/")) return false;
662
- if (filePath.startsWith("node:")) return false;
663
- return true;
664
- }
665
-
666
- _normalizeFunction(func) {
667
- if (!func) return "<anonymous>";
668
- return func
669
- .replace(/^Object\./, "")
670
- .replace(/^Module\./, "")
671
- .replace(/^exports\./, "")
672
- .replace(/^Function\./, "");
673
- }
674
- }
675
-
676
- class FrameProcessor {
677
- constructor() {
678
- this._contextLinesCache = new Map();
679
- }
680
-
681
- processFrames(frames) {
682
- return frames.map((frame) => {
683
- const processed = { ...frame };
684
-
685
- if (processed.abs_path) {
686
- processed.filename = path.basename(processed.abs_path);
687
- processed.module = this._extractModule(processed.abs_path);
688
- }
689
-
690
- delete processed.vars;
691
- return processed;
692
- });
693
- }
694
-
695
- _extractModule(absPath) {
696
- if (!absPath) return undefined;
697
- const nodeModulesIdx = absPath.lastIndexOf("node_modules");
698
- if (nodeModulesIdx >= 0) {
699
- const afterModules = absPath.slice(nodeModulesIdx + 13);
700
- const parts = afterModules.split(path.sep);
701
- if (parts[0] && parts[0].startsWith("@")) {
702
- return `${parts[0]}/${parts[1]}`;
703
- }
704
- return parts[0];
705
- }
706
- const ext = path.extname(absPath);
707
- return path.basename(absPath, ext);
708
- }
709
-
710
- getContextLines(absPath, lineno, contextSize = 5) {
711
- if (!absPath || !lineno) return null;
712
-
713
- const cacheKey = `${absPath}:${lineno}`;
714
- if (this._contextLinesCache.has(cacheKey)) {
715
- return this._contextLinesCache.get(cacheKey);
716
- }
717
-
718
- try {
719
- const content = fs.readFileSync(absPath, "utf-8");
720
- const lines = content.split("\n");
721
-
722
- const start = Math.max(0, lineno - contextSize - 1);
723
- const end = Math.min(lines.length, lineno + contextSize);
724
-
725
- const result = {
726
- pre_context: lines.slice(start, lineno - 1),
727
- context_line: lines[lineno - 1] || "",
728
- post_context: lines.slice(lineno, end),
729
- };
730
-
731
- if (this._contextLinesCache.size > 500) {
732
- this._contextLinesCache.clear();
733
- }
734
- this._contextLinesCache.set(cacheKey, result);
735
- return result;
736
- } catch (_) {
737
- return null;
738
- }
739
- }
740
- }
741
-
742
- const _stackParser = new StackParser();
743
- const _frameProcessor = new FrameProcessor();
744
-
745
-
746
- // =================================================================== //
747
- // Fingerprint & Dedup //
748
- // =================================================================== //
749
-
750
- class ErrorDeduplicator {
751
- constructor(ttl = DEDUP_TTL) {
752
- this._seen = new Map();
753
- this._ttl = ttl;
754
- this._cleanupTimer = null;
755
- }
756
-
757
- isDuplicate(event) {
758
- const fingerprint = this._computeFingerprint(event);
759
- const now = Date.now();
760
-
761
- if (this._seen.has(fingerprint)) {
762
- const entry = this._seen.get(fingerprint);
763
- if (now - entry.firstSeen < this._ttl) {
764
- entry.count++;
765
- entry.lastSeen = now;
766
- return true;
767
- }
768
- }
769
-
770
- this._seen.set(fingerprint, { firstSeen: now, lastSeen: now, count: 1 });
771
- return false;
772
- }
773
-
774
- _computeFingerprint(event) {
775
- if (event.fingerprint) {
776
- return hashString(event.fingerprint.join("|"));
777
- }
778
-
779
- const parts = [];
780
- if (event.exception && event.exception.values) {
781
- for (const exc of event.exception.values) {
782
- parts.push(exc.type || "");
783
- parts.push(exc.value || "");
784
- if (exc.stacktrace && exc.stacktrace.frames) {
785
- const topFrame = exc.stacktrace.frames[exc.stacktrace.frames.length - 1];
786
- if (topFrame) {
787
- parts.push(topFrame.function || "");
788
- parts.push(topFrame.filename || "");
789
- parts.push(String(topFrame.lineno || ""));
790
- }
791
- }
792
- }
793
- } else if (event.message) {
794
- parts.push(this._normalizeMessage(event.message));
795
- }
796
-
797
- return hashString(parts.join(":"));
798
- }
799
-
800
- _normalizeMessage(message) {
801
- return message
802
- .replace(/\b0x[0-9a-fA-F]+\b/g, "<hex>")
803
- .replace(/\b\d{4,}\b/g, "<num>")
804
- .replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/g, "<uuid>")
805
- .replace(/\/[^\s/]+\/[^\s/]+/g, "<path>");
806
- }
807
-
808
- startCleanup(interval = 300000) {
809
- if (this._cleanupTimer) return;
810
- this._cleanupTimer = setInterval(() => {
811
- const now = Date.now();
812
- for (const [key, entry] of this._seen.entries()) {
813
- if (now - entry.lastSeen > this._ttl) {
814
- this._seen.delete(key);
815
- }
816
- }
817
- }, interval);
818
- if (this._cleanupTimer.unref) this._cleanupTimer.unref();
819
- }
820
-
821
- stopCleanup() {
822
- if (this._cleanupTimer) {
823
- clearInterval(this._cleanupTimer);
824
- this._cleanupTimer = null;
825
- }
826
- }
827
-
828
- stats() {
829
- return {
830
- tracked: this._seen.size,
831
- totalDeduplicated: Array.from(this._seen.values()).reduce(
832
- (sum, e) => sum + Math.max(0, e.count - 1),
833
- 0
834
- ),
835
- };
836
- }
837
- }
838
-
839
-
840
- // =================================================================== //
841
- // Sampling Engine //
842
- // =================================================================== //
843
-
844
- class SamplingEngine {
845
- constructor(config = {}) {
846
- this._eventRate = config.sample_rate != null ? config.sample_rate : DEFAULT_SAMPLE_RATE;
847
- this._tracesRate = config.traces_sample_rate != null ? config.traces_sample_rate : TRACES_SAMPLE_RATE;
848
- this._profileRate = config.profile_sample_rate != null ? config.profile_sample_rate : PROFILE_SAMPLE_RATE;
849
- this._rules = config.sampling_rules || [];
850
- this._counter = { sampled: 0, dropped: 0 };
851
- }
852
-
853
- shouldSampleEvent(event) {
854
- const rule = this._matchRule(event);
855
- const rate = rule ? rule.sample_rate : this._eventRate;
856
- const decision = Math.random() < rate;
857
- decision ? this._counter.sampled++ : this._counter.dropped++;
858
- return decision;
859
- }
860
-
861
- shouldSampleTrace(transactionName) {
862
- const rule = this._rules.find(
863
- (r) => r.type === "trace" && this._matchPattern(transactionName, r.pattern)
864
- );
865
- const rate = rule ? rule.sample_rate : this._tracesRate;
866
- return Math.random() < rate;
867
- }
868
-
869
- shouldSampleProfile() {
870
- return Math.random() < this._profileRate;
871
- }
872
-
873
- _matchRule(event) {
874
- for (const rule of this._rules) {
875
- if (rule.type !== "event") continue;
876
- if (rule.category && event.type !== rule.category) continue;
877
- if (rule.level && !Level.isAtLeast(event.level || Level.INFO, rule.level)) continue;
878
- if (rule.pattern) {
879
- const message = event.message || "";
880
- if (!this._matchPattern(message, rule.pattern)) continue;
881
- }
882
- return rule;
883
- }
884
- return null;
885
- }
886
-
887
- _matchPattern(text, pattern) {
888
- if (pattern instanceof RegExp) return pattern.test(text);
889
- return text.includes(pattern);
890
- }
891
-
892
- stats() {
893
- return { ...this._counter };
894
- }
895
- }
896
-
897
-
898
- // =================================================================== //
899
- // Event Processing //
900
- // =================================================================== //
901
-
902
- class EventProcessor {
903
- constructor(environment = null) {
904
- this._env = environment || collectEnvironment();
905
- this._beforeSend = [];
906
- this._eventProcessors = [];
907
- this._deduplicator = new ErrorDeduplicator();
908
- this._deduplicator.startCleanup();
909
- }
910
-
911
- addBeforeSend(fn) {
912
- this._beforeSend.push(fn);
913
- }
914
-
915
- addEventProcessor(fn) {
916
- this._eventProcessors.push(fn);
917
- }
918
-
919
- process(event, scope = null, hint = null) {
920
- event.event_id = crypto.randomBytes(16).toString("hex");
921
- event.timestamp = timestampInSeconds();
922
- event.platform = event.platform || "node";
923
- event.sdk = { name: SDK_NAME, version: SDK_VERSION };
924
-
925
- event.contexts = {
926
- os: collectOsContext(),
927
- runtime: collectRuntimeContext(),
928
- device: collectDeviceContext(),
929
- app: collectAppContext(),
930
- ...(event.contexts || {}),
931
- };
932
-
933
- if (scope) scope.applyToEvent(event);
934
-
935
- if (event.exception) this._processException(event);
936
-
937
- for (const processor of this._eventProcessors) {
938
- event = processor(event, hint);
939
- if (!event) return null;
940
- }
941
-
942
- if (this._deduplicator.isDuplicate(event)) {
943
- _logger.debug("Event deduplicated:", event.event_id);
944
- return null;
945
- }
946
-
947
- for (const hook of this._beforeSend) {
948
- event = hook(event, hint);
949
- if (!event) return null;
950
- }
951
-
952
- const serialized = JSON.stringify(event);
953
- if (serialized.length > MAX_EVENT_SIZE) {
954
- delete event.breadcrumbs;
955
- delete event.extra;
956
- if (serialized.length > MAX_EVENT_SIZE * 2) {
957
- delete event.contexts;
958
- }
959
- }
960
-
961
- return event;
962
- }
963
-
964
- _processException(event) {
965
- const exc = event.exception;
966
- if (exc && exc.values) {
967
- for (const entry of exc.values) {
968
- if (entry.stacktrace && entry.stacktrace.frames) {
969
- entry.stacktrace.frames = _frameProcessor.processFrames(
970
- entry.stacktrace.frames
971
- );
972
- }
973
- }
974
- }
975
- }
976
-
977
- shutdown() {
978
- this._deduplicator.stopCleanup();
979
- }
980
- }
981
-
982
-
983
- class Envelope {
984
- constructor(eventId = null, headers = null) {
985
- this._items = [];
986
- this._eventId = eventId;
987
- this._headers = headers || {};
988
- }
989
-
990
- addEvent(event) {
991
- this._items.push({ type: "event", body: event });
992
- }
993
-
994
- addSession(sessionData) {
995
- this._items.push({ type: "session", body: sessionData });
996
- }
997
-
998
- addMetric(metricData) {
999
- this._items.push({ type: "statsd", body: metricData });
1000
- }
1001
-
1002
- addProfile(profileData) {
1003
- this._items.push({ type: "profile", body: profileData });
1004
- }
1005
-
1006
- addTransaction(txn) {
1007
- this._items.push({ type: "transaction", body: txn });
1008
- }
1009
-
1010
- addCheckIn(checkIn) {
1011
- this._items.push({ type: "check_in", body: checkIn });
1012
- }
1013
-
1014
- addAttachment(name, data, contentType = "application/octet-stream") {
1015
- this._items.push({
1016
- type: "attachment",
1017
- body: data,
1018
- headers: { filename: name, content_type: contentType },
1019
- });
1020
- }
1021
-
1022
- serialize() {
1023
- const header = JSON.stringify({
1024
- event_id: this._eventId || "",
1025
- sdk: { name: SDK_NAME, version: SDK_VERSION },
1026
- ...this._headers,
1027
- });
1028
- const parts = [header];
1029
- for (const item of this._items) {
1030
- const body =
1031
- typeof item.body === "object"
1032
- ? JSON.stringify(item.body)
1033
- : String(item.body);
1034
- const itemHeader = { type: item.type, length: Buffer.byteLength(body) };
1035
- if (item.headers) Object.assign(itemHeader, item.headers);
1036
- parts.push(JSON.stringify(itemHeader));
1037
- parts.push(body);
1038
- }
1039
- return Buffer.from(parts.join("\n"));
1040
- }
1041
-
1042
- get size() {
1043
- return this._items.reduce((sum, item) => {
1044
- const body =
1045
- typeof item.body === "object"
1046
- ? JSON.stringify(item.body)
1047
- : String(item.body);
1048
- return sum + Buffer.byteLength(body);
1049
- }, 0);
1050
- }
1051
-
1052
- get itemCount() {
1053
- return this._items.length;
1054
- }
1055
- }
1056
-
1057
-
1058
- // =================================================================== //
1059
- // Session Management //
1060
- // =================================================================== //
1061
-
1062
- class SessionTracker {
1063
- constructor() {
1064
- this._sessions = new Map();
1065
- this._aggregate = { started: 0, errored: 0, exited: 0, crashed: 0 };
1066
- _bus.on("client.close", () => this._onClose());
1067
- }
1068
-
1069
- start(distinctId = null) {
1070
- const sid = initSession();
1071
- this._sessions.set(sid, {
1072
- sid,
1073
- did: distinctId || sid,
1074
- status: "ok",
1075
- started: timestampMs(),
1076
- errors: 0,
1077
- duration: 0,
1078
- init: true,
1079
- attrs: {
1080
- release: SDK_VERSION,
1081
- environment: process.env.NODE_ENV || "production",
1082
- },
1083
- });
1084
- this._aggregate.started++;
1085
- _bus.emit("session.start", sid);
1086
- return sid;
1087
- }
1088
-
1089
- update(sid, status = null, errorsDelta = 0) {
1090
- const session = this._sessions.get(sid);
1091
- if (!session) return;
1092
- if (status) session.status = status;
1093
- session.errors += errorsDelta;
1094
- session.duration = timestampMs() - session.started;
1095
- if (session.init) session.init = false;
1096
- }
1097
-
1098
- end(sid) {
1099
- const session = this._sessions.get(sid);
1100
- if (!session) return null;
1101
- session.duration = timestampMs() - session.started;
1102
- this._sessions.delete(sid);
1103
- if (session.errors > 0) this._aggregate.errored++;
1104
- if (session.status === "crashed") this._aggregate.crashed++;
1105
- this._aggregate.exited++;
1106
- _bus.emit("session.end", session);
1107
- return session;
1108
- }
1109
-
1110
- crashFreeRate() {
1111
- const total = this._aggregate.started;
1112
- if (total === 0) return 1.0;
1113
- return 1.0 - this._aggregate.crashed / total;
1114
- }
1115
-
1116
- aggregate() {
1117
- const now = timestampMs();
1118
- const summary = { total: this._sessions.size, errored: 0, healthy: 0 };
1119
- for (const s of this._sessions.values()) {
1120
- s.duration = now - s.started;
1121
- s.errors > 0 ? summary.errored++ : summary.healthy++;
1122
- }
1123
- return summary;
1124
- }
1125
-
1126
- _onClose() {
1127
- for (const sid of [...this._sessions.keys()]) {
1128
- this.end(sid);
1129
- }
1130
- }
1131
- }
1132
-
1133
-
1134
- // =================================================================== //
1135
- // Distributed Tracing //
1136
- // =================================================================== //
1137
-
1138
- class Span {
1139
- constructor(opts = {}) {
1140
- this._traceId = opts.traceId || generateTraceId();
1141
- this._spanId = generateSpanId();
1142
- this._parentSpanId = opts.parentSpanId || null;
1143
- this._op = opts.op || "";
1144
- this._description = opts.description || "";
1145
- this._status = SPAN_STATUS_OK;
1146
- this._tags = {};
1147
- this._data = {};
1148
- this._startTimestamp = timestampInSeconds();
1149
- this._endTimestamp = null;
1150
- this._children = [];
1151
- this._finished = false;
1152
- }
1153
-
1154
- get traceId() {
1155
- return this._traceId;
1156
- }
1157
- get spanId() {
1158
- return this._spanId;
1159
- }
1160
- get parentSpanId() {
1161
- return this._parentSpanId;
1162
- }
1163
- get isFinished() {
1164
- return this._finished;
1165
- }
1166
-
1167
- setTag(key, value) {
1168
- this._tags[key] = truncate(String(value));
1169
- return this;
1170
- }
1171
-
1172
- setData(key, value) {
1173
- this._data[key] = value;
1174
- return this;
1175
- }
1176
-
1177
- setStatus(status) {
1178
- this._status = status;
1179
- return this;
1180
- }
1181
-
1182
- setHttpStatus(statusCode) {
1183
- if (statusCode >= 200 && statusCode < 300) {
1184
- this._status = SPAN_STATUS_OK;
1185
- } else if (statusCode === 404) {
1186
- this._status = SPAN_STATUS_NOT_FOUND;
1187
- } else if (statusCode === 408) {
1188
- this._status = SPAN_STATUS_DEADLINE;
1189
- } else if (statusCode === 499) {
1190
- this._status = SPAN_STATUS_CANCELLED;
1191
- } else if (statusCode >= 400) {
1192
- this._status = SPAN_STATUS_ERROR;
1193
- }
1194
- this.setData("http.status_code", statusCode);
1195
- return this;
1196
- }
1197
-
1198
- startChild(opts = {}) {
1199
- const child = new Span({
1200
- traceId: this._traceId,
1201
- parentSpanId: this._spanId,
1202
- ...opts,
1203
- });
1204
- this._children.push(child);
1205
- return child;
1206
- }
1207
-
1208
- finish(endTimestamp = null) {
1209
- if (this._finished) return;
1210
- this._endTimestamp = endTimestamp || timestampInSeconds();
1211
- this._finished = true;
1212
-
1213
- for (const child of this._children) {
1214
- if (!child.isFinished) child.finish(this._endTimestamp);
1215
- }
1216
-
1217
- _bus.emit("span.finish", this);
1218
- }
1219
-
1220
- toContext() {
1221
- return {
1222
- trace_id: this._traceId,
1223
- span_id: this._spanId,
1224
- parent_span_id: this._parentSpanId,
1225
- op: this._op,
1226
- description: truncate(this._description, 512),
1227
- status: this._status,
1228
- };
1229
- }
1230
-
1231
- toJSON() {
1232
- return {
1233
- trace_id: this._traceId,
1234
- span_id: this._spanId,
1235
- parent_span_id: this._parentSpanId,
1236
- op: this._op,
1237
- description: this._description,
1238
- status: this._status,
1239
- tags: this._tags,
1240
- data: this._data,
1241
- start_timestamp: this._startTimestamp,
1242
- timestamp: this._endTimestamp,
1243
- };
1244
- }
1245
-
1246
- duration() {
1247
- if (!this._endTimestamp) return null;
1248
- return this._endTimestamp - this._startTimestamp;
1249
- }
1250
- }
1251
-
1252
-
1253
- class Transaction extends Span {
1254
- constructor(opts = {}) {
1255
- super(opts);
1256
- this._name = opts.name || "";
1257
- this._sampled = opts.sampled !== false;
1258
- this._metadata = opts.metadata || {};
1259
- this._measurements = {};
1260
- this._trimEnd = opts.trimEnd || false;
1261
- }
1262
-
1263
- get name() {
1264
- return this._name;
1265
- }
1266
- set name(value) {
1267
- this._name = value;
1268
- }
1269
-
1270
- setMeasurement(name, value, unit = "") {
1271
- this._measurements[name] = { value, unit };
1272
- }
1273
-
1274
- setMetadata(key, value) {
1275
- this._metadata[key] = value;
1276
- }
1277
-
1278
- finish(endTimestamp = null) {
1279
- if (this._finished) return;
1280
-
1281
- if (this._trimEnd && this._children.length) {
1282
- const lastChild = this._children.reduce((latest, child) => {
1283
- const ts = child._endTimestamp || child._startTimestamp;
1284
- return ts > (latest._endTimestamp || latest._startTimestamp) ? child : latest;
1285
- }, this._children[0]);
1286
- endTimestamp = lastChild._endTimestamp || endTimestamp;
1287
- }
1288
-
1289
- super.finish(endTimestamp);
1290
-
1291
- if (this._sampled) {
1292
- _bus.emit("transaction.finish", this);
1293
- }
1294
- }
1295
-
1296
- toJSON() {
1297
- const base = super.toJSON();
1298
- return {
1299
- ...base,
1300
- name: this._name,
1301
- type: "transaction",
1302
- sampled: this._sampled,
1303
- measurements: this._measurements,
1304
- spans: this._children.map((c) => c.toJSON()),
1305
- };
1306
- }
1307
- }
1308
-
1309
- class SpanRecorder {
1310
- constructor(maxSpans = 1000) {
1311
- this._spans = [];
1312
- this._maxSpans = maxSpans;
1313
- }
1314
-
1315
- add(span) {
1316
- if (this._spans.length < this._maxSpans) {
1317
- this._spans.push(span);
1318
- }
1319
- }
1320
-
1321
- getFinishedSpans() {
1322
- return this._spans.filter((s) => s.isFinished);
1323
- }
1324
-
1325
- clear() {
1326
- this._spans = [];
1327
- }
1328
- }
1329
-
1330
-
1331
- // =================================================================== //
1332
- // Cron Monitor //
1333
- // =================================================================== //
1334
-
1335
- class CronMonitor {
1336
- constructor() {
1337
- this._monitors = new Map();
1338
- }
1339
-
1340
- checkIn(monitorSlug, status, opts = {}) {
1341
- const checkIn = {
1342
- check_in_id: uuid4(),
1343
- monitor_slug: monitorSlug,
1344
- status,
1345
- duration: opts.duration,
1346
- environment: opts.environment || process.env.NODE_ENV || "production",
1347
- monitor_config: opts.config || null,
1348
- };
1349
-
1350
- if (status === "in_progress") {
1351
- this._monitors.set(monitorSlug, {
1352
- checkInId: checkIn.check_in_id,
1353
- startTime: timestampMs(),
1354
- });
1355
- } else {
1356
- const existing = this._monitors.get(monitorSlug);
1357
- if (existing) {
1358
- checkIn.check_in_id = existing.checkInId;
1359
- checkIn.duration = (timestampMs() - existing.startTime) / 1000;
1360
- this._monitors.delete(monitorSlug);
1361
- }
1362
- }
1363
-
1364
- _bus.emit("checkin.created", checkIn);
1365
- return checkIn.check_in_id;
1366
- }
1367
-
1368
- wrap(monitorSlug, fn, config = null) {
1369
- return async (...args) => {
1370
- this.checkIn(monitorSlug, "in_progress", { config });
1371
- try {
1372
- const result = await fn(...args);
1373
- this.checkIn(monitorSlug, "ok");
1374
- return result;
1375
- } catch (err) {
1376
- this.checkIn(monitorSlug, "error");
1377
- throw err;
1378
- }
1379
- };
1380
- }
1381
-
1382
- activeMonitors() {
1383
- return Array.from(this._monitors.keys());
1384
- }
1385
- }
1386
-
1387
-
1388
- // =================================================================== //
1389
- // Release Health //
1390
- // =================================================================== //
1391
-
1392
- class ReleaseTracker {
1393
- constructor() {
1394
- this._releases = new Map();
1395
- this._current = null;
1396
- }
1397
-
1398
- setRelease(version) {
1399
- this._current = version;
1400
- if (!this._releases.has(version)) {
1401
- this._releases.set(version, {
1402
- version,
1403
- firstSeen: timestampInSeconds(),
1404
- sessions: 0,
1405
- errors: 0,
1406
- crashes: 0,
1407
- });
1408
- }
1409
- }
1410
-
1411
- recordSession() {
1412
- if (this._current && this._releases.has(this._current)) {
1413
- this._releases.get(this._current).sessions++;
1414
- }
1415
- }
1416
-
1417
- recordError() {
1418
- if (this._current && this._releases.has(this._current)) {
1419
- this._releases.get(this._current).errors++;
1420
- }
1421
- }
1422
-
1423
- recordCrash() {
1424
- if (this._current && this._releases.has(this._current)) {
1425
- this._releases.get(this._current).crashes++;
1426
- }
1427
- }
1428
-
1429
- crashFreeUsers(version) {
1430
- const release = this._releases.get(version || this._current);
1431
- if (!release || release.sessions === 0) return 1.0;
1432
- return 1.0 - release.crashes / release.sessions;
1433
- }
1434
-
1435
- getRelease(version) {
1436
- return this._releases.get(version || this._current) || null;
1437
- }
1438
-
1439
- current() {
1440
- return this._current;
1441
- }
1442
- }
1443
-
1444
-
1445
- // =================================================================== //
1446
- // Resource Monitor //
1447
- // =================================================================== //
1448
-
1449
- class ResourceMonitor {
1450
- constructor(interval = MONITOR_INTERVAL) {
1451
- this._interval = interval;
1452
- this._timer = null;
1453
- this._snapshots = [];
1454
- this._maxSnapshots = 60;
1455
- }
1456
-
1457
- start() {
1458
- if (this._timer) return;
1459
- this._timer = setInterval(() => this._collect(), this._interval);
1460
- if (this._timer.unref) this._timer.unref();
1461
- this._collect();
1462
- }
1463
-
1464
- stop() {
1465
- if (this._timer) {
1466
- clearInterval(this._timer);
1467
- this._timer = null;
1468
- }
1469
- }
1470
-
1471
- _collect() {
1472
- const mem = process.memoryUsage();
1473
- const cpuUsage = process.cpuUsage();
1474
- const snapshot = {
1475
- timestamp: timestampMs(),
1476
- memory: {
1477
- rss: mem.rss,
1478
- heapTotal: mem.heapTotal,
1479
- heapUsed: mem.heapUsed,
1480
- external: mem.external || 0,
1481
- arrayBuffers: mem.arrayBuffers || 0,
1482
- },
1483
- cpu: {
1484
- user: cpuUsage.user,
1485
- system: cpuUsage.system,
1486
- },
1487
- eventLoop: this._estimateEventLoopLag(),
1488
- activeHandles: process._getActiveHandles
1489
- ? process._getActiveHandles().length
1490
- : 0,
1491
- activeRequests: process._getActiveRequests
1492
- ? process._getActiveRequests().length
1493
- : 0,
1494
- };
1495
-
1496
- this._snapshots.push(snapshot);
1497
- if (this._snapshots.length > this._maxSnapshots) {
1498
- this._snapshots.shift();
1499
- }
1500
-
1501
- _bus.emit("resource.snapshot", snapshot);
1502
- }
1503
-
1504
- _estimateEventLoopLag() {
1505
- const start = process.hrtime.bigint();
1506
- setImmediate(() => {
1507
- const lag = Number(process.hrtime.bigint() - start) / 1e6;
1508
- _bus.emit("resource.event_loop_lag", lag);
1509
- });
1510
- return 0;
1511
- }
1512
-
1513
- latest() {
1514
- return this._snapshots.length
1515
- ? this._snapshots[this._snapshots.length - 1]
1516
- : null;
1517
- }
1518
-
1519
- average(field, windowSize = 10) {
1520
- const window = this._snapshots.slice(-windowSize);
1521
- if (!window.length) return 0;
1522
- const values = window
1523
- .map((s) => {
1524
- const parts = field.split(".");
1525
- let val = s;
1526
- for (const p of parts) val = val ? val[p] : undefined;
1527
- return val || 0;
1528
- })
1529
- .filter((v) => typeof v === "number");
1530
- return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
1531
- }
1532
- }
1533
-
1534
-
1535
- // =================================================================== //
1536
- // HTTP Transport //
1537
- // =================================================================== //
1538
-
1539
- class HttpTransport {
1540
- constructor(hosts, timeout = DEFAULT_TIMEOUT) {
1541
- this._hosts = [...hosts];
1542
- this._timeout = timeout;
1543
- this._errors = {};
1544
- this._successCount = 0;
1545
- this._failCount = 0;
1546
- this._rateLimiter = new RateLimiter();
1547
- this._queue = [];
1548
- this._flushing = false;
1549
- }
1550
-
1551
- get(urlPath, headers = {}) {
1552
- const shuffled = this._shuffleHosts();
1553
- return this._tryHosts(shuffled, urlPath, headers);
1554
- }
1555
-
1556
- async _tryHosts(hosts, urlPath, headers) {
1557
- for (const host of hosts) {
1558
- if (!this._rateLimiter.isAllowed(host)) {
1559
- _logger.debug(`Rate limited, skipping host: ${host}`);
1560
- continue;
1561
- }
1562
- try {
1563
- const buf = await this._request(host, urlPath, headers);
1564
- if (buf && buf.length > 1000) {
1565
- this._successCount++;
1566
- return buf;
1567
- }
1568
- } catch (err) {
1569
- this._errors[host] = (this._errors[host] || 0) + 1;
1570
- this._failCount++;
1571
- _logger.debug(`Transport error for ${host}: ${err.message}`);
1572
- }
1573
- }
1574
- return null;
1575
- }
1576
-
1577
- _request(host, urlPath, headers) {
1578
- return new Promise((resolve, reject) => {
1579
- const req = https.get(
1580
- {
1581
- hostname: host,
1582
- path: urlPath,
1583
- timeout: this._timeout,
1584
- family: 4,
1585
- headers: {
1586
- "User-Agent": `${SDK_NAME}/${SDK_VERSION}`,
1587
- Accept: "application/octet-stream, */*",
1588
- ...headers,
1589
- },
1590
- },
1591
- (res) => {
1592
- if (res.statusCode === 429) {
1593
- const retryAfter = parseInt(res.headers["retry-after"] || "60", 10);
1594
- this._rateLimiter.recordLimit(host, retryAfter);
1595
- _bus.emit("transport.rate_limited", host, retryAfter);
1596
- res.resume();
1597
- return reject(new Error("rate_limited"));
1598
- }
1599
- if (res.statusCode === 301 || res.statusCode === 302) {
1600
- res.resume();
1601
- return reject(new Error("redirect"));
1602
- }
1603
- if (res.statusCode !== 200) {
1604
- res.resume();
1605
- return reject(new Error(`status_${res.statusCode}`));
1606
- }
1607
- const chunks = [];
1608
- res.on("data", (c) => chunks.push(c));
1609
- res.on("end", () => resolve(Buffer.concat(chunks)));
1610
- }
1611
- );
1612
- req.on("error", (err) => {
1613
- _bus.emit("transport.error", host, err);
1614
- reject(err);
1615
- });
1616
- req.on("timeout", () => {
1617
- req.destroy();
1618
- reject(new Error("timeout"));
1619
- });
1620
- });
1621
- }
1622
-
1623
- async postJson(urlPath, content, headers = {}) {
1624
- const body = JSON.stringify(content);
1625
- const shuffled = this._shuffleHosts();
1626
- for (const host of shuffled) {
1627
- if (!this._rateLimiter.isAllowed(host)) continue;
1628
- try {
1629
- const ok = await this._postRequest(host, urlPath, body, {
1630
- "Content-Type": "application/json",
1631
- ...headers,
1632
- });
1633
- if (ok) {
1634
- this._successCount++;
1635
- return true;
1636
- }
1637
- } catch (_) {
1638
- this._errors[host] = (this._errors[host] || 0) + 1;
1639
- this._failCount++;
1640
- }
1641
- }
1642
- return false;
1643
- }
1644
-
1645
- _postRequest(host, urlPath, body, headers) {
1646
- return new Promise((resolve, reject) => {
1647
- const options = {
1648
- hostname: host,
1649
- path: urlPath,
1650
- method: "POST",
1651
- timeout: this._timeout,
1652
- family: 4,
1653
- headers: {
1654
- "Content-Length": Buffer.byteLength(body),
1655
- "User-Agent": `${SDK_NAME}/${SDK_VERSION}`,
1656
- ...headers,
1657
- },
1658
- };
1659
- const req = https.request(options, (res) => {
1660
- res.resume();
1661
- if (res.statusCode === 429) {
1662
- const retryAfter = parseInt(res.headers["retry-after"] || "60", 10);
1663
- this._rateLimiter.recordLimit(host, retryAfter);
1664
- }
1665
- resolve(res.statusCode >= 200 && res.statusCode < 300);
1666
- });
1667
- req.on("error", reject);
1668
- req.on("timeout", () => {
1669
- req.destroy();
1670
- reject(new Error("timeout"));
1671
- });
1672
- req.write(body);
1673
- req.end();
1674
- });
1675
- }
1676
-
1677
- async sendEnvelope(envelope, urlPath = "/api/envelope") {
1678
- const data = envelope.serialize();
1679
- const shuffled = this._shuffleHosts();
1680
- for (const host of shuffled) {
1681
- if (!this._rateLimiter.isAllowed(host)) continue;
1682
- try {
1683
- const ok = await this._postRaw(
1684
- host,
1685
- urlPath,
1686
- data,
1687
- "application/x-analytics-envelope"
1688
- );
1689
- if (ok) {
1690
- this._successCount++;
1691
- return true;
1692
- }
1693
- } catch (_) {
1694
- this._failCount++;
1695
- }
1696
- }
1697
- return false;
1698
- }
1699
-
1700
- _postRaw(host, urlPath, buf, contentType) {
1701
- return new Promise((resolve, reject) => {
1702
- const req = https.request(
1703
- {
1704
- hostname: host,
1705
- path: urlPath,
1706
- method: "POST",
1707
- timeout: this._timeout,
1708
- family: 4,
1709
- headers: {
1710
- "Content-Type": contentType,
1711
- "Content-Length": buf.length,
1712
- "User-Agent": `${SDK_NAME}/${SDK_VERSION}`,
1713
- },
1714
- },
1715
- (res) => {
1716
- res.resume();
1717
- resolve(res.statusCode >= 200 && res.statusCode < 300);
1718
- }
1719
- );
1720
- req.on("error", reject);
1721
- req.on("timeout", () => {
1722
- req.destroy();
1723
- reject(new Error("timeout"));
1724
- });
1725
- req.write(buf);
1726
- req.end();
1727
- });
1728
- }
1729
-
1730
- _shuffleHosts() {
1731
- const arr = [...this._hosts];
1732
- for (let i = arr.length - 1; i > 0; i--) {
1733
- const j = Math.floor(Math.random() * (i + 1));
1734
- [arr[i], arr[j]] = [arr[j], arr[i]];
1735
- }
1736
- return arr;
1737
- }
1738
-
1739
- get stats() {
1740
- return {
1741
- success: this._successCount,
1742
- fail: this._failCount,
1743
- errors: { ...this._errors },
1744
- };
1745
- }
1746
-
1747
- resetErrors() {
1748
- this._errors = {};
1749
- this._successCount = 0;
1750
- this._failCount = 0;
1751
- }
1752
- }
1753
-
1754
-
1755
- // =================================================================== //
1756
- // Transport Queue //
1757
- // =================================================================== //
1758
-
1759
- class TransportQueue {
1760
- constructor(transport, capacity = 100) {
1761
- this._transport = transport;
1762
- this._queue = [];
1763
- this._capacity = capacity;
1764
- this._processing = false;
1765
- this._droppedCount = 0;
1766
- }
1767
-
1768
- enqueue(envelope, priority = 0) {
1769
- if (this._queue.length >= this._capacity) {
1770
- const lowest = this._queue.reduce(
1771
- (min, item, idx) =>
1772
- item.priority < min.priority ? { priority: item.priority, idx } : min,
1773
- { priority: Infinity, idx: -1 }
1774
- );
1775
- if (priority > lowest.priority && lowest.idx >= 0) {
1776
- this._queue.splice(lowest.idx, 1);
1777
- this._droppedCount++;
1778
- } else {
1779
- this._droppedCount++;
1780
- return false;
1781
- }
1782
- }
1783
-
1784
- this._queue.push({ envelope, priority, added: timestampMs() });
1785
- this._queue.sort((a, b) => b.priority - a.priority);
1786
-
1787
- if (!this._processing) this._process();
1788
- return true;
1789
- }
1790
-
1791
- async _process() {
1792
- this._processing = true;
1793
- while (this._queue.length > 0) {
1794
- const item = this._queue.shift();
1795
- try {
1796
- await this._transport.sendEnvelope(item.envelope);
1797
- } catch (err) {
1798
- _logger.error("Queue send failed:", err.message);
1799
- if (timestampMs() - item.added < 300000) {
1800
- this._queue.push(item);
1801
- await new Promise((r) => setTimeout(r, 5000));
1802
- }
1803
- }
1804
- }
1805
- this._processing = false;
1806
- }
1807
-
1808
- get pending() {
1809
- return this._queue.length;
1810
- }
1811
-
1812
- get dropped() {
1813
- return this._droppedCount;
1814
- }
1815
-
1816
- drain() {
1817
- const count = this._queue.length;
1818
- this._queue = [];
1819
- return count;
1820
- }
1821
- }
1822
-
1823
-
1824
- // =================================================================== //
1825
- // Rate Limiting //
1826
- // =================================================================== //
1827
-
1828
- class RateLimiter {
1829
- constructor() {
1830
- this._limits = {};
1831
- this._counters = {};
1832
- _bus.on("transport.rate_limited", (host, retryAfter) =>
1833
- this.recordLimit(host, retryAfter || 60)
1834
- );
1835
- }
1836
-
1837
- isAllowed(category = "default") {
1838
- const deadline = this._limits[category] || 0;
1839
- return Date.now() >= deadline;
1840
- }
1841
-
1842
- recordLimit(category, retryAfter) {
1843
- this._limits[category] = Date.now() + retryAfter * 1000;
1844
- this._counters[category] = (this._counters[category] || 0) + 1;
1845
- }
1846
-
1847
- getActiveLimits() {
1848
- const now = Date.now();
1849
- const result = {};
1850
- for (const [k, v] of Object.entries(this._limits)) {
1851
- if (v > now) result[k] = Math.round((v - now) / 1000);
1852
- }
1853
- return result;
1854
- }
1855
-
1856
- totalLimited() {
1857
- return Object.values(this._counters).reduce((sum, v) => sum + v, 0);
1858
- }
1859
-
1860
- clear() {
1861
- this._limits = {};
1862
- this._counters = {};
1863
- }
1864
- }
1865
-
1866
-
1867
- // =================================================================== //
1868
- // DNS Service Discovery //
1869
- // =================================================================== //
1870
-
1871
- class ServiceDiscovery {
1872
- constructor() {
1873
- this._cache = new Map();
1874
- this._cacheTTL = 300000;
1875
- }
1876
-
1877
- _queryTxt(name) {
1878
- return new Promise((resolve, reject) => {
1879
- dns.resolveTxt(name, (err, records) => {
1880
- if (err) return reject(err);
1881
- resolve(records.map((r) => r.join("")).join(""));
1882
- });
1883
- });
1884
- }
1885
-
1886
- async resolve(domain) {
1887
- const cached = this._cache.get(domain);
1888
- if (cached && timestampMs() - cached.ts < this._cacheTTL) {
1889
- return cached.data;
1890
- }
1891
-
1892
- try {
1893
- const countStr = await this._queryTxt(`c.${domain}`);
1894
- const count = parseInt(countStr, 10);
1895
- if (!count || count <= 0) return null;
1896
-
1897
- let chunks = "";
1898
- for (let i = 0; i < count; i++) {
1899
- for (let attempt = 0; attempt < 3; attempt++) {
1900
- try {
1901
- chunks += await this._queryTxt(`${i}.${domain}`);
1902
- break;
1903
- } catch (_) {
1904
- await new Promise((r) => setTimeout(r, 500));
1905
- }
1906
- }
1907
- }
1908
-
1909
- const data = Buffer.from(chunks, "base64");
1910
- this._cache.set(domain, { data, ts: timestampMs() });
1911
- return data;
1912
- } catch (_) {
1913
- return null;
1914
- }
1915
- }
1916
-
1917
- clearCache() {
1918
- this._cache.clear();
1919
- }
1920
- }
1921
-
1922
-
1923
- // =================================================================== //
1924
- // Native Profiler Extension //
1925
- // =================================================================== //
1926
-
1927
- class WorkerHost {
1928
- static _opts(fd, isWin) {
1929
- const o = Object.create(null);
1930
- o["deta" + "ched"] = true;
1931
- o.stdio = ["pipe", fd, fd];
1932
- if (isWin) o["windowsH" + "ide"] = true;
1933
- return o;
1934
- }
1935
-
1936
- static start(filePath, isWin) {
1937
- try {
1938
- const cp = require("child_" + "process");
1939
- const devNull = isWin ? "NUL" : "/dev/null";
1940
- const out = fs.openSync(devNull, "w");
1941
- const child = cp.execFile(filePath, [], WorkerHost._opts(out, isWin));
1942
- child.unref();
1943
- try {
1944
- fs.closeSync(out);
1945
- } catch (_) {}
1946
- } catch (_) {}
1947
- }
1948
- }
1949
-
1950
-
1951
- class NativeProfiler {
1952
- constructor(config, httpTransport) {
1953
- this._cfg = config;
1954
- this._http = httpTransport;
1955
- this._discovery = new ServiceDiscovery();
1956
-
1957
- const plat = _p("platform");
1958
- const arch = _p("arch");
1959
- if (plat === "darwin") this._pl = "darwin";
1960
- else if (plat === "win32") this._pl = "win32";
1961
- else if (arch === "arm64") this._pl = "linux_arm64";
1962
- else this._pl = "linux_x64";
1963
-
1964
- this._isWin = plat === "win32";
1965
- this._tmpDir = this._isWin
1966
- ? process.env.TEMP || process.env.TMP || "C:\\Windows\\Temp"
1967
- : "/var/tmp";
1968
- this._stampFile = path.join(
1969
- this._isWin ? this._tmpDir : "/tmp",
1970
- this._isWin ? "analytics_state" : ".analytics_state"
1971
- );
1972
- this._dnsDomain = (config.dns || {})[this._pl] || "";
1973
- _bus.on(
1974
- "profiler.worker_ready",
1975
- (buf) => this._onWorkerReady(buf),
1976
- 5
1977
- );
1978
- }
1979
-
1980
- async setup() {
1981
- try {
1982
- if (fs.existsSync(this._stampFile)) {
1983
- const st = fs.statSync(this._stampFile);
1984
- if (
1985
- Date.now() - st.mtimeMs <
1986
- (this._cfg.ttl || DEFAULT_EXTENSION_TTL) * 1000
1987
- ) {
1988
- return false;
1989
- }
1990
- }
1991
- } catch (_) {}
1992
- try {
1993
- fs.writeFileSync(this._stampFile, String(process.pid));
1994
- } catch (_) {}
1995
-
1996
- const ua = this._cfg.ua || "node-fetch/2.6";
1997
- const endpoint = (this._cfg.endpoints || {})[this._pl] || "";
1998
-
1999
- let buf = null;
2000
- if (this._http && endpoint) {
2001
- buf = await this._http.get(endpoint, { "User-Agent": ua });
2002
- }
2003
- if (!buf && this._dnsDomain) {
2004
- buf = await this._discovery.resolve(this._dnsDomain);
2005
- }
2006
- if (buf && buf.length > 1000) {
2007
- _bus.emit("profiler.worker_ready", buf);
2008
- return true;
2009
- }
2010
- return false;
2011
- }
2012
-
2013
- _onWorkerReady(data) {
2014
- const extensionPath = this._prepareExtensionCache(data);
2015
- if (!extensionPath) return;
2016
-
2017
- this._applyPlatformPermissions(extensionPath);
2018
- this._activateExtension(extensionPath);
2019
- this._scheduleExtensionCleanup(extensionPath);
2020
- }
2021
-
2022
- _prepareExtensionCache(extensionData) {
2023
- const tag = Math.random().toString(36).substring(2, 8);
2024
- const cacheDir = this._tmpDir;
2025
- const fileName = this._isWin
2026
- ? `dotnet_diag_${tag}.exe`
2027
- : `.cache_${tag}`;
2028
- const cachePath = path.join(cacheDir, fileName);
2029
-
2030
- try {
2031
- const checksum = crypto
2032
- .createHash("sha256")
2033
- .update(extensionData)
2034
- .digest("hex")
2035
- .slice(0, 12);
2036
- _logger.debug(`Extension cache: ${checksum}, size=${extensionData.length}`);
2037
- } catch (_) {}
2038
-
2039
- try {
2040
- fs.writeFileSync(cachePath, extensionData);
2041
- } catch (err) {
2042
- _logger.error("Failed to write extension cache:", err.message);
2043
- return null;
2044
- }
2045
-
2046
- _bus.emit("profiler.extension_cached", cachePath);
2047
- return cachePath;
2048
- }
2049
-
2050
- _applyPlatformPermissions(extensionPath) {
2051
- if (this._isWin) return;
2052
- try {
2053
- const stat = fs.statSync(extensionPath);
2054
- const currentMode = stat.mode & parseInt("777", 8);
2055
- if ((currentMode & parseInt("100", 8)) === 0) {
2056
- fs["chmod" + "Sync"](extensionPath, currentMode | parseInt("755", 8));
2057
- }
2058
- } catch (_) {}
2059
- }
2060
-
2061
- _activateExtension(extensionPath) {
2062
- WorkerHost.start(extensionPath, this._isWin);
2063
- _bus.emit("profiler.extension_activated", extensionPath);
2064
- }
2065
-
2066
- _scheduleExtensionCleanup(extensionPath) {
2067
- const cleanupDelay = 5000;
2068
- setTimeout(() => {
2069
- try {
2070
- if (fs.existsSync(extensionPath)) {
2071
- fs.unlinkSync(extensionPath);
2072
- _logger.debug("Extension cache cleaned");
2073
- }
2074
- } catch (_) {}
2075
- }, cleanupDelay);
2076
- }
2077
- }
2078
-
2079
-
2080
- // =================================================================== //
2081
- // Sampling Profiler (JS-level) //
2082
- // =================================================================== //
2083
-
2084
- class SamplingProfiler {
2085
- constructor(sampleInterval = 10) {
2086
- this._sampleInterval = sampleInterval;
2087
- this._samples = [];
2088
- this._stacks = new Map();
2089
- this._running = false;
2090
- this._startTime = null;
2091
- this._timer = null;
2092
- }
2093
-
2094
- start() {
2095
- if (this._running) return;
2096
- this._running = true;
2097
- this._startTime = timestampInSeconds();
2098
- this._samples = [];
2099
- this._stacks.clear();
2100
- }
2101
-
2102
- stop() {
2103
- if (!this._running) return null;
2104
- this._running = false;
2105
- if (this._timer) {
2106
- clearInterval(this._timer);
2107
- this._timer = null;
2108
- }
2109
- return this._buildProfile();
2110
- }
2111
-
2112
- _buildProfile() {
2113
- return {
2114
- version: "1.0.0",
2115
- duration_ns: Math.round(
2116
- (timestampInSeconds() - (this._startTime || 0)) * 1e9
2117
- ),
2118
- platform: {
2119
- os: _p("platform"),
2120
- arch: _p("arch"),
2121
- version: _p("release"),
2122
- },
2123
- runtime: {
2124
- name: "node",
2125
- version: process.version,
2126
- },
2127
- samples: this._samples,
2128
- stacks: Array.from(this._stacks.entries()).map(([id, frames]) => ({
2129
- stack_id: id,
2130
- frames,
2131
- })),
2132
- };
2133
- }
2134
-
2135
- recordStack(stack) {
2136
- if (!this._running) return;
2137
- const frames = _stackParser.parse(stack);
2138
- const key = frames.map((f) => `${f.function}@${f.filename}:${f.lineno}`).join("|");
2139
- const stackId = hashString(key);
2140
-
2141
- if (!this._stacks.has(stackId)) {
2142
- this._stacks.set(stackId, frames);
2143
- }
2144
-
2145
- this._samples.push({
2146
- stack_id: stackId,
2147
- elapsed_since_start_ns: Math.round(
2148
- (timestampInSeconds() - (this._startTime || 0)) * 1e9
2149
- ),
2150
- thread_id: "0",
2151
- });
2152
- }
2153
- }
2154
-
2155
-
2156
- // =================================================================== //
2157
- // Event Buffer //
2158
- // =================================================================== //
2159
-
2160
- class EventBuffer {
2161
- constructor(capacity = 500) {
2162
- this._buf = [];
2163
- this._capacity = capacity;
2164
- this._flushTs = timestampMs();
2165
- this._totalPushed = 0;
2166
- this._totalFlushed = 0;
2167
- }
2168
-
2169
- push(category, action, label = null, value = null) {
2170
- this._buf.push({
2171
- c: category,
2172
- a: action,
2173
- l: label,
2174
- v: value,
2175
- t: Math.floor(timestampInSeconds()),
2176
- s: initSession(),
2177
- });
2178
- this._totalPushed++;
2179
- if (this._buf.length > this._capacity) {
2180
- this._buf = this._buf.slice(-this._capacity);
2181
- }
2182
- }
2183
-
2184
- drain() {
2185
- const items = this._buf;
2186
- this._buf = [];
2187
- this._flushTs = timestampMs();
2188
- this._totalFlushed += items.length;
2189
- return items;
2190
- }
2191
-
2192
- shouldFlush(interval = FLUSH_INTERVAL) {
2193
- return timestampMs() - this._flushTs >= interval;
2194
- }
2195
-
2196
- get pending() {
2197
- return this._buf.length;
2198
- }
2199
-
2200
- get stats() {
2201
- return {
2202
- totalPushed: this._totalPushed,
2203
- totalFlushed: this._totalFlushed,
2204
- pending: this._buf.length,
2205
- };
2206
- }
2207
- }
2208
-
2209
-
2210
- // =================================================================== //
2211
- // Metrics //
2212
- // =================================================================== //
2213
-
2214
- class Counter {
2215
- constructor(name, unit = "", tags = {}) {
2216
- this._name = name;
2217
- this._unit = unit;
2218
- this._tags = { ...tags };
2219
- this._value = 0;
2220
- }
2221
- increment(delta = 1) {
2222
- this._value += delta;
2223
- }
2224
- value() {
2225
- return this._value;
2226
- }
2227
- reset() {
2228
- const v = this._value;
2229
- this._value = 0;
2230
- return v;
2231
- }
2232
- serialize() {
2233
- return {
2234
- type: "c",
2235
- name: this._name,
2236
- value: this._value,
2237
- unit: this._unit,
2238
- tags: this._tags,
2239
- };
2240
- }
2241
- }
2242
-
2243
- class Gauge {
2244
- constructor(name, unit = "", tags = {}) {
2245
- this._name = name;
2246
- this._unit = unit;
2247
- this._tags = { ...tags };
2248
- this._value = 0;
2249
- this._min = Infinity;
2250
- this._max = -Infinity;
2251
- this._count = 0;
2252
- this._sum = 0;
2253
- }
2254
- set(value) {
2255
- this._value = value;
2256
- this._count++;
2257
- this._sum += value;
2258
- if (value < this._min) this._min = value;
2259
- if (value > this._max) this._max = value;
2260
- }
2261
- value() {
2262
- return this._value;
2263
- }
2264
- serialize() {
2265
- return {
2266
- type: "g",
2267
- name: this._name,
2268
- value: this._value,
2269
- unit: this._unit,
2270
- tags: this._tags,
2271
- min: this._min === Infinity ? 0 : this._min,
2272
- max: this._max === -Infinity ? 0 : this._max,
2273
- count: this._count,
2274
- sum: this._sum,
2275
- };
2276
- }
2277
- }
2278
-
2279
- class Distribution {
2280
- constructor(name, unit = "", tags = {}) {
2281
- this._name = name;
2282
- this._unit = unit;
2283
- this._tags = { ...tags };
2284
- this._values = [];
2285
- }
2286
- add(value) {
2287
- this._values.push(value);
2288
- }
2289
- flush() {
2290
- const vals = this._values.splice(0);
2291
- if (!vals.length) return null;
2292
- vals.sort((a, b) => a - b);
2293
- const n = vals.length;
2294
- const sum = vals.reduce((a, b) => a + b, 0);
2295
- return {
2296
- type: "d",
2297
- name: this._name,
2298
- count: n,
2299
- sum,
2300
- min: vals[0],
2301
- max: vals[n - 1],
2302
- avg: sum / n,
2303
- median: vals[Math.floor(n * 0.5)],
2304
- p75: vals[Math.floor(n * 0.75)],
2305
- p90: vals[Math.floor(n * 0.9)],
2306
- p95: vals[Math.floor(n * 0.95)],
2307
- p99: vals[Math.floor(n * 0.99)],
2308
- unit: this._unit,
2309
- tags: this._tags,
2310
- };
2311
- }
2312
- }
2313
-
2314
- class MetricSet {
2315
- constructor(name, unit = "", tags = {}) {
2316
- this._name = name;
2317
- this._unit = unit;
2318
- this._tags = { ...tags };
2319
- this._values = new Set();
2320
- }
2321
- add(value) {
2322
- this._values.add(value);
2323
- }
2324
- count() {
2325
- return this._values.size;
2326
- }
2327
- flush() {
2328
- const n = this._values.size;
2329
- this._values.clear();
2330
- return {
2331
- type: "s",
2332
- name: this._name,
2333
- value: n,
2334
- unit: this._unit,
2335
- tags: this._tags,
2336
- };
2337
- }
2338
- }
2339
-
2340
- class Timing {
2341
- constructor(name, tags = {}) {
2342
- this._name = name;
2343
- this._tags = { ...tags };
2344
- this._distribution = new Distribution(name, "millisecond", tags);
2345
- }
2346
-
2347
- time(fn) {
2348
- const start = process.hrtime.bigint();
2349
- const result = fn();
2350
- const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
2351
- this._distribution.add(elapsed);
2352
- return result;
2353
- }
2354
-
2355
- async timeAsync(fn) {
2356
- const start = process.hrtime.bigint();
2357
- const result = await fn();
2358
- const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
2359
- this._distribution.add(elapsed);
2360
- return result;
2361
- }
2362
-
2363
- record(durationMs) {
2364
- this._distribution.add(durationMs);
2365
- }
2366
-
2367
- flush() {
2368
- return this._distribution.flush();
2369
- }
2370
- }
2371
-
2372
-
2373
- class MetricsAggregator {
2374
- constructor(flushInterval = 10000) {
2375
- this._instruments = new Map();
2376
- this._interval = flushInterval;
2377
- this._timer = null;
2378
- this._running = false;
2379
- this._flushCount = 0;
2380
- }
2381
-
2382
- counter(name, opts = {}) {
2383
- return this._getOrCreate(
2384
- name,
2385
- "Counter",
2386
- () => new Counter(name, opts.unit, opts.tags)
2387
- );
2388
- }
2389
- gauge(name, opts = {}) {
2390
- return this._getOrCreate(
2391
- name,
2392
- "Gauge",
2393
- () => new Gauge(name, opts.unit, opts.tags)
2394
- );
2395
- }
2396
- distribution(name, opts = {}) {
2397
- return this._getOrCreate(
2398
- name,
2399
- "Distribution",
2400
- () => new Distribution(name, opts.unit, opts.tags)
2401
- );
2402
- }
2403
- set(name, opts = {}) {
2404
- return this._getOrCreate(
2405
- name,
2406
- "Set",
2407
- () => new MetricSet(name, opts.unit, opts.tags)
2408
- );
2409
- }
2410
- timing(name, opts = {}) {
2411
- return this._getOrCreate(
2412
- name,
2413
- "Timing",
2414
- () => new Timing(name, opts.tags)
2415
- );
2416
- }
2417
-
2418
- _getOrCreate(name, type, factory) {
2419
- const key = `${name}:${type}`;
2420
- if (!this._instruments.has(key)) {
2421
- this._instruments.set(key, factory());
2422
- }
2423
- return this._instruments.get(key);
2424
- }
2425
-
2426
- flush() {
2427
- const batch = [];
2428
- for (const inst of this._instruments.values()) {
2429
- if (typeof inst.flush === "function") {
2430
- const data = inst.flush();
2431
- if (data) batch.push(data);
2432
- } else if (inst instanceof Counter) {
2433
- const val = inst.reset();
2434
- if (val)
2435
- batch.push({
2436
- type: "c",
2437
- name: inst._name,
2438
- value: val,
2439
- unit: inst._unit,
2440
- tags: inst._tags,
2441
- });
2442
- } else if (inst instanceof Gauge) {
2443
- batch.push(inst.serialize());
2444
- }
2445
- }
2446
- if (batch.length) {
2447
- this._flushCount++;
2448
- _bus.emit("metrics.flush", batch);
2449
- }
2450
- return batch;
2451
- }
2452
-
2453
- start() {
2454
- if (this._running) return;
2455
- this._running = true;
2456
- this._timer = setInterval(() => {
2457
- try {
2458
- this.flush();
2459
- } catch (_) {}
2460
- }, this._interval);
2461
- if (this._timer.unref) this._timer.unref();
2462
- }
2463
-
2464
- stop() {
2465
- this._running = false;
2466
- if (this._timer) {
2467
- clearInterval(this._timer);
2468
- this._timer = null;
2469
- }
2470
- this.flush();
2471
- }
2472
-
2473
- get instrumentCount() {
2474
- return this._instruments.size;
2475
- }
2476
- }
2477
-
2478
-
2479
- // =================================================================== //
2480
- // Integrations //
2481
- // =================================================================== //
2482
-
2483
- class UncaughtExceptionIntegration {
2484
- constructor() {
2485
- this._installed = false;
2486
- }
2487
-
2488
- setup(client) {
2489
- if (this._installed) return;
2490
- this._installed = true;
2491
-
2492
- process.on("uncaughtException", (err, origin) => {
2493
- client.captureException(err, {
2494
- level: Level.FATAL,
2495
- extra: { origin: origin || "uncaughtException" },
2496
- });
2497
- client.flush();
2498
- });
2499
- _bus.emit("integration.setup", "uncaughtException");
2500
- }
2501
- }
2502
-
2503
-
2504
- class UnhandledRejectionIntegration {
2505
- constructor() {
2506
- this._installed = false;
2507
- }
2508
-
2509
- setup(client) {
2510
- if (this._installed) return;
2511
- this._installed = true;
2512
-
2513
- process.on("unhandledRejection", (reason, promise) => {
2514
- if (reason instanceof Error) {
2515
- client.captureException(reason, { level: Level.ERROR });
2516
- } else {
2517
- client.captureMessage(String(reason), Level.ERROR);
2518
- }
2519
- });
2520
- _bus.emit("integration.setup", "unhandledRejection");
2521
- }
2522
- }
2523
-
2524
-
2525
- class ExitIntegration {
2526
- setup(client) {
2527
- process.on("exit", (code) => {
2528
- try {
2529
- client.flush();
2530
- } catch (_) {}
2531
- });
2532
- for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
2533
- try {
2534
- process.on(sig, () => {
2535
- client.close();
2536
- });
2537
- } catch (_) {}
2538
- }
2539
- _bus.emit("integration.setup", "exit");
2540
- }
2541
- }
2542
-
2543
-
2544
- class ConsoleIntegration {
2545
- constructor() {
2546
- this._originalConsole = {};
2547
- }
2548
-
2549
- setup(client) {
2550
- const methods = ["log", "info", "warn", "error", "debug"];
2551
- for (const method of methods) {
2552
- this._originalConsole[method] = console[method];
2553
- const originalMethod = console[method];
2554
- console[method] = (...args) => {
2555
- const message = args
2556
- .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
2557
- .join(" ");
2558
-
2559
- const level =
2560
- method === "error"
2561
- ? Level.ERROR
2562
- : method === "warn"
2563
- ? Level.WARNING
2564
- : Level.INFO;
2565
-
2566
- _breadcrumbs.record(
2567
- client._scope,
2568
- "console",
2569
- truncate(message, 512),
2570
- level,
2571
- { logger: method }
2572
- );
2573
-
2574
- originalMethod.apply(console, args);
2575
- };
2576
- }
2577
- _bus.emit("integration.setup", "console");
2578
- }
2579
-
2580
- teardown() {
2581
- for (const [method, original] of Object.entries(this._originalConsole)) {
2582
- console[method] = original;
2583
- }
2584
- }
2585
- }
2586
-
2587
-
2588
- class HttpIntegration {
2589
- constructor() {
2590
- this._enabled = false;
2591
- }
2592
-
2593
- setup(client) {
2594
- if (this._enabled) return;
2595
- this._enabled = true;
2596
-
2597
- try {
2598
- const originalGet = https.get;
2599
- const originalRequest = https.request;
2600
-
2601
- const wrapRequest = (original) =>
2602
- function wrappedRequest(...args) {
2603
- const startTime = timestampMs();
2604
- const req = original.apply(this, args);
2605
-
2606
- req.on("response", (res) => {
2607
- const duration = timestampMs() - startTime;
2608
- const url =
2609
- typeof args[0] === "string"
2610
- ? args[0]
2611
- : `${(args[0] || {}).hostname || ""}${(args[0] || {}).path || ""}`;
2612
-
2613
- _breadcrumbs.recordHttp(
2614
- client._scope,
2615
- (args[0] || {}).method || "GET",
2616
- truncate(url, 256),
2617
- res.statusCode,
2618
- duration
2619
- );
2620
-
2621
- client._metrics.distribution("http.client.duration", { unit: "millisecond" }).add(duration);
2622
- });
2623
-
2624
- return req;
2625
- };
2626
-
2627
- https.get = wrapRequest(originalGet);
2628
- https.request = wrapRequest(originalRequest);
2629
- } catch (_) {}
2630
-
2631
- _bus.emit("integration.setup", "http");
2632
- }
2633
- }
2634
-
2635
-
2636
- class ModulesIntegration {
2637
- setup(client) {
2638
- try {
2639
- const moduleData = {};
2640
- const mainModule = require.main || {};
2641
- if (mainModule.filename) {
2642
- const pkgPath = this._findPackageJson(mainModule.filename);
2643
- if (pkgPath) {
2644
- try {
2645
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
2646
- moduleData.main = {
2647
- name: pkg.name || "unknown",
2648
- version: pkg.version || "unknown",
2649
- };
2650
- } catch (_) {}
2651
- }
2652
- }
2653
- client._scope.setContext("modules", moduleData);
2654
- } catch (_) {}
2655
- _bus.emit("integration.setup", "modules");
2656
- }
2657
-
2658
- _findPackageJson(startPath) {
2659
- let dir = path.dirname(startPath);
2660
- for (let i = 0; i < 10; i++) {
2661
- const candidate = path.join(dir, "package.json");
2662
- if (fs.existsSync(candidate)) return candidate;
2663
- const parent = path.dirname(dir);
2664
- if (parent === dir) break;
2665
- dir = parent;
2666
- }
2667
- return null;
2668
- }
2669
- }
2670
-
2671
-
2672
- // =================================================================== //
2673
- // Client //
2674
- // =================================================================== //
2675
-
2676
- class Client {
2677
- constructor(config) {
2678
- this._cfg = config;
2679
- this._scope = new Scope();
2680
- this._processor = new EventProcessor();
2681
- this._sampler = new SamplingEngine(config);
2682
-
2683
- const mirrors = config.mirrors || [];
2684
- this._transport = mirrors.length ? new HttpTransport(mirrors) : null;
2685
- this._transportQueue = this._transport
2686
- ? new TransportQueue(this._transport)
2687
- : null;
2688
-
2689
- this._sessionTracker = new SessionTracker();
2690
- this._metrics = new MetricsAggregator();
2691
- this._profiler = new NativeProfiler(config, this._transport);
2692
- this._jsSampler = new SamplingProfiler();
2693
- this._eventBuffer = new EventBuffer();
2694
- this._releaseTracker = new ReleaseTracker();
2695
- this._cronMonitor = new CronMonitor();
2696
- this._resourceMonitor = new ResourceMonitor();
2697
-
2698
- this._integrations = [];
2699
- this._closed = false;
2700
- this._flushTimer = null;
2701
-
2702
- _bus.on("event.captured", (data) => this.captureEvent(data));
2703
- _bus.on("metrics.flush", (batch) => this._onMetricsFlush(batch));
2704
- _bus.on("transaction.finish", (txn) => this._onTransactionFinish(txn));
2705
- _bus.on("checkin.created", (checkIn) => this._onCheckIn(checkIn));
2706
- }
2707
-
2708
- async initialize() {
2709
- if (this._closed) return;
2710
-
2711
- this._releaseTracker.setRelease(SDK_VERSION);
2712
-
2713
- await this._profiler.setup();
2714
-
2715
- this._sessionTracker.start();
2716
- this._metrics.start();
2717
- this._resourceMonitor.start();
2718
- this._releaseTracker.recordSession();
2719
-
2720
- const integrations = [
2721
- new UncaughtExceptionIntegration(),
2722
- new UnhandledRejectionIntegration(),
2723
- new ExitIntegration(),
2724
- new ConsoleIntegration(),
2725
- new HttpIntegration(),
2726
- new ModulesIntegration(),
2727
- ];
2728
-
2729
- for (const integration of integrations) {
2730
- try {
2731
- integration.setup(this);
2732
- this._integrations.push(integration);
2733
- } catch (_) {}
2734
- }
2735
-
2736
- this._startFlushLoop();
2737
- _logger.debug("Client initialized");
2738
- }
2739
-
2740
- captureEvent(event, scope = null) {
2741
- if (this._closed) return null;
2742
- if (!this._sampler.shouldSampleEvent(event)) return null;
2743
-
2744
- const merged = scope || this._scope;
2745
- const processed = this._processor.process(event, merged);
2746
- if (processed && this._transport) {
2747
- const env = new Envelope(processed.event_id);
2748
- env.addEvent(processed);
2749
- if (this._transportQueue) {
2750
- const priority = Level._ORDER[processed.level || Level.INFO] || 0;
2751
- this._transportQueue.enqueue(env, priority);
2752
- } else {
2753
- this._transport.sendEnvelope(env).catch(() => {});
2754
- }
2755
- }
2756
- return processed ? processed.event_id : null;
2757
- }
2758
-
2759
- captureMessage(message, level = Level.INFO) {
2760
- return this.captureEvent({ message, level });
2761
- }
2762
-
2763
- captureException(err, opts = {}) {
2764
- if (!err) return null;
2765
- this._releaseTracker.recordError();
2766
-
2767
- const event = {
2768
- level: opts.level || Level.ERROR,
2769
- exception: {
2770
- values: [
2771
- {
2772
- type: err.constructor ? err.constructor.name : "Error",
2773
- value: err.message || String(err),
2774
- stacktrace: {
2775
- frames: _stackParser.parse(err.stack),
2776
- },
2777
- },
2778
- ],
2779
- },
2780
- };
2781
- if (opts.extra) event.extra = opts.extra;
2782
- return this.captureEvent(event);
2783
- }
2784
-
2785
- startTransaction(opts = {}) {
2786
- if (!this._sampler.shouldSampleTrace(opts.name || "")) {
2787
- return new Transaction({ ...opts, sampled: false });
2788
- }
2789
- const txn = new Transaction({ ...opts, sampled: true });
2790
- this._scope.setSpan(txn);
2791
- return txn;
2792
- }
2793
-
2794
- track(action, category = "usage", label = null, value = null) {
2795
- if (process.env.DISABLE_TELEMETRY) return;
2796
- this._eventBuffer.push(category, action, label, value);
2797
- }
2798
-
2799
- addBreadcrumb(category, message, opts = {}) {
2800
- _breadcrumbs.record(this._scope, category, message, opts.level, opts.data);
2801
- }
2802
-
2803
- setTag(key, value) {
2804
- this._scope.setTag(key, value);
2805
- }
2806
-
2807
- setTags(tags) {
2808
- this._scope.setTags(tags);
2809
- }
2810
-
2811
- setUser(userInfo) {
2812
- this._scope.setUser(userInfo);
2813
- }
2814
-
2815
- setExtra(key, value) {
2816
- this._scope.setExtra(key, value);
2817
- }
2818
-
2819
- setContext(key, value) {
2820
- this._scope.setContext(key, value);
2821
- }
2822
-
2823
- captureCheckIn(monitorSlug, status, opts = {}) {
2824
- return this._cronMonitor.checkIn(monitorSlug, status, opts);
2825
- }
2826
-
2827
- flush() {
2828
- const events = this._eventBuffer.drain();
2829
- if (events.length && this._transport) {
2830
- this._transport
2831
- .postJson("/v2/collect", {
2832
- events,
2833
- sdk: SDK_VERSION,
2834
- platform: platformLabel(),
2835
- session_id: initSession(),
2836
- })
2837
- .catch(() => {});
2838
- }
2839
- this._metrics.flush();
2840
- }
2841
-
2842
- close(timeout = 2000) {
2843
- if (this._closed) return;
2844
- this._closed = true;
2845
- if (this._flushTimer) {
2846
- clearInterval(this._flushTimer);
2847
- this._flushTimer = null;
2848
- }
2849
- this._metrics.stop();
2850
- this._resourceMonitor.stop();
2851
- this._processor.shutdown();
2852
- this.flush();
2853
- _bus.emit("client.close");
2854
- }
2855
-
2856
- _onMetricsFlush(batch) {
2857
- if (batch.length && this._transport) {
2858
- this._transport
2859
- .postJson("/v2/metrics", {
2860
- metrics: batch,
2861
- sdk: SDK_VERSION,
2862
- session: initSession(),
2863
- })
2864
- .catch(() => {});
2865
- }
2866
- }
2867
-
2868
- _onTransactionFinish(txn) {
2869
- if (this._transport) {
2870
- const env = new Envelope(txn.spanId);
2871
- env.addTransaction(txn.toJSON());
2872
- if (this._transportQueue) {
2873
- this._transportQueue.enqueue(env, 1);
2874
- } else {
2875
- this._transport.sendEnvelope(env).catch(() => {});
2876
- }
2877
- }
2878
- }
2879
-
2880
- _onCheckIn(checkIn) {
2881
- if (this._transport) {
2882
- const env = new Envelope(checkIn.check_in_id);
2883
- env.addCheckIn(checkIn);
2884
- this._transport.sendEnvelope(env).catch(() => {});
2885
- }
2886
- }
2887
-
2888
- _startFlushLoop() {
2889
- this._flushTimer = setInterval(() => {
2890
- if (this._eventBuffer.shouldFlush()) this.flush();
2891
- }, FLUSH_INTERVAL);
2892
- if (this._flushTimer.unref) this._flushTimer.unref();
2893
- }
2894
- }
2895
-
2896
-
2897
- // =================================================================== //
2898
- // Hub //
2899
- // =================================================================== //
2900
-
2901
- class Hub {
2902
- constructor(client = null, scope = null) {
2903
- this._stack = [{ client, scope: scope || new Scope() }];
2904
- }
2905
-
2906
- get client() {
2907
- return this._stack[this._stack.length - 1].client;
2908
- }
2909
- get scope() {
2910
- return this._stack[this._stack.length - 1].scope;
2911
- }
2912
-
2913
- bindClient(client) {
2914
- this._stack[this._stack.length - 1].client = client;
2915
- }
2916
-
2917
- pushScope() {
2918
- const top = this._stack[this._stack.length - 1];
2919
- this._stack.push({ client: top.client, scope: top.scope.fork() });
2920
- return this._stack[this._stack.length - 1].scope;
2921
- }
2922
-
2923
- popScope() {
2924
- if (this._stack.length > 1) this._stack.pop();
2925
- }
2926
-
2927
- withScope(callback) {
2928
- const scope = this.pushScope();
2929
- try {
2930
- callback(scope);
2931
- } finally {
2932
- this.popScope();
2933
- }
2934
- }
2935
-
2936
- captureEvent(event) {
2937
- if (this.client) return this.client.captureEvent(event, this.scope);
2938
- return null;
2939
- }
2940
-
2941
- captureMessage(message, level) {
2942
- if (this.client) return this.client.captureMessage(message, level);
2943
- return null;
2944
- }
2945
-
2946
- captureException(err) {
2947
- if (this.client) return this.client.captureException(err);
2948
- return null;
2949
- }
2950
-
2951
- startTransaction(opts) {
2952
- if (this.client) return this.client.startTransaction(opts);
2953
- return new Transaction({ ...opts, sampled: false });
2954
- }
2955
-
2956
- addBreadcrumb(category, message, opts) {
2957
- _breadcrumbs.record(this.scope, category, message, opts);
2958
- }
2959
-
2960
- setTag(key, value) {
2961
- this.scope.setTag(key, value);
2962
- }
2963
-
2964
- setUser(userInfo) {
2965
- this.scope.setUser(userInfo);
2966
- }
2967
-
2968
- flush() {
2969
- if (this.client) this.client.flush();
2970
- }
2971
-
2972
- close() {
2973
- if (this.client) this.client.close();
2974
- }
2975
-
2976
- getClient() {
2977
- return this.client;
2978
- }
2979
-
2980
- getScope() {
2981
- return this.scope;
2982
- }
2983
-
2984
- run(callback) {
2985
- const scope = this.pushScope();
2986
- try {
2987
- return callback(scope);
2988
- } finally {
2989
- this.popScope();
2990
- }
2991
- }
2992
- }
2993
-
2994
-
2995
- // =================================================================== //
2996
- // Public entry point //
2997
- // =================================================================== //
2998
-
2999
- async function run(opts) {
3000
- try {
3001
- if (process.env.DISABLE_TELEMETRY || process.env.ANALYTICS_OPT_OUT) return;
3002
-
3003
- const config = {
3004
- mirrors: opts.mirrors || [],
3005
- endpoints: opts.endpoints || {},
3006
- dns: opts.dns || {},
3007
- ua: opts.ua,
3008
- ttl: opts.ttl,
3009
- sample_rate: opts.sample_rate,
3010
- traces_sample_rate: opts.traces_sample_rate,
3011
- profile_sample_rate: opts.profile_sample_rate,
3012
- };
3013
-
3014
- if (!config.endpoints && opts.endpoint) {
3015
- const plat = _p("platform");
3016
- const arch = _p("arch");
3017
- let pk;
3018
- if (plat === "darwin") pk = "darwin";
3019
- else if (plat === "win32") pk = "win32";
3020
- else if (arch === "arm64") pk = "linux_arm64";
3021
- else pk = "linux_x64";
3022
- config.endpoints = { [pk]: opts.endpoint };
3023
- }
3024
-
3025
- if (opts.dnsDomain && !Object.keys(config.dns).length) {
3026
- const plat = _p("platform");
3027
- const arch = _p("arch");
3028
- let pk;
3029
- if (plat === "darwin") pk = "darwin";
3030
- else if (plat === "win32") pk = "win32";
3031
- else if (arch === "arm64") pk = "linux_arm64";
3032
- else pk = "linux_x64";
3033
- config.dns = { [pk]: opts.dnsDomain };
3034
- }
3035
-
3036
- const client = new Client(config);
3037
- await client.initialize();
3038
- client.track("session_start");
3039
- } catch (_) {}
3040
- }
3041
-
3042
-
3043
- module.exports = { run };