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
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Unit tests for Queue's integration with ReplayMap
3
+ */
4
+
5
+ /* globals describe */
6
+ /* globals it */
7
+ /* globals beforeEach */
8
+ /* globals afterEach */
9
+ /* globals sinon */
10
+
11
+ import { expect } from 'chai';
12
+ import sinon from 'sinon';
13
+
14
+ // We need to use require since the Queue module is CommonJS
15
+ const Queue = require('../../../src/queue');
16
+
17
+ class MockReplayMap {
18
+ constructor() {
19
+ this.add = sinon.stub().returnsArg(0);
20
+ this.send = sinon.stub().resolves(true);
21
+ this.discard = sinon.stub().returns(true);
22
+ }
23
+ }
24
+
25
+ class MockApi {
26
+ constructor() {
27
+ this.postItem = sinon.stub().callsFake((item, callback) => {
28
+ callback(null, { err: 0 });
29
+ });
30
+ }
31
+ }
32
+
33
+ class MockRateLimiter {
34
+ constructor() {
35
+ this.shouldSend = sinon.stub().returns({ shouldSend: true });
36
+ }
37
+ }
38
+
39
+ describe('Queue with ReplayMap', function () {
40
+ let queue;
41
+ let replayMap;
42
+ let api;
43
+ let rateLimiter;
44
+ let logger;
45
+
46
+ beforeEach(function () {
47
+ replayMap = new MockReplayMap();
48
+ api = new MockApi();
49
+ rateLimiter = new MockRateLimiter();
50
+ logger = { error: sinon.stub(), log: sinon.stub() };
51
+
52
+ queue = new Queue(rateLimiter, api, logger, { transmit: true }, replayMap);
53
+ });
54
+
55
+ afterEach(function () {
56
+ sinon.restore();
57
+ });
58
+
59
+ describe('addItem', function () {
60
+ it('should add replayId to the item when replayMap is available', function () {
61
+ const item = {
62
+ body: { message: 'test error' },
63
+ attributes: [
64
+ { key: 'replay_id', value: '1234567812345678' },
65
+ ],
66
+ };
67
+ const callback = sinon.stub();
68
+
69
+ queue.addItem(item, callback);
70
+
71
+ expect(replayMap.add.called).to.be.true;
72
+ expect(item.replayId).to.equal('1234567812345678');
73
+ });
74
+
75
+ it('should not add replayId when item has no body', function () {
76
+ const item = {
77
+ noBody: true,
78
+ attributes: [
79
+ { key: 'replay_id', value: '1234567812345678' },
80
+ ],
81
+ };
82
+ const callback = sinon.stub();
83
+
84
+ queue.addItem(item, callback);
85
+
86
+ expect(replayMap.add.called).to.be.false;
87
+ expect(item.replayId).to.be.undefined;
88
+ });
89
+
90
+ it('should not add replayId when replayMap is not available', function () {
91
+ queue = new Queue(rateLimiter, api, logger, { transmit: true });
92
+
93
+ const item = {
94
+ body: { message: 'test error' },
95
+ attributes: [
96
+ { key: 'replay_id', value: '1234567812345678' },
97
+ ],
98
+ };
99
+ const callback = sinon.stub();
100
+
101
+ queue.addItem(item, callback);
102
+
103
+ expect(item.replayId).to.be.undefined;
104
+ });
105
+ });
106
+
107
+ describe('_handleReplayResponse', function () {
108
+ it('should send the replay when response is successful', async function () {
109
+ const replayId = 'test-replay-id';
110
+ const response = { err: 0 };
111
+ const headers = { 'Rollbar-Replay-Enabled': 'true' };
112
+
113
+ await queue._handleReplayResponse(replayId, response, headers);
114
+
115
+ expect(replayMap.send.calledWith(replayId)).to.be.true;
116
+ expect(replayMap.discard.called).to.be.false;
117
+ });
118
+
119
+ it('should discard the replay when response has an error', async function () {
120
+ const replayId = 'test-replay-id';
121
+ const response = { err: 1 };
122
+ const headers = { 'Rollbar-Replay-Enabled': 'true' };
123
+
124
+ await queue._handleReplayResponse(replayId, response, headers);
125
+
126
+ expect(replayMap.send.called).to.be.false;
127
+ expect(replayMap.discard.calledWith(replayId)).to.be.true;
128
+ });
129
+
130
+ it('should log a warning when replayMap is not available', async function () {
131
+ queue = new Queue(rateLimiter, api, logger, { transmit: true });
132
+
133
+ const consoleSpy = sinon.spy(console, 'warn');
134
+
135
+ await queue._handleReplayResponse('test-replay-id', { err: 0 });
136
+
137
+ expect(consoleSpy.called).to.be.true;
138
+ expect(consoleSpy.args[0][0]).to.include('ReplayMap not available');
139
+ });
140
+
141
+ it('should log a warning when replayId is missing', async function () {
142
+ const consoleSpy = sinon.spy(console, 'warn');
143
+
144
+ await queue._handleReplayResponse(null, { err: 0 });
145
+
146
+ expect(consoleSpy.called).to.be.true;
147
+ expect(consoleSpy.args[0][0]).to.include('No replayId provided');
148
+ });
149
+
150
+ it('should handle errors during send/discard', async function () {
151
+ const replayId = 'test-replay-id';
152
+ const response = { err: 0 };
153
+ const headers = { 'Rollbar-Replay-Enabled': 'true' };
154
+
155
+ replayMap.send.rejects(new Error('Send error'));
156
+
157
+ const consoleSpy = sinon.spy(console, 'error');
158
+
159
+ await queue._handleReplayResponse(replayId, response, headers);
160
+
161
+ expect(consoleSpy.called).to.be.true;
162
+ expect(consoleSpy.args[0][0]).to.include(
163
+ 'Error handling replay response',
164
+ );
165
+ });
166
+ });
167
+
168
+ describe('API callback handling', function () {
169
+ it('should call _handleReplayResponse with replayId and response on success', function () {
170
+ const handleStub = sinon.stub(queue, '_handleReplayResponse');
171
+
172
+ const item = {
173
+ body: { message: 'test error' },
174
+ attributes: [
175
+ { key: 'replay_id', value: '1234567812345678' },
176
+ ],
177
+ };
178
+ const callback = sinon.stub();
179
+
180
+ queue.addItem(item, callback);
181
+
182
+ expect(handleStub.called).to.be.true;
183
+ expect(handleStub.firstCall.args[0]).to.equal('1234567812345678');
184
+ expect(handleStub.firstCall.args[1]).to.deep.equal({ err: 0 });
185
+ });
186
+
187
+ it('should not call _handleReplayResponse when there is an API error', function () {
188
+ api.postItem.callsFake((item, callback) => {
189
+ callback(new Error('API error'));
190
+ });
191
+
192
+ const handleStub = sinon.stub(queue, '_handleReplayResponse');
193
+
194
+ const item = {
195
+ body: { message: 'test error' },
196
+ attributes: [
197
+ { key: 'replay_id', value: '1234567812345678' },
198
+ ],
199
+ };
200
+ const callback = sinon.stub();
201
+
202
+ queue.addItem(item, callback);
203
+
204
+ expect(handleStub.called).to.be.false;
205
+ });
206
+
207
+ it('should not call _handleReplayResponse when item has no replayId', function () {
208
+ const handleStub = sinon.stub(queue, '_handleReplayResponse');
209
+
210
+ queue = new Queue(rateLimiter, api, logger, { transmit: true });
211
+
212
+ const item = {
213
+ body: { message: 'test error' },
214
+ attributes: [
215
+ { key: 'replay_id', value: '1234567812345678' },
216
+ ],
217
+ };
218
+ const callback = sinon.stub();
219
+
220
+ queue.addItem(item, callback);
221
+
222
+ expect(handleStub.called).to.be.false;
223
+ });
224
+ });
225
+ });
@@ -0,0 +1,348 @@
1
+ /**
2
+ * Unit tests for the ReplayMap module
3
+ */
4
+
5
+ /* globals describe */
6
+ /* globals it */
7
+ /* globals beforeEach */
8
+ /* globals afterEach */
9
+ /* globals sinon */
10
+
11
+ import { expect } from 'chai';
12
+ import sinon from 'sinon';
13
+ import ReplayMap from '../../../src/browser/replay/replayMap.js';
14
+ import id from '../../../src/tracing/id.js';
15
+
16
+ class MockRecorder {
17
+ constructor(returnPayload = true) {
18
+ this.payload = returnPayload ? [{ id: 'span1' }, { id: 'span2' }] : null;
19
+ this.dump = sinon.stub().returns(this.payload);
20
+ }
21
+ }
22
+
23
+ class MockApi {
24
+ constructor() {
25
+ this.postSpans = sinon.stub();
26
+ this.postSpans.resolves({ success: true });
27
+ }
28
+ }
29
+
30
+ class MockTracing {
31
+ constructor() {
32
+ this.exporter = {
33
+ toPayload: sinon.stub().returns([{ id: 'span1' }, { id: 'span2' }]),
34
+ };
35
+ }
36
+ }
37
+
38
+ describe('ReplayMap', function () {
39
+ let replayMap;
40
+ let mockRecorder;
41
+ let mockApi;
42
+ let mockTracing;
43
+
44
+ beforeEach(function () {
45
+ sinon.stub(id, 'gen').returns('1234567890abcdef');
46
+
47
+ mockRecorder = new MockRecorder();
48
+ mockApi = new MockApi();
49
+ mockTracing = new MockTracing();
50
+
51
+ replayMap = new ReplayMap({
52
+ recorder: mockRecorder,
53
+ api: mockApi,
54
+ tracing: mockTracing,
55
+ });
56
+ });
57
+
58
+ afterEach(function () {
59
+ sinon.restore();
60
+ });
61
+
62
+ describe('constructor', function () {
63
+ it('should throw when required dependencies are missing', function () {
64
+ expect(() => new ReplayMap({})).to.throw(TypeError);
65
+ expect(() => new ReplayMap({ recorder: mockRecorder })).to.throw(
66
+ TypeError,
67
+ );
68
+ expect(
69
+ () =>
70
+ new ReplayMap({
71
+ recorder: mockRecorder,
72
+ api: mockApi,
73
+ }),
74
+ ).to.throw(TypeError);
75
+
76
+ expect(
77
+ () =>
78
+ new ReplayMap({
79
+ recorder: mockRecorder,
80
+ api: mockApi,
81
+ tracing: mockTracing,
82
+ }),
83
+ ).to.not.throw();
84
+ });
85
+ });
86
+
87
+ describe('_processReplay', function () {
88
+ it('should dump recording and add payload to the map', async function () {
89
+ const replayId = '1234567890abcdef';
90
+
91
+ const expectedPayload = [{ id: 'span1' }, { id: 'span2' }];
92
+
93
+ const result = await replayMap._processReplay(replayId);
94
+
95
+ expect(result).to.equal(replayId);
96
+ expect(mockRecorder.dump.called).to.be.true;
97
+ expect(mockRecorder.dump.calledWith(mockTracing, replayId)).to.be.true;
98
+
99
+ expect(replayMap.size).to.equal(1);
100
+
101
+ const retrievedPayload = replayMap.getSpans(replayId);
102
+ expect(retrievedPayload).to.deep.equal(expectedPayload);
103
+ });
104
+
105
+ it('should store null in map when dump returns null', async function () {
106
+ mockRecorder = new MockRecorder(false);
107
+ replayMap = new ReplayMap({
108
+ recorder: mockRecorder,
109
+ api: mockApi,
110
+ tracing: mockTracing,
111
+ });
112
+
113
+ const replayId = '1234567890abcdef';
114
+ const result = await replayMap._processReplay(replayId);
115
+
116
+ expect(result).to.equal(replayId);
117
+ expect(mockRecorder.dump.called).to.be.true;
118
+ expect(mockRecorder.dump.calledWith(mockTracing, replayId)).to.be.true;
119
+
120
+ expect(replayMap.size).to.equal(1);
121
+ expect(replayMap.getSpans(replayId)).to.be.null;
122
+ });
123
+
124
+ it('should handle errors gracefully', async function () {
125
+ mockRecorder.dump.throws(new Error('Test error'));
126
+
127
+ const consoleSpy = sinon.spy(console, 'error');
128
+ const replayId = '1234567890abcdef';
129
+ const result = await replayMap._processReplay(replayId);
130
+
131
+ expect(result).to.equal(replayId);
132
+ expect(mockRecorder.dump.called).to.be.true;
133
+ expect(consoleSpy.called).to.be.true;
134
+ expect(consoleSpy.args[0][1]).to.include('Error transforming spans');
135
+
136
+ expect(replayMap.size).to.equal(1);
137
+ expect(replayMap.getSpans(replayId)).to.be.null;
138
+ });
139
+ });
140
+
141
+ describe('add', function () {
142
+ it('should use provided replayId and initiate async processing', function () {
143
+ const replayId = '1122334455667788';
144
+ const uuid = '12345678-1234-5678-1234-1234567890ab';
145
+ const processStub = sinon
146
+ .stub(replayMap, '_processReplay')
147
+ .resolves('1234567890abcdef');
148
+
149
+ const resp = replayMap.add(replayId, uuid);
150
+
151
+ expect(resp).to.equal(replayId);
152
+ expect(id.gen.calledWith(8)).to.be.false;
153
+ expect(processStub.calledWith(replayId, uuid)).to.be.true;
154
+ });
155
+
156
+ it('should generate a replayId and initiate async processing', function () {
157
+ const processStub = sinon
158
+ .stub(replayMap, '_processReplay')
159
+ .resolves('1234567890abcdef');
160
+
161
+ const replayId = replayMap.add();
162
+
163
+ expect(replayId).to.equal('1234567890abcdef');
164
+ expect(id.gen.calledWith(8)).to.be.true;
165
+ expect(processStub.calledWith(replayId)).to.be.true;
166
+ });
167
+
168
+ it('should handle errors from _processReplay', function (done) {
169
+ sinon.stub(replayMap, '_processReplay').rejects(new Error('Test error'));
170
+
171
+ const errorSpy = sinon.spy(console, 'error');
172
+
173
+ try {
174
+ replayMap.add();
175
+
176
+ setTimeout(() => {
177
+ expect(errorSpy.called).to.be.true;
178
+ expect(errorSpy.args[0][1]).to.include('Failed to process replay');
179
+ done();
180
+ }, 0);
181
+ } catch (error) {
182
+ done(error);
183
+ }
184
+ });
185
+ });
186
+
187
+ describe('send', function () {
188
+ it('should send payload and remove it from the map', async function () {
189
+ const mockPayload = [{ id: 'payload1' }, { id: 'payload2' }];
190
+ replayMap.setSpans('testReplayId', mockPayload);
191
+ expect(replayMap.size).to.equal(1);
192
+
193
+ const result = await replayMap.send('testReplayId');
194
+
195
+ expect(result).to.be.true;
196
+ expect(mockApi.postSpans.called).to.be.true;
197
+ expect(mockApi.postSpans.firstCall.args[0]).to.deep.equal(mockPayload);
198
+
199
+ expect(replayMap.size).to.equal(0);
200
+ });
201
+
202
+ it('should handle missing replayId parameter', async function () {
203
+ const consoleSpy = sinon.spy(console, 'error');
204
+
205
+ const result = await replayMap.send();
206
+
207
+ expect(result).to.be.false;
208
+ expect(consoleSpy.called).to.be.true;
209
+ expect(consoleSpy.args[0][1]).to.include(
210
+ 'ReplayMap.send: No replayId provided',
211
+ );
212
+ expect(mockApi.postSpans.called).to.be.false;
213
+ });
214
+
215
+ it('should handle non-existent replayId', async function () {
216
+ const consoleSpy = sinon.spy(console, 'error');
217
+
218
+ const result = await replayMap.send('nonexistent');
219
+
220
+ expect(result).to.be.false;
221
+ expect(consoleSpy.called).to.be.true;
222
+ expect(consoleSpy.args[0][1]).to.include('No replay found for replayId');
223
+ expect(mockApi.postSpans.called).to.be.false;
224
+ });
225
+
226
+ it('should handle empty array payload', async function () {
227
+ replayMap.setSpans('emptyReplayId', []);
228
+
229
+ const consoleSpy = sinon.spy(console, 'error');
230
+ const result = await replayMap.send('emptyReplayId');
231
+
232
+ expect(result).to.be.false;
233
+ expect(consoleSpy.called).to.be.true;
234
+ expect(consoleSpy.args[0][1]).to.include('No payload found for replayId');
235
+ expect(mockApi.postSpans.called).to.be.false;
236
+ });
237
+
238
+ it('should handle empty OTLP payload', async function () {
239
+ replayMap.setSpans('emptyOTLPReplayId', { resourceSpans: [] });
240
+
241
+ const consoleSpy = sinon.spy(console, 'error');
242
+ const result = await replayMap.send('emptyOTLPReplayId');
243
+
244
+ expect(result).to.be.false;
245
+ expect(consoleSpy.called).to.be.true;
246
+ expect(consoleSpy.args[0][1]).to.include('No payload found for replayId');
247
+ expect(mockApi.postSpans.called).to.be.false;
248
+ });
249
+
250
+ it('should handle API errors during sending', async function () {
251
+ replayMap.setSpans('errorReplayId', [{ id: 'span1' }]);
252
+
253
+ mockApi.postSpans.rejects(new Error('API error'));
254
+
255
+ const consoleSpy = sinon.spy(console, 'error');
256
+ const result = await replayMap.send('errorReplayId');
257
+
258
+ expect(result).to.be.false;
259
+ expect(consoleSpy.called).to.be.true;
260
+ expect(consoleSpy.args[0][1]).to.include('Error sending replay');
261
+ expect(mockApi.postSpans.called).to.be.true;
262
+
263
+ expect(replayMap.size).to.equal(0);
264
+ });
265
+ });
266
+
267
+ describe('discard', function () {
268
+ it('should remove the replay from the map without sending', function () {
269
+ replayMap.setSpans('discardReplayId', [{ id: 'span1' }]);
270
+ expect(replayMap.size).to.equal(1);
271
+
272
+ const result = replayMap.discard('discardReplayId');
273
+
274
+ expect(result).to.be.true;
275
+ expect(mockApi.postSpans.called).to.be.false;
276
+ expect(replayMap.size).to.equal(0);
277
+ });
278
+
279
+ it('should handle missing replayId parameter', function () {
280
+ const consoleSpy = sinon.spy(console, 'error');
281
+
282
+ const result = replayMap.discard();
283
+
284
+ expect(result).to.be.false;
285
+ expect(consoleSpy.called).to.be.true;
286
+ expect(consoleSpy.args[0][1]).to.include(
287
+ 'ReplayMap.discard: No replayId provided',
288
+ );
289
+ });
290
+
291
+ it('should handle non-existent replayId', function () {
292
+ const consoleSpy = sinon.spy(console, 'error');
293
+
294
+ const result = replayMap.discard('nonexistent');
295
+
296
+ expect(result).to.be.false;
297
+ expect(consoleSpy.called).to.be.true;
298
+ expect(consoleSpy.args[0][1]).to.include('No replay found for replayId');
299
+ });
300
+ });
301
+
302
+ describe('getSpans and setSpans', function () {
303
+ it('should get spans correctly when they exist', function () {
304
+ const testPayload = [{ id: 'testSpan1' }, { id: 'testSpan2' }];
305
+ replayMap.setSpans('testReplayId', testPayload);
306
+
307
+ const result = replayMap.getSpans('testReplayId');
308
+ expect(result).to.deep.equal(testPayload);
309
+ });
310
+
311
+ it('should return null when getting spans for non-existent replayId', function () {
312
+ const result = replayMap.getSpans('nonExistentId');
313
+ expect(result).to.be.null;
314
+ });
315
+
316
+ it('should allow overwriting existing spans', function () {
317
+ const initialPayload = [{ id: 'initialSpan' }];
318
+ const updatedPayload = [{ id: 'updatedSpan' }];
319
+
320
+ replayMap.setSpans('replayId', initialPayload);
321
+ expect(replayMap.getSpans('replayId')).to.deep.equal(initialPayload);
322
+
323
+ replayMap.setSpans('replayId', updatedPayload);
324
+ expect(replayMap.getSpans('replayId')).to.deep.equal(updatedPayload);
325
+ });
326
+ });
327
+
328
+ describe('size and clear', function () {
329
+ it('should report correct size of the map', function () {
330
+ expect(replayMap.size).to.equal(0);
331
+
332
+ replayMap.setSpans('id1', [{ id: 'span1' }]);
333
+ expect(replayMap.size).to.equal(1);
334
+
335
+ replayMap.setSpans('id2', [{ id: 'span2' }]);
336
+ expect(replayMap.size).to.equal(2);
337
+ });
338
+
339
+ it('should clear all entries from the map', function () {
340
+ replayMap.setSpans('id1', [{ id: 'span1' }]);
341
+ replayMap.setSpans('id2', [{ id: 'span2' }]);
342
+ expect(replayMap.size).to.equal(2);
343
+
344
+ replayMap.clear();
345
+ expect(replayMap.size).to.equal(0);
346
+ });
347
+ });
348
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Test utilities for Rollbar.js replay tests
3
+ */
4
+
5
+ export { default as mockRecordFn } from './mockRecordFn.js';
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Mock implementation of rrweb.record for testing
3
+ * Emits fixture events on a schedule to test the Recorder
4
+ */
5
+
6
+ import { allEvents } from '../../fixtures/replay/index.js';
7
+
8
+ /**
9
+ * Mock implementation of rrweb's record function
10
+ *
11
+ * @param {Object} options Configuration options
12
+ * @param {Function} options.emit Function that will receive events
13
+ * @param {number} options.checkoutEveryNms Milliseconds between checkpoints
14
+ * @param {number} options.emitEveryNms Milliseconds between events
15
+ * @returns {Function} Stop function
16
+ */
17
+ export default function mockRecordFn(options = {}) {
18
+ function* cycling(xs) {
19
+ let i = 0;
20
+ while (true) {
21
+ yield xs[i++ % xs.length];
22
+ }
23
+ }
24
+
25
+ const systemEvents = ['domContentLoaded', 'load', 'meta', 'fullSnapshot'];
26
+ const events = cycling(
27
+ Object.entries(allEvents)
28
+ .filter(([eventName]) => !systemEvents.includes(eventName))
29
+ .map(([_, event]) => event),
30
+ );
31
+
32
+ const emit = options.emit;
33
+
34
+ let lastCheckoutTime = Date.now();
35
+ let intervalId = null;
36
+ let stopping = false;
37
+ let initialSnapshotDone = false;
38
+
39
+ const emitNextEvent = () => {
40
+ if (stopping) return;
41
+
42
+ // Check if we need to do a checkout
43
+ const now = Date.now();
44
+
45
+ if (
46
+ initialSnapshotDone &&
47
+ options.checkoutEveryNms &&
48
+ now - lastCheckoutTime >= options.checkoutEveryNms
49
+ ) {
50
+ lastCheckoutTime = now;
51
+
52
+ // checkout:
53
+ // rrweb sends both Meta and FullSnapshot events in the same tick
54
+ // with isCheckout = true
55
+ emit({ ...allEvents.meta }, true);
56
+ emit({ ...allEvents.fullSnapshot }, true);
57
+ }
58
+
59
+ emit(events.next().value, false);
60
+ };
61
+
62
+ // Start emitting events on a regular interval
63
+ // 1 event every 100ms is a reasonable pace for testing
64
+ intervalId = setInterval(emitNextEvent, options.emitEveryNms);
65
+
66
+ // Initial events: domContentLoaded, load, meta, fullSnapshot
67
+ // Based on rrweb's implementation, the initial snapshot is NOT a checkout
68
+ emit(allEvents.domContentLoaded, false);
69
+ emit(allEvents.load, false);
70
+ emit(allEvents.meta, false);
71
+ emit(allEvents.fullSnapshot, false);
72
+ initialSnapshotDone = true;
73
+
74
+ // Return a stop function that cleans up the intervals
75
+ return () => {
76
+ stopping = true;
77
+ clearInterval(intervalId);
78
+ intervalId = null;
79
+ };
80
+ }