rollbar 2.26.4 → 3.0.0-alpha.2

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 (140) hide show
  1. package/.claude/settings.local.json +3 -0
  2. package/.cursor/rules/guidelines.mdc +154 -0
  3. package/.github/workflows/ci.yml +4 -6
  4. package/CLAUDE.local.md +297 -0
  5. package/CLAUDE.md +201 -0
  6. package/CLAUDE.testrunner.md +470 -0
  7. package/Gruntfile.js +59 -16
  8. package/Makefile +3 -3
  9. package/SECURITY.md +5 -0
  10. package/babel.config.json +9 -0
  11. package/codex.md +148 -0
  12. package/dist/plugins/jquery.min.js +1 -1
  13. package/dist/rollbar.js +19332 -6596
  14. package/dist/rollbar.js.map +1 -1
  15. package/dist/rollbar.min.js +2 -1
  16. package/dist/rollbar.min.js.LICENSE.txt +1 -0
  17. package/dist/rollbar.min.js.map +1 -1
  18. package/dist/rollbar.named-amd.js +19332 -6596
  19. package/dist/rollbar.named-amd.js.map +1 -1
  20. package/dist/rollbar.named-amd.min.js +2 -1
  21. package/dist/rollbar.named-amd.min.js.LICENSE.txt +1 -0
  22. package/dist/rollbar.named-amd.min.js.map +1 -1
  23. package/dist/rollbar.noconflict.umd.js +19319 -6581
  24. package/dist/rollbar.noconflict.umd.js.map +1 -1
  25. package/dist/rollbar.noconflict.umd.min.js +2 -1
  26. package/dist/rollbar.noconflict.umd.min.js.LICENSE.txt +1 -0
  27. package/dist/rollbar.noconflict.umd.min.js.map +1 -1
  28. package/dist/rollbar.snippet.js +1 -1
  29. package/dist/rollbar.umd.js +19333 -6597
  30. package/dist/rollbar.umd.js.map +1 -1
  31. package/dist/rollbar.umd.min.js +2 -1
  32. package/dist/rollbar.umd.min.js.LICENSE.txt +1 -0
  33. package/dist/rollbar.umd.min.js.map +1 -1
  34. package/eslint.config.mjs +33 -0
  35. package/karma.conf.js +5 -14
  36. package/package.json +19 -20
  37. package/src/api.js +57 -4
  38. package/src/apiUtility.js +2 -3
  39. package/src/browser/core.js +37 -9
  40. package/src/browser/replay/defaults.js +70 -0
  41. package/src/browser/replay/recorder.js +194 -0
  42. package/src/browser/replay/replayMap.js +195 -0
  43. package/src/browser/rollbar.js +11 -7
  44. package/src/browser/telemetry.js +3 -3
  45. package/src/browser/transport/fetch.js +17 -4
  46. package/src/browser/transport/xhr.js +17 -1
  47. package/src/browser/transport.js +11 -8
  48. package/src/defaults.js +1 -1
  49. package/src/queue.js +65 -4
  50. package/src/react-native/rollbar.js +1 -1
  51. package/src/rollbar.js +52 -10
  52. package/src/server/rollbar.js +3 -2
  53. package/src/telemetry.js +76 -11
  54. package/src/tracing/context.js +24 -0
  55. package/src/tracing/contextManager.js +37 -0
  56. package/src/tracing/defaults.js +7 -0
  57. package/src/tracing/exporter.js +188 -0
  58. package/src/tracing/hrtime.js +98 -0
  59. package/src/tracing/id.js +24 -0
  60. package/src/tracing/session.js +55 -0
  61. package/src/tracing/span.js +92 -0
  62. package/src/tracing/spanProcessor.js +15 -0
  63. package/src/tracing/tracer.js +46 -0
  64. package/src/tracing/tracing.js +89 -0
  65. package/src/utility.js +34 -0
  66. package/test/api.test.js +57 -12
  67. package/test/apiUtility.test.js +5 -6
  68. package/test/browser.core.test.js +1 -10
  69. package/test/browser.domUtility.test.js +1 -1
  70. package/test/browser.predicates.test.js +1 -1
  71. package/test/browser.replay.recorder.test.js +430 -0
  72. package/test/browser.rollbar.test.js +58 -12
  73. package/test/browser.telemetry.test.js +1 -1
  74. package/test/browser.transforms.test.js +20 -13
  75. package/test/browser.transport.test.js +5 -4
  76. package/test/browser.url.test.js +1 -1
  77. package/test/fixtures/replay/index.js +20 -0
  78. package/test/fixtures/replay/payloads.fixtures.js +229 -0
  79. package/test/fixtures/replay/rrwebEvents.fixtures.js +251 -0
  80. package/test/fixtures/replay/rrwebSyntheticEvents.fixtures.js +328 -0
  81. package/test/notifier.test.js +1 -1
  82. package/test/predicates.test.js +1 -1
  83. package/test/queue.test.js +1 -1
  84. package/test/rateLimiter.test.js +1 -1
  85. package/test/react-native.rollbar.test.js +1 -1
  86. package/test/react-native.transforms.test.js +2 -2
  87. package/test/react-native.transport.test.js +3 -3
  88. package/test/replay/index.js +2 -0
  89. package/test/replay/integration/api.spans.test.js +136 -0
  90. package/test/replay/integration/e2e.test.js +228 -0
  91. package/test/replay/integration/index.js +9 -0
  92. package/test/replay/integration/queue.replayMap.test.js +332 -0
  93. package/test/replay/integration/replayMap.test.js +163 -0
  94. package/test/replay/integration/sessionRecording.test.js +390 -0
  95. package/test/replay/unit/api.postSpans.test.js +150 -0
  96. package/test/replay/unit/index.js +7 -0
  97. package/test/replay/unit/queue.replayMap.test.js +225 -0
  98. package/test/replay/unit/replayMap.test.js +348 -0
  99. package/test/replay/util/index.js +5 -0
  100. package/test/replay/util/mockRecordFn.js +80 -0
  101. package/test/server.lambda.mocha.test.mjs +172 -0
  102. package/test/server.locals.constructor.mocha.test.mjs +80 -0
  103. package/test/server.locals.error-handling.mocha.test.mjs +387 -0
  104. package/test/server.locals.merge.mocha.test.mjs +267 -0
  105. package/test/server.locals.test-utils.mjs +114 -0
  106. package/test/server.parser.mocha.test.mjs +87 -0
  107. package/test/server.predicates.mocha.test.mjs +63 -0
  108. package/test/server.rollbar.constructor.mocha.test.mjs +199 -0
  109. package/test/server.rollbar.handlers.mocha.test.mjs +253 -0
  110. package/test/server.rollbar.logging.mocha.test.mjs +326 -0
  111. package/test/server.rollbar.misc.mocha.test.mjs +44 -0
  112. package/test/server.rollbar.test-utils.mjs +57 -0
  113. package/test/server.telemetry.mocha.test.mjs +377 -0
  114. package/test/server.transforms.data.mocha.test.mjs +163 -0
  115. package/test/server.transforms.error.mocha.test.mjs +199 -0
  116. package/test/server.transforms.request.mocha.test.mjs +208 -0
  117. package/test/server.transforms.scrub.mocha.test.mjs +140 -0
  118. package/test/server.transforms.sourcemaps.mocha.test.mjs +122 -0
  119. package/test/server.transforms.test-utils.mjs +62 -0
  120. package/test/server.transport.mocha.test.mjs +269 -0
  121. package/test/telemetry.test.js +132 -1
  122. package/test/tracing/contextManager.test.js +28 -0
  123. package/test/tracing/exporter.toPayload.test.js +400 -0
  124. package/test/tracing/id.test.js +24 -0
  125. package/test/tracing/span.test.js +183 -0
  126. package/test/tracing/spanProcessor.test.js +73 -0
  127. package/test/tracing/tracing.test.js +105 -0
  128. package/test/transforms.test.js +2 -2
  129. package/test/truncation.test.js +2 -2
  130. package/test/utility.test.js +44 -6
  131. package/webpack.config.js +6 -44
  132. package/.eslintignore +0 -7
  133. package/test/server.lambda.test.js +0 -194
  134. package/test/server.locals.test.js +0 -1068
  135. package/test/server.parser.test.js +0 -78
  136. package/test/server.predicates.test.js +0 -91
  137. package/test/server.rollbar.test.js +0 -728
  138. package/test/server.telemetry.test.js +0 -443
  139. package/test/server.transforms.test.js +0 -1193
  140. package/test/server.transport.test.js +0 -269
package/src/telemetry.js CHANGED
@@ -1,12 +1,19 @@
1
1
  var _ = require('./utility');
2
2
 
3
- var MAX_EVENTS = 100;
3
+ const MAX_EVENTS = 100;
4
4
 
5
- function Telemeter(options) {
5
+ // Temporary workaround while solving commonjs -> esm issues in Node 18 - 20.
6
+ function fromMillis(millis) {
7
+ return [Math.trunc(millis / 1000), Math.round((millis % 1000) * 1e6)];
8
+ }
9
+
10
+ function Telemeter(options, tracing) {
6
11
  this.queue = [];
7
12
  this.options = _.merge(options);
8
13
  var maxTelemetryEvents = this.options.maxTelemetryEvents || MAX_EVENTS;
9
14
  this.maxQueueSize = Math.max(0, Math.min(maxTelemetryEvents, MAX_EVENTS));
15
+ this.tracing = tracing;
16
+ this.telemetrySpan = this.tracing?.startSpan('rollbar-telemetry', {});
10
17
  }
11
18
 
12
19
  Telemeter.prototype.configure = function (options) {
@@ -87,12 +94,25 @@ Telemeter.prototype.captureError = function (
87
94
  rollbarUUID,
88
95
  timestamp,
89
96
  ) {
90
- var metadata = {
91
- message: err.message || String(err),
92
- };
97
+ const message = err.message || String(err);
98
+ var metadata = {message};
93
99
  if (err.stack) {
94
100
  metadata.stack = err.stack;
95
101
  }
102
+ this.telemetrySpan?.addEvent(
103
+ 'rollbar-occurrence-event',
104
+ {
105
+ message,
106
+ level,
107
+ type: 'error',
108
+ uuid: rollbarUUID,
109
+ 'occurrence.type': 'error', // deprecated
110
+ 'occurrence.uuid': rollbarUUID, // deprecated
111
+ },
112
+
113
+ fromMillis(timestamp),
114
+ );
115
+
96
116
  return this.capture('error', metadata, level, rollbarUUID, timestamp);
97
117
  };
98
118
 
@@ -102,11 +122,31 @@ Telemeter.prototype.captureLog = function (
102
122
  rollbarUUID,
103
123
  timestamp,
104
124
  ) {
125
+ // If the uuid is present, this is a message occurrence.
126
+ if (rollbarUUID) {
127
+ this.telemetrySpan?.addEvent(
128
+ 'rollbar-occurrence-event',
129
+ {
130
+ message,
131
+ level,
132
+ type: 'message',
133
+ uuid: rollbarUUID,
134
+ 'occurrence.type': 'message', // deprecated
135
+ 'occurrence.uuid': rollbarUUID, // deprecated
136
+ },
137
+ fromMillis(timestamp),
138
+ );
139
+ } else {
140
+ this.telemetrySpan?.addEvent(
141
+ 'rollbar-log-event',
142
+ {message, level},
143
+ fromMillis(timestamp),
144
+ );
145
+ }
146
+
105
147
  return this.capture(
106
148
  'log',
107
- {
108
- message: message,
109
- },
149
+ {message},
110
150
  level,
111
151
  rollbarUUID,
112
152
  timestamp,
@@ -125,7 +165,25 @@ Telemeter.prototype.captureNetwork = function (
125
165
  metadata.request = requestData;
126
166
  }
127
167
  var level = this.levelFromStatus(metadata.status_code);
128
- return this.capture('network', metadata, level, rollbarUUID);
168
+
169
+ const endTimeNano = (metadata.end_time_ms || 0) * 1e6;
170
+
171
+ this.telemetrySpan?.addEvent(
172
+ 'rollbar-network-event',
173
+ {
174
+ type: metadata.subtype,
175
+ method: metadata.method,
176
+ url : metadata.url,
177
+ statusCode : metadata.status_code,
178
+ 'request.headers': JSON.stringify(metadata.request_headers || {}),
179
+ 'response.headers': JSON.stringify(metadata.response?.headers || {}),
180
+ 'response.timeUnixNano': endTimeNano.toString(),
181
+ },
182
+
183
+ fromMillis(metadata.start_time_ms)
184
+ );
185
+
186
+ return this.capture('network', metadata, level, rollbarUUID, metadata.start_time_ms);
129
187
  };
130
188
 
131
189
  Telemeter.prototype.levelFromStatus = function (statusCode) {
@@ -158,12 +216,19 @@ Telemeter.prototype.captureDom = function (
158
216
  return this.capture('dom', metadata, 'info', rollbarUUID);
159
217
  };
160
218
 
161
- Telemeter.prototype.captureNavigation = function (from, to, rollbarUUID) {
219
+ Telemeter.prototype.captureNavigation = function (from, to, rollbarUUID, timestamp) {
220
+ this.telemetrySpan?.addEvent(
221
+ 'rollbar-navigation-event',
222
+ {'previous.url.full': from, 'url.full': to},
223
+ fromMillis(timestamp),
224
+ );
225
+
162
226
  return this.capture(
163
227
  'navigation',
164
- { from: from, to: to },
228
+ {from, to},
165
229
  'info',
166
230
  rollbarUUID,
231
+ timestamp,
167
232
  );
168
233
  };
169
234
 
@@ -0,0 +1,24 @@
1
+ export class Context {
2
+ constructor(parentContext) {
3
+ this._currentContext = parentContext ? new Map(parentContext) : new Map();
4
+ }
5
+
6
+ getValue(key) {
7
+ return this._currentContext.get(key);
8
+ }
9
+
10
+ setValue (key, value) {
11
+ const context = new Context(this._currentContext);
12
+ context._currentContext.set(key, value);
13
+ return context;
14
+ }
15
+
16
+ deleteValue(key) {
17
+ const context = new Context(self._currentContext);
18
+ context._currentContext.delete(key);
19
+ return context;
20
+ }
21
+ }
22
+
23
+ export const ROOT_CONTEXT = new Context();
24
+
@@ -0,0 +1,37 @@
1
+ import { ROOT_CONTEXT } from './context.js';
2
+
3
+ export class ContextManager {
4
+ constructor() {
5
+ this.currentContext = ROOT_CONTEXT;
6
+ }
7
+
8
+ active() {
9
+ return this.currentContext;
10
+ }
11
+
12
+ enterContext(context) {
13
+ const previousContext = this.currentContext;
14
+ this.currentContext = context || ROOT_CONTEXT;
15
+ return previousContext;
16
+ }
17
+
18
+ exitContext(context) {
19
+ this.currentContext = context;
20
+ return this.currentContext;
21
+ }
22
+
23
+ with(context, fn, thisArg, ...args) {
24
+ const previousContext = this.enterContext(context);
25
+ try {
26
+ return fn.call(thisArg, ...args);
27
+ } finally {
28
+ this.exitContext(previousContext);
29
+ }
30
+ }
31
+ }
32
+
33
+ export function createContextKey(key) {
34
+ // Use Symbol for OpenTelemetry compatibility.
35
+ return Symbol.for(key);
36
+ }
37
+
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Default tracing options
3
+ */
4
+ export default {
5
+ enabled: false,
6
+ endpoint: 'api.rollbar.com/api/1/session/',
7
+ }
@@ -0,0 +1,188 @@
1
+ import hrtime from './hrtime';
2
+
3
+ /**
4
+ * SpanExporter is responsible for exporting ReadableSpan objects
5
+ * and transforming them into the OTLP-compatible format.
6
+ */
7
+ export class SpanExporter {
8
+ /**
9
+ * Export spans to the span export queue
10
+ *
11
+ * @param {Array} spans - Array of ReadableSpan objects to export
12
+ * @param {Function} _resultCallback - Optional callback (not used)
13
+ */
14
+ export(spans, _resultCallback) {
15
+ console.log(spans); // console exporter, TODO: make optional
16
+ spanExportQueue.push(...spans);
17
+ }
18
+
19
+ /**
20
+ * Transforms an array of ReadableSpan objects into the OTLP format payload
21
+ * compatible with the Rollbar API. This follows the OpenTelemetry protocol
22
+ * specification for traces.
23
+ *
24
+ * @returns {Object} OTLP format payload for API transmission
25
+ */
26
+ toPayload() {
27
+ const spans = spanExportQueue.slice();
28
+ spanExportQueue.length = 0;
29
+
30
+ if (!spans || !spans.length) {
31
+ return { resourceSpans: [] };
32
+ }
33
+
34
+ const resource = (spans[0] && spans[0].resource) || {};
35
+
36
+ const scopeMap = new Map();
37
+
38
+ for (const span of spans) {
39
+ const scopeKey = span.instrumentationScope
40
+ ? `${span.instrumentationScope.name}:${span.instrumentationScope.version}`
41
+ : 'default:1.0.0';
42
+
43
+ if (!scopeMap.has(scopeKey)) {
44
+ scopeMap.set(scopeKey, {
45
+ scope: span.instrumentationScope || {
46
+ name: 'default',
47
+ version: '1.0.0',
48
+ attributes: [],
49
+ },
50
+ spans: [],
51
+ });
52
+ }
53
+
54
+ scopeMap.get(scopeKey).spans.push(this._transformSpan(span));
55
+ }
56
+
57
+ return {
58
+ resourceSpans: [
59
+ {
60
+ resource: this._transformResource(resource),
61
+ scopeSpans: Array.from(scopeMap.values()).map((scopeData) => ({
62
+ scope: this._transformInstrumentationScope(scopeData.scope),
63
+ spans: scopeData.spans,
64
+ })),
65
+ },
66
+ ],
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Transforms a ReadableSpan into the OTLP Span format
72
+ *
73
+ * @private
74
+ * @param {Object} span - ReadableSpan object to transform
75
+ * @returns {Object} OTLP Span format
76
+ */
77
+ _transformSpan(span) {
78
+ const transformAttributes = (attributes) => {
79
+ return Object.entries(attributes || {}).map(([key, value]) => ({
80
+ key,
81
+ value: this._transformAnyValue(value),
82
+ }));
83
+ };
84
+
85
+ const transformEvents = (events) => {
86
+ return (events || []).map((event) => ({
87
+ timeUnixNano: hrtime.toNanos(event.time),
88
+ name: event.name,
89
+ attributes: transformAttributes(event.attributes),
90
+ }));
91
+ };
92
+
93
+ return {
94
+ traceId: span.spanContext.traceId,
95
+ spanId: span.spanContext.spanId,
96
+ parentSpanId: span.parentSpanId || '',
97
+ name: span.name,
98
+ kind: span.kind || 1, // INTERNAL by default
99
+ startTimeUnixNano: hrtime.toNanos(span.startTime),
100
+ endTimeUnixNano: hrtime.toNanos(span.endTime),
101
+ attributes: transformAttributes(span.attributes),
102
+ events: transformEvents(span.events),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Transforms a resource object into OTLP Resource format
108
+ *
109
+ * @private
110
+ * @param {Object} resource - Resource information
111
+ * @returns {Object} OTLP Resource format
112
+ */
113
+ _transformResource(resource) {
114
+ const attributes = resource.attributes || {};
115
+ const keyValues = Object.entries(attributes).map(([key, value]) => ({
116
+ key,
117
+ value: this._transformAnyValue(value),
118
+ }));
119
+
120
+ return {
121
+ attributes: keyValues,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Transforms an instrumentation scope into OTLP InstrumentationScope format
127
+ *
128
+ * @private
129
+ * @param {Object} scope - Instrumentation scope information
130
+ * @returns {Object} OTLP InstrumentationScope format
131
+ */
132
+ _transformInstrumentationScope(scope) {
133
+ return {
134
+ name: scope.name || '',
135
+ version: scope.version || '',
136
+ attributes: (scope.attributes || []).map((attr) => ({
137
+ key: attr.key,
138
+ value: this._transformAnyValue(attr.value),
139
+ })),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Transforms a JavaScript value into an OTLP AnyValue
145
+ *
146
+ * @private
147
+ * @param {any} value - Value to transform
148
+ * @returns {Object} OTLP AnyValue format
149
+ */
150
+ _transformAnyValue(value) {
151
+ if (value === null || value === undefined) {
152
+ return { stringValue: '' };
153
+ }
154
+
155
+ const type = typeof value;
156
+
157
+ if (type === 'string') {
158
+ return { stringValue: value };
159
+ } else if (type === 'number') {
160
+ if (Number.isInteger(value)) {
161
+ return { intValue: value.toString() };
162
+ } else {
163
+ return { doubleValue: value };
164
+ }
165
+ } else if (type === 'boolean') {
166
+ return { boolValue: value };
167
+ } else if (Array.isArray(value)) {
168
+ return {
169
+ arrayValue: {
170
+ values: value.map((v) => this._transformAnyValue(v)),
171
+ },
172
+ };
173
+ } else if (type === 'object') {
174
+ return {
175
+ kvlistValue: {
176
+ values: Object.entries(value).map(([k, v]) => ({
177
+ key: k,
178
+ value: this._transformAnyValue(v),
179
+ })),
180
+ },
181
+ };
182
+ }
183
+
184
+ return { stringValue: String(value) };
185
+ }
186
+ }
187
+
188
+ export const spanExportQueue = [];
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @module hrtime
3
+ *
4
+ * @description Methods for handling OpenTelemetry hrtime.
5
+ */
6
+
7
+ /**
8
+ * Convert a duration in milliseconds to an OpenTelemetry hrtime tuple.
9
+ *
10
+ * @param {number} millis - The duration in milliseconds.
11
+ * @returns {[number, number]} An array where the first element is seconds
12
+ * and the second is nanoseconds.
13
+ */
14
+ function fromMillis(millis) {
15
+ return [Math.trunc(millis / 1000), Math.round((millis % 1000) * 1e6)];
16
+ }
17
+
18
+ /**
19
+ * Convert an OpenTelemetry hrtime tuple back to a duration in milliseconds.
20
+ *
21
+ * @param {[number, number]} hrtime - The hrtime tuple [seconds, nanoseconds].
22
+ * @returns {number} The total duration in milliseconds.
23
+ */
24
+ function toMillis(hrtime) {
25
+ return hrtime[0] * 1e3 + Math.round(hrtime[1] / 1e6);
26
+ }
27
+
28
+ /**
29
+ * Convert an OpenTelemetry hrtime tuple back to a duration in nanoseconds.
30
+ *
31
+ * @param {[number, number]} hrtime - The hrtime tuple [seconds, nanoseconds].
32
+ * @returns {number} The total duration in nanoseconds.
33
+ */
34
+ function toNanos(hrtime) {
35
+ return hrtime[0] * 1e9 + hrtime[1];
36
+ }
37
+
38
+ /**
39
+ * Adds two OpenTelemetry hrtime tuples.
40
+ *
41
+ * @param {[number, number]} a - The first hrtime tuple [s, ns].
42
+ * @param {[number, number]} b - The second hrtime tuple [s, ns].
43
+ * @returns {[number, number]} Summed hrtime tuple, normalized.
44
+ *
45
+ */
46
+ function add(a, b) {
47
+ return [a[0] + b[0] + Math.trunc((a[1] + b[1]) / 1e9), (a[1] + b[1]) % 1e9];
48
+ }
49
+
50
+ /**
51
+ * Get the current high-resolution time as an OpenTelemetry hrtime tuple.
52
+ *
53
+ * Uses the Performance API (timeOrigin + now()).
54
+ *
55
+ * @returns {[number, number]} The current hrtime tuple [s, ns].
56
+ */
57
+ function now() {
58
+ return add(fromMillis(performance.timeOrigin), fromMillis(performance.now()));
59
+ }
60
+
61
+ /**
62
+ * Check if a value is a valid OpenTelemetry hrtime tuple.
63
+ *
64
+ * An hrtime tuple is an Array of exactly two numbers:
65
+ * [seconds, nanoseconds]
66
+ *
67
+ * @param {*} value – anything to test
68
+ * @returns {boolean} true if `value` is a [number, number] array of length 2
69
+ *
70
+ * @example
71
+ * isHrTime([ 1, 500 ]); // true
72
+ * isHrTime([ 0, 1e9 ]); // true
73
+ * isHrTime([ '1', 500 ]); // false
74
+ * isHrTime({ 0: 1, 1: 500 }); // false
75
+ */
76
+ function isHrTime(value) {
77
+ return (
78
+ Array.isArray(value) &&
79
+ value.length === 2 &&
80
+ typeof value[0] === 'number' &&
81
+ typeof value[1] === 'number'
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Methods for handling hrtime. OpenTelemetry uses the [seconds, nanoseconds]
87
+ * format for hrtime in the `ReadableSpan` interface.
88
+ *
89
+ * @example
90
+ * import hrtime from '@tracing/hrtime.js';
91
+ *
92
+ * hrtime.fromMillis(1000);
93
+ * hrtime.toMillis([0, 1000]);
94
+ * hrtime.add([0, 0], [0, 1000]);
95
+ * hrtime.now();
96
+ * hrtime.isHrTime([0, 1000]);
97
+ */
98
+ export default { fromMillis, toMillis, toNanos, add, now, isHrTime };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generate a random hexadecimal ID of specified byte length
3
+ *
4
+ * @param {number} bytes - Number of bytes for the ID (default: 16)
5
+ * @returns {string} - Hexadecimal string representation
6
+ */
7
+ function gen(bytes = 16) {
8
+ let randomBytes = new Uint8Array(bytes);
9
+ crypto.getRandomValues(randomBytes);
10
+ let randHex = Array.from(randomBytes, (byte) =>
11
+ byte.toString(16).padStart(2, '0'),
12
+ ).join('');
13
+ return randHex;
14
+ }
15
+
16
+ /**
17
+ * Tracing id generation utils
18
+ *
19
+ * @example
20
+ * import id from './id.js';
21
+ *
22
+ * const spanId = id.gen(8); // => "a1b2c3d4e5f6..."
23
+ */
24
+ export default { gen };
@@ -0,0 +1,55 @@
1
+ import id from './id.js';
2
+
3
+ const SESSION_KEY = 'RollbarSession';
4
+
5
+ export class Session {
6
+ constructor(tracing, options) {
7
+ this.options = options;
8
+ this.tracing = tracing;
9
+ this.window = tracing.window;
10
+ this.session = null;
11
+ }
12
+
13
+ init() {
14
+ if (this.session) {
15
+ return this;
16
+ }
17
+ return this.getSession() || this.createSession();
18
+ }
19
+
20
+ getSession() {
21
+ try {
22
+ const serializedSession = this.window.sessionStorage.getItem(SESSION_KEY);
23
+
24
+ if (!serializedSession) {
25
+ return null;
26
+ }
27
+
28
+ this.session = JSON.parse(serializedSession);
29
+ } catch {
30
+ return null;
31
+ }
32
+ return this;
33
+ }
34
+
35
+ createSession() {
36
+ this.session = {
37
+ id: id.gen(),
38
+ createdAt: Date.now(),
39
+ };
40
+
41
+ return this.setSession(this.session);
42
+ }
43
+
44
+ setSession(session) {
45
+ const sessionString = JSON.stringify(session);
46
+
47
+ try {
48
+ this.window.sessionStorage.setItem(SESSION_KEY, sessionString);
49
+ } catch {
50
+ return null;
51
+ }
52
+ return this;
53
+ }
54
+ }
55
+
@@ -0,0 +1,92 @@
1
+ import hrtime from './hrtime.js';
2
+
3
+ export class Span {
4
+ constructor(options) {
5
+ this.initReadableSpan(options);
6
+
7
+ this.spanProcessor = options.spanProcessor;
8
+ this.spanProcessor.onStart(this, options.context);
9
+
10
+ if (options.attributes) {
11
+ this.setAttributes(options.attributes);
12
+ }
13
+ return this;
14
+ }
15
+
16
+ initReadableSpan(options) {
17
+ this.span = {
18
+ name: options.name,
19
+ kind: options.kind,
20
+ spanContext: options.spanContext,
21
+ parentSpanId: options.parentSpanId,
22
+ startTime: options.startTime || hrtime.now(),
23
+ endTime: [0, 0],
24
+ status: { code: 0, message: '' },
25
+ attributes: { 'session.id': options.session.id },
26
+ links: [],
27
+ events: [],
28
+ duration: 0,
29
+ ended: false,
30
+ resource: options.resource,
31
+ instrumentationScope: options.scope,
32
+ droppedAttributesCount: 0,
33
+ droppedEventsCount: 0,
34
+ droppedLinksCount: 0,
35
+ };
36
+ }
37
+
38
+ spanContext() {
39
+ return this.span.spanContext;
40
+ }
41
+
42
+ get spanId() {
43
+ return this.span.spanContext.spanId;
44
+ }
45
+
46
+ get traceId() {
47
+ return this.span.spanContext.traceId;
48
+ }
49
+
50
+ setAttribute(key, value) {
51
+ if (value == null || this.ended) return this;
52
+ if (key.length === 0) return this;
53
+
54
+ this.span.attributes[key] = value;
55
+ return this;
56
+ }
57
+
58
+ setAttributes(attributes) {
59
+ for (const [k, v] of Object.entries(attributes)) {
60
+ this.setAttribute(k, v);
61
+ }
62
+ return this;
63
+ }
64
+
65
+ addEvent(name, attributes = {}, time) {
66
+ if (this.span.ended) return this;
67
+
68
+ this.span.events.push({
69
+ name,
70
+ attributes,
71
+ time: time || hrtime.now(),
72
+ droppedAttributesCount: 0,
73
+ });
74
+
75
+ return this;
76
+ }
77
+
78
+ isRecording() {
79
+ return this.span.ended === false;
80
+ }
81
+
82
+ end(attributes, time) {
83
+ if (attributes) this.setAttributes(attributes);
84
+ this.span.endTime = time || hrtime.now();
85
+ this.span.ended = true;
86
+ this.spanProcessor.onEnd(this);
87
+ }
88
+
89
+ export() {
90
+ return this.span;
91
+ }
92
+ }
@@ -0,0 +1,15 @@
1
+ export class SpanProcessor {
2
+ constructor(exporter) {
3
+ this.exporter = exporter;
4
+ this.pendingSpans = new Map()
5
+ }
6
+
7
+ onStart(span, _parentContext) {
8
+ this.pendingSpans.set(span.span.spanContext.spanId, span);
9
+ }
10
+
11
+ onEnd(span) {
12
+ this.exporter.export([span.export()])
13
+ this.pendingSpans.delete(span.span.spanContext.spanId);
14
+ }
15
+ }