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.
- package/.claude/settings.local.json +3 -0
- package/.cursor/rules/guidelines.mdc +154 -0
- package/.github/workflows/ci.yml +4 -6
- package/CLAUDE.local.md +297 -0
- package/CLAUDE.md +201 -0
- package/CLAUDE.testrunner.md +470 -0
- package/Gruntfile.js +59 -16
- package/Makefile +3 -3
- package/SECURITY.md +5 -0
- package/babel.config.json +9 -0
- package/codex.md +148 -0
- package/dist/plugins/jquery.min.js +1 -1
- package/dist/rollbar.js +19332 -6596
- package/dist/rollbar.js.map +1 -1
- package/dist/rollbar.min.js +2 -1
- package/dist/rollbar.min.js.LICENSE.txt +1 -0
- package/dist/rollbar.min.js.map +1 -1
- package/dist/rollbar.named-amd.js +19332 -6596
- package/dist/rollbar.named-amd.js.map +1 -1
- package/dist/rollbar.named-amd.min.js +2 -1
- package/dist/rollbar.named-amd.min.js.LICENSE.txt +1 -0
- package/dist/rollbar.named-amd.min.js.map +1 -1
- package/dist/rollbar.noconflict.umd.js +19319 -6581
- package/dist/rollbar.noconflict.umd.js.map +1 -1
- package/dist/rollbar.noconflict.umd.min.js +2 -1
- package/dist/rollbar.noconflict.umd.min.js.LICENSE.txt +1 -0
- package/dist/rollbar.noconflict.umd.min.js.map +1 -1
- package/dist/rollbar.snippet.js +1 -1
- package/dist/rollbar.umd.js +19333 -6597
- package/dist/rollbar.umd.js.map +1 -1
- package/dist/rollbar.umd.min.js +2 -1
- package/dist/rollbar.umd.min.js.LICENSE.txt +1 -0
- package/dist/rollbar.umd.min.js.map +1 -1
- package/eslint.config.mjs +33 -0
- package/karma.conf.js +5 -14
- package/package.json +19 -20
- package/src/api.js +57 -4
- package/src/apiUtility.js +2 -3
- package/src/browser/core.js +37 -9
- package/src/browser/replay/defaults.js +70 -0
- package/src/browser/replay/recorder.js +194 -0
- package/src/browser/replay/replayMap.js +195 -0
- package/src/browser/rollbar.js +11 -7
- package/src/browser/telemetry.js +3 -3
- package/src/browser/transport/fetch.js +17 -4
- package/src/browser/transport/xhr.js +17 -1
- package/src/browser/transport.js +11 -8
- package/src/defaults.js +1 -1
- package/src/queue.js +65 -4
- package/src/react-native/rollbar.js +1 -1
- package/src/rollbar.js +52 -10
- package/src/server/rollbar.js +3 -2
- package/src/telemetry.js +76 -11
- package/src/tracing/context.js +24 -0
- package/src/tracing/contextManager.js +37 -0
- package/src/tracing/defaults.js +7 -0
- package/src/tracing/exporter.js +188 -0
- package/src/tracing/hrtime.js +98 -0
- package/src/tracing/id.js +24 -0
- package/src/tracing/session.js +55 -0
- package/src/tracing/span.js +92 -0
- package/src/tracing/spanProcessor.js +15 -0
- package/src/tracing/tracer.js +46 -0
- package/src/tracing/tracing.js +89 -0
- package/src/utility.js +34 -0
- package/test/api.test.js +57 -12
- package/test/apiUtility.test.js +5 -6
- package/test/browser.core.test.js +1 -10
- package/test/browser.domUtility.test.js +1 -1
- package/test/browser.predicates.test.js +1 -1
- package/test/browser.replay.recorder.test.js +430 -0
- package/test/browser.rollbar.test.js +58 -12
- package/test/browser.telemetry.test.js +1 -1
- package/test/browser.transforms.test.js +20 -13
- package/test/browser.transport.test.js +5 -4
- package/test/browser.url.test.js +1 -1
- package/test/fixtures/replay/index.js +20 -0
- package/test/fixtures/replay/payloads.fixtures.js +229 -0
- package/test/fixtures/replay/rrwebEvents.fixtures.js +251 -0
- package/test/fixtures/replay/rrwebSyntheticEvents.fixtures.js +328 -0
- package/test/notifier.test.js +1 -1
- package/test/predicates.test.js +1 -1
- package/test/queue.test.js +1 -1
- package/test/rateLimiter.test.js +1 -1
- package/test/react-native.rollbar.test.js +1 -1
- package/test/react-native.transforms.test.js +2 -2
- package/test/react-native.transport.test.js +3 -3
- package/test/replay/index.js +2 -0
- package/test/replay/integration/api.spans.test.js +136 -0
- package/test/replay/integration/e2e.test.js +228 -0
- package/test/replay/integration/index.js +9 -0
- package/test/replay/integration/queue.replayMap.test.js +332 -0
- package/test/replay/integration/replayMap.test.js +163 -0
- package/test/replay/integration/sessionRecording.test.js +390 -0
- package/test/replay/unit/api.postSpans.test.js +150 -0
- package/test/replay/unit/index.js +7 -0
- package/test/replay/unit/queue.replayMap.test.js +225 -0
- package/test/replay/unit/replayMap.test.js +348 -0
- package/test/replay/util/index.js +5 -0
- package/test/replay/util/mockRecordFn.js +80 -0
- package/test/server.lambda.mocha.test.mjs +172 -0
- package/test/server.locals.constructor.mocha.test.mjs +80 -0
- package/test/server.locals.error-handling.mocha.test.mjs +387 -0
- package/test/server.locals.merge.mocha.test.mjs +267 -0
- package/test/server.locals.test-utils.mjs +114 -0
- package/test/server.parser.mocha.test.mjs +87 -0
- package/test/server.predicates.mocha.test.mjs +63 -0
- package/test/server.rollbar.constructor.mocha.test.mjs +199 -0
- package/test/server.rollbar.handlers.mocha.test.mjs +253 -0
- package/test/server.rollbar.logging.mocha.test.mjs +326 -0
- package/test/server.rollbar.misc.mocha.test.mjs +44 -0
- package/test/server.rollbar.test-utils.mjs +57 -0
- package/test/server.telemetry.mocha.test.mjs +377 -0
- package/test/server.transforms.data.mocha.test.mjs +163 -0
- package/test/server.transforms.error.mocha.test.mjs +199 -0
- package/test/server.transforms.request.mocha.test.mjs +208 -0
- package/test/server.transforms.scrub.mocha.test.mjs +140 -0
- package/test/server.transforms.sourcemaps.mocha.test.mjs +122 -0
- package/test/server.transforms.test-utils.mjs +62 -0
- package/test/server.transport.mocha.test.mjs +269 -0
- package/test/telemetry.test.js +132 -1
- package/test/tracing/contextManager.test.js +28 -0
- package/test/tracing/exporter.toPayload.test.js +400 -0
- package/test/tracing/id.test.js +24 -0
- package/test/tracing/span.test.js +183 -0
- package/test/tracing/spanProcessor.test.js +73 -0
- package/test/tracing/tracing.test.js +105 -0
- package/test/transforms.test.js +2 -2
- package/test/truncation.test.js +2 -2
- package/test/utility.test.js +44 -6
- package/webpack.config.js +6 -44
- package/.eslintignore +0 -7
- package/test/server.lambda.test.js +0 -194
- package/test/server.locals.test.js +0 -1068
- package/test/server.parser.test.js +0 -78
- package/test/server.predicates.test.js +0 -91
- package/test/server.rollbar.test.js +0 -728
- package/test/server.telemetry.test.js +0 -443
- package/test/server.transforms.test.js +0 -1193
- 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,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
|
+
}
|