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,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for ReplayMap with API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* globals describe */
|
|
6
|
+
/* globals it */
|
|
7
|
+
/* globals beforeEach */
|
|
8
|
+
/* globals afterEach */
|
|
9
|
+
|
|
10
|
+
import { expect } from 'chai';
|
|
11
|
+
import sinon from 'sinon';
|
|
12
|
+
|
|
13
|
+
import ReplayMap from '../../../src/browser/replay/replayMap.js';
|
|
14
|
+
import Recorder from '../../../src/browser/replay/recorder.js';
|
|
15
|
+
import Tracing from '../../../src/tracing/tracing.js';
|
|
16
|
+
import Api from '../../../src/api.js';
|
|
17
|
+
import { mockRecordFn } from '../util';
|
|
18
|
+
|
|
19
|
+
const options = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
recorder: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
emitEveryNms: 100,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe('ReplayMap API Integration', function () {
|
|
28
|
+
let tracing;
|
|
29
|
+
let recorder;
|
|
30
|
+
let api;
|
|
31
|
+
let transport;
|
|
32
|
+
let replayMap;
|
|
33
|
+
|
|
34
|
+
beforeEach(function () {
|
|
35
|
+
transport = {
|
|
36
|
+
post: sinon
|
|
37
|
+
.stub()
|
|
38
|
+
.callsFake((accessToken, transportOptions, payload, callback) => {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
callback(
|
|
41
|
+
null,
|
|
42
|
+
{ err: 0, result: { id: '12345' } },
|
|
43
|
+
{ 'Rollbar-Replay-Enabled': 'true' }
|
|
44
|
+
);
|
|
45
|
+
}, 10);
|
|
46
|
+
}),
|
|
47
|
+
postJsonPayload: sinon.stub(),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const urlMock = { parse: sinon.stub().returns({}) };
|
|
51
|
+
const truncationMock = {
|
|
52
|
+
truncate: sinon.stub().returns({ error: null, value: '{}' }),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
tracing = new Tracing(window, options);
|
|
56
|
+
tracing.initSession();
|
|
57
|
+
|
|
58
|
+
const mockPayload = [{ id: 'span1', name: 'recording-span' }];
|
|
59
|
+
|
|
60
|
+
api = new Api(
|
|
61
|
+
{ accessToken: 'test-token' },
|
|
62
|
+
transport,
|
|
63
|
+
urlMock,
|
|
64
|
+
truncationMock,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
recorder = new Recorder(options.recorder, mockRecordFn);
|
|
68
|
+
sinon.stub(recorder, 'dump').returns(mockPayload);
|
|
69
|
+
|
|
70
|
+
replayMap = new ReplayMap({
|
|
71
|
+
recorder,
|
|
72
|
+
api,
|
|
73
|
+
tracing,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
recorder.start();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(function () {
|
|
80
|
+
if (recorder) {
|
|
81
|
+
recorder.stop();
|
|
82
|
+
}
|
|
83
|
+
sinon.restore();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should add replay data to map', async function () {
|
|
87
|
+
const uuid = 'test-uuid';
|
|
88
|
+
const replayId = replayMap.add(null, uuid);
|
|
89
|
+
expect(replayId).to.be.a('string');
|
|
90
|
+
expect(replayId.length).to.equal(16); // 8 bytes as hex = 16 characters
|
|
91
|
+
|
|
92
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
93
|
+
|
|
94
|
+
expect(recorder.dump.calledOnce).to.be.true;
|
|
95
|
+
expect(recorder.dump.calledWith(tracing, replayId, uuid)).to.be.true;
|
|
96
|
+
|
|
97
|
+
const payload = replayMap.getSpans(replayId);
|
|
98
|
+
expect(payload).to.not.be.null;
|
|
99
|
+
expect(payload).to.be.an('array');
|
|
100
|
+
expect(payload[0]).to.have.property('name', 'recording-span');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should successfully send replay to API', async function () {
|
|
104
|
+
const postSpansSpy = sinon.spy(api, 'postSpans');
|
|
105
|
+
|
|
106
|
+
const replayId = 'test-replay-id';
|
|
107
|
+
const mockPayload = [{ id: 'test-span', name: 'recording-span' }];
|
|
108
|
+
replayMap.setSpans(replayId, mockPayload);
|
|
109
|
+
|
|
110
|
+
const result = await replayMap.send(replayId);
|
|
111
|
+
|
|
112
|
+
expect(result).to.be.true;
|
|
113
|
+
expect(postSpansSpy.calledOnce).to.be.true;
|
|
114
|
+
expect(postSpansSpy.calledWith(mockPayload)).to.be.true;
|
|
115
|
+
|
|
116
|
+
expect(replayMap.getSpans(replayId)).to.be.null;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle API errors during send', async function () {
|
|
120
|
+
const apiError = new Error('API failure');
|
|
121
|
+
sinon.stub(api, 'postSpans').rejects(apiError);
|
|
122
|
+
|
|
123
|
+
const consoleSpy = sinon.spy(console, 'error');
|
|
124
|
+
|
|
125
|
+
const replayId = 'error-replay-id';
|
|
126
|
+
const mockPayload = [{ id: 'test-span', name: 'recording-span' }];
|
|
127
|
+
replayMap.setSpans(replayId, mockPayload);
|
|
128
|
+
|
|
129
|
+
const result = await replayMap.send(replayId);
|
|
130
|
+
|
|
131
|
+
expect(result).to.be.false;
|
|
132
|
+
expect(replayMap.getSpans(replayId)).to.be.null;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should discard replay without sending', function () {
|
|
136
|
+
const postSpansSpy = sinon.spy(api, 'postSpans');
|
|
137
|
+
|
|
138
|
+
const replayId = 'discard-replay-id';
|
|
139
|
+
const mockPayload = [{ id: 'test-span', name: 'recording-span' }];
|
|
140
|
+
replayMap.setSpans(replayId, mockPayload);
|
|
141
|
+
|
|
142
|
+
const result = replayMap.discard(replayId);
|
|
143
|
+
|
|
144
|
+
expect(result).to.be.true;
|
|
145
|
+
expect(postSpansSpy.called).to.be.false;
|
|
146
|
+
|
|
147
|
+
expect(replayMap.getSpans(replayId)).to.be.null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should generate unique replay IDs', function () {
|
|
151
|
+
const replayIds = new Set();
|
|
152
|
+
|
|
153
|
+
sinon.stub(replayMap, '_processReplay').resolves();
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < 100; i++) {
|
|
156
|
+
const replayId = replayMap.add();
|
|
157
|
+
expect(replayIds.has(replayId)).to.be.false;
|
|
158
|
+
replayIds.add(replayId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
expect(replayIds.size).to.equal(100);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the Recorder and Tracing interaction
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* globals describe */
|
|
6
|
+
/* globals it */
|
|
7
|
+
/* globals beforeEach */
|
|
8
|
+
/* globals afterEach */
|
|
9
|
+
|
|
10
|
+
import { expect } from 'chai';
|
|
11
|
+
import sinon from 'sinon';
|
|
12
|
+
|
|
13
|
+
import Tracing from '../../../src/tracing/tracing.js';
|
|
14
|
+
import { Context } from '../../../src/tracing/context.js';
|
|
15
|
+
import Recorder from '../../../src/browser/replay/recorder.js';
|
|
16
|
+
import ReplayMap from '../../../src/browser/replay/replayMap.js';
|
|
17
|
+
import recorderDefaults from '../../../src/browser/replay/defaults.js';
|
|
18
|
+
import { mockRecordFn } from '../util';
|
|
19
|
+
import Api from '../../../src/api.js';
|
|
20
|
+
import Queue from '../../../src/queue.js';
|
|
21
|
+
import { createPayloadWithReplayId } from '../../fixtures/replay';
|
|
22
|
+
|
|
23
|
+
const options = {
|
|
24
|
+
enabled: true,
|
|
25
|
+
resource: {
|
|
26
|
+
attributes: {
|
|
27
|
+
'service.name': 'unknown_service',
|
|
28
|
+
'telemetry.sdk.language': 'webjs',
|
|
29
|
+
'telemetry.sdk.name': 'rollbar',
|
|
30
|
+
'telemetry.sdk.version': '0.1.0',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
notifier: {
|
|
34
|
+
name: 'rollbar.js',
|
|
35
|
+
version: '0.1.0',
|
|
36
|
+
},
|
|
37
|
+
recorder: {
|
|
38
|
+
...recorderDefaults,
|
|
39
|
+
enabled: true,
|
|
40
|
+
autoStart: false,
|
|
41
|
+
emitEveryNms: 100, // non-rrweb, used by mockRecordFn
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createMockTransport = () => {
|
|
46
|
+
return {
|
|
47
|
+
post: sinon
|
|
48
|
+
.stub()
|
|
49
|
+
.callsFake((accessToken, transportOptions, payload, callback) => {
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
callback(
|
|
52
|
+
null,
|
|
53
|
+
{ err: 0, result: { id: '12345' } },
|
|
54
|
+
{ 'Rollbar-Replay-Enabled': 'true' }
|
|
55
|
+
);
|
|
56
|
+
}, 10);
|
|
57
|
+
}),
|
|
58
|
+
postJsonPayload: sinon.stub(),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getAttributeValue = (attributes, key) => {
|
|
63
|
+
const attr = attributes.find((attr) => attr.key === key);
|
|
64
|
+
return attr ? attr.value : null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe('Session Replay Integration', function () {
|
|
68
|
+
let tracing;
|
|
69
|
+
let recorder;
|
|
70
|
+
|
|
71
|
+
beforeEach(function () {
|
|
72
|
+
tracing = new Tracing(window, options);
|
|
73
|
+
tracing.initSession();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(function () {
|
|
77
|
+
if (recorder) {
|
|
78
|
+
recorder.stop();
|
|
79
|
+
}
|
|
80
|
+
sinon.restore();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('dumping recording should export tracing', function (done) {
|
|
84
|
+
recorder = new Recorder(options.recorder, mockRecordFn);
|
|
85
|
+
recorder.start();
|
|
86
|
+
|
|
87
|
+
const tracingContext = tracing.contextManager.active();
|
|
88
|
+
expect(tracingContext).to.be.instanceOf(Context);
|
|
89
|
+
|
|
90
|
+
const dumpRecording = () => {
|
|
91
|
+
const replayId = 'test-replay-id';
|
|
92
|
+
const uuid = 'test-uuid';
|
|
93
|
+
const payload = recorder.dump(tracing, replayId, uuid);
|
|
94
|
+
|
|
95
|
+
expect(payload).to.not.be.null;
|
|
96
|
+
expect(payload).to.be.an('object');
|
|
97
|
+
expect(payload).to.have.property('resourceSpans');
|
|
98
|
+
expect(payload.resourceSpans).to.be.an('array');
|
|
99
|
+
expect(payload.resourceSpans[0]).to.have.property('scopeSpans');
|
|
100
|
+
expect(payload.resourceSpans[0].scopeSpans[0]).to.have.property('spans');
|
|
101
|
+
|
|
102
|
+
const uuidValue = getAttributeValue(
|
|
103
|
+
payload.resourceSpans[0].scopeSpans[0].spans[0].attributes,
|
|
104
|
+
'rollbar.occurrence.uuid',
|
|
105
|
+
);
|
|
106
|
+
expect(uuidValue['stringValue']).to.equal(uuid);
|
|
107
|
+
|
|
108
|
+
const events = payload.resourceSpans[0].scopeSpans[0].spans[0].events;
|
|
109
|
+
expect(events.length).to.be.greaterThan(0);
|
|
110
|
+
expect(events.every((e) => e.name === 'rrweb-replay-events')).to.be.true;
|
|
111
|
+
|
|
112
|
+
const eventAttrs = events[0].attributes;
|
|
113
|
+
const eventTypeAttr = eventAttrs.find((attr) => attr.key === 'eventType');
|
|
114
|
+
const jsonAttr = eventAttrs.find((attr) => attr.key === 'json');
|
|
115
|
+
|
|
116
|
+
expect(eventTypeAttr).to.exist;
|
|
117
|
+
expect(jsonAttr).to.exist;
|
|
118
|
+
|
|
119
|
+
done();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
setTimeout(dumpRecording, 1000);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle checkouts correctly', function (done) {
|
|
126
|
+
recorder = new Recorder(
|
|
127
|
+
{
|
|
128
|
+
...options.recorder,
|
|
129
|
+
checkoutEveryNms: 250,
|
|
130
|
+
},
|
|
131
|
+
mockRecordFn,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
recorder.start();
|
|
135
|
+
|
|
136
|
+
const dumpRecording = () => {
|
|
137
|
+
const replayId = 'test-replay-id';
|
|
138
|
+
const payload = recorder.dump(tracing, replayId);
|
|
139
|
+
|
|
140
|
+
expect(payload).to.not.be.null;
|
|
141
|
+
expect(payload).to.be.an('object');
|
|
142
|
+
expect(payload).to.have.property('resourceSpans');
|
|
143
|
+
expect(payload.resourceSpans).to.be.an('array');
|
|
144
|
+
expect(payload.resourceSpans[0]).to.have.property('scopeSpans');
|
|
145
|
+
expect(payload.resourceSpans[0].scopeSpans[0]).to.have.property('spans');
|
|
146
|
+
|
|
147
|
+
const events = payload.resourceSpans[0].scopeSpans[0].spans[0].events;
|
|
148
|
+
expect(events).to.be.an('array');
|
|
149
|
+
expect(events.length).to.be.greaterThan(0);
|
|
150
|
+
|
|
151
|
+
done();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
setTimeout(dumpRecording, 1000);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle no checkouts correctly', function (done) {
|
|
158
|
+
recorder = new Recorder(
|
|
159
|
+
{
|
|
160
|
+
...options.recorder,
|
|
161
|
+
checkoutEveryNms: 500,
|
|
162
|
+
},
|
|
163
|
+
mockRecordFn,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
recorder.start();
|
|
167
|
+
|
|
168
|
+
const dumpRecording = () => {
|
|
169
|
+
const replayId = 'test-replay-id';
|
|
170
|
+
const payload = recorder.dump(tracing, replayId);
|
|
171
|
+
|
|
172
|
+
expect(payload).to.not.be.null;
|
|
173
|
+
expect(payload).to.be.an('object');
|
|
174
|
+
expect(payload).to.have.property('resourceSpans');
|
|
175
|
+
expect(payload.resourceSpans).to.be.an('array');
|
|
176
|
+
expect(payload.resourceSpans[0]).to.have.property('scopeSpans');
|
|
177
|
+
expect(payload.resourceSpans[0].scopeSpans[0]).to.have.property('spans');
|
|
178
|
+
|
|
179
|
+
const events = payload.resourceSpans[0].scopeSpans[0].spans[0].events;
|
|
180
|
+
expect(events).to.be.an('array');
|
|
181
|
+
expect(events.length).to.be.greaterThan(0);
|
|
182
|
+
|
|
183
|
+
done();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
setTimeout(dumpRecording, 250);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('Session Replay Transport Integration', function () {
|
|
191
|
+
let tracing;
|
|
192
|
+
let recorder;
|
|
193
|
+
let api;
|
|
194
|
+
let transport;
|
|
195
|
+
let replayMap;
|
|
196
|
+
let queue;
|
|
197
|
+
|
|
198
|
+
beforeEach(function () {
|
|
199
|
+
transport = createMockTransport();
|
|
200
|
+
const urlMock = { parse: sinon.stub().returns({}) };
|
|
201
|
+
const truncationMock = {
|
|
202
|
+
truncate: sinon.stub().returns({ error: null, value: '{}' }),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
tracing = new Tracing(window, options);
|
|
206
|
+
tracing.initSession();
|
|
207
|
+
|
|
208
|
+
api = new Api(
|
|
209
|
+
{ accessToken: 'test-token' },
|
|
210
|
+
transport,
|
|
211
|
+
urlMock,
|
|
212
|
+
truncationMock,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
recorder = new Recorder(options.recorder, mockRecordFn);
|
|
216
|
+
|
|
217
|
+
sinon.stub(recorder, 'dump').callsFake((tracing, replayId) => {
|
|
218
|
+
return createPayloadWithReplayId(replayId);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
replayMap = new ReplayMap({
|
|
222
|
+
recorder,
|
|
223
|
+
api,
|
|
224
|
+
tracing,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
queue = new Queue(
|
|
228
|
+
{ shouldSend: () => ({ shouldSend: true }) },
|
|
229
|
+
api,
|
|
230
|
+
console,
|
|
231
|
+
{ transmit: true, retryInterval: 500 },
|
|
232
|
+
replayMap,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
recorder.start();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
afterEach(function () {
|
|
239
|
+
if (recorder) {
|
|
240
|
+
recorder.stop();
|
|
241
|
+
}
|
|
242
|
+
sinon.restore();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should add replayId to error item and send replay on success', function (done) {
|
|
246
|
+
const addSpy = sinon.spy(replayMap, 'add');
|
|
247
|
+
const sendSpy = sinon.spy(replayMap, 'send');
|
|
248
|
+
const postSpansSpy = sinon.spy(api, 'postSpans');
|
|
249
|
+
|
|
250
|
+
const errorItem = {
|
|
251
|
+
body: {
|
|
252
|
+
trace: {
|
|
253
|
+
exception: {
|
|
254
|
+
message: 'Test error for session replay',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
attributes: [
|
|
259
|
+
{ key: 'replay_id', value: '1234567812345678' },
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
queue.addItem(errorItem, function (err, resp) {
|
|
264
|
+
expect(errorItem).to.have.property('replayId');
|
|
265
|
+
expect(addSpy.calledOnce).to.be.true;
|
|
266
|
+
|
|
267
|
+
expect(err).to.be.null;
|
|
268
|
+
expect(resp).to.have.property('err', 0);
|
|
269
|
+
|
|
270
|
+
setTimeout(function () {
|
|
271
|
+
expect(sendSpy.calledWith(errorItem.replayId)).to.be.true;
|
|
272
|
+
expect(postSpansSpy.calledOnce).to.be.true;
|
|
273
|
+
|
|
274
|
+
done();
|
|
275
|
+
}, 50);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should discard replay when API returns an error', function (done) {
|
|
280
|
+
transport.post = sinon
|
|
281
|
+
.stub()
|
|
282
|
+
.callsFake((accessToken, transportOptions, payload, callback) => {
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
callback(null, { err: 1, message: 'API Error' });
|
|
285
|
+
}, 10);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const addSpy = sinon.spy(replayMap, 'add');
|
|
289
|
+
const sendSpy = sinon.spy(replayMap, 'send');
|
|
290
|
+
const discardSpy = sinon.spy(replayMap, 'discard');
|
|
291
|
+
|
|
292
|
+
const errorItem = {
|
|
293
|
+
body: {
|
|
294
|
+
trace: {
|
|
295
|
+
exception: {
|
|
296
|
+
message: 'Test error with API failure',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
attributes: [
|
|
301
|
+
{ key: 'replay_id', value: '1234567812345678' },
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
queue.addItem(errorItem, function (err, resp) {
|
|
306
|
+
expect(errorItem).to.have.property('replayId');
|
|
307
|
+
expect(addSpy.calledOnce).to.be.true;
|
|
308
|
+
|
|
309
|
+
setTimeout(function () {
|
|
310
|
+
expect(discardSpy.calledWith(errorItem.replayId)).to.be.true;
|
|
311
|
+
expect(sendSpy.called).to.be.false;
|
|
312
|
+
|
|
313
|
+
done();
|
|
314
|
+
}, 50);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should handle full end-to-end flow from error to spans', async function () {
|
|
319
|
+
const addSpy = sinon.spy(replayMap, 'add');
|
|
320
|
+
const sendSpy = sinon.spy(replayMap, 'send');
|
|
321
|
+
const postSpansSpy = sinon.spy(api, 'postSpans');
|
|
322
|
+
|
|
323
|
+
const handleReplayResponseSpy = sinon.spy(queue, '_handleReplayResponse');
|
|
324
|
+
|
|
325
|
+
const errorItem = {
|
|
326
|
+
body: {
|
|
327
|
+
trace: {
|
|
328
|
+
exception: {
|
|
329
|
+
message: 'Test E2E flow',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
attributes: [
|
|
334
|
+
{ key: 'replay_id', value: '1234567812345678' },
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const addItemPromise = new Promise((resolve, reject) => {
|
|
339
|
+
queue.addItem(errorItem, (err, resp) => {
|
|
340
|
+
if (err) reject(err);
|
|
341
|
+
else resolve(resp);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await addItemPromise;
|
|
346
|
+
expect(errorItem).to.have.property('replayId');
|
|
347
|
+
expect(addSpy.calledOnce).to.be.true;
|
|
348
|
+
|
|
349
|
+
const responsePromise = handleReplayResponseSpy.returnValues[0];
|
|
350
|
+
await responsePromise;
|
|
351
|
+
expect(sendSpy.calledOnce).to.be.true;
|
|
352
|
+
expect(sendSpy.calledWith(errorItem.replayId)).to.be.true;
|
|
353
|
+
expect(postSpansSpy.calledOnce).to.be.true;
|
|
354
|
+
|
|
355
|
+
expect(recorder.dump.called).to.be.true;
|
|
356
|
+
expect(recorder.dump.calledWith(tracing, errorItem.replayId)).to.be.true;
|
|
357
|
+
const callArgs = postSpansSpy.firstCall.args;
|
|
358
|
+
expect(callArgs[0]).to.be.an('object');
|
|
359
|
+
expect(callArgs[0].resourceSpans[0].scopeSpans[0].spans[0].name).to.equal(
|
|
360
|
+
'rrweb-replay-recording',
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should not add replayId when replayMap is not provided', function (done) {
|
|
365
|
+
const queueWithoutReplayMap = new Queue(
|
|
366
|
+
{ shouldSend: () => ({ shouldSend: true }) },
|
|
367
|
+
api,
|
|
368
|
+
console,
|
|
369
|
+
{ transmit: true },
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const errorItem = {
|
|
373
|
+
body: {
|
|
374
|
+
trace: {
|
|
375
|
+
exception: {
|
|
376
|
+
message: 'Test without replayMap',
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
attributes: [
|
|
381
|
+
{ key: 'replay_id', value: '1234567812345678' },
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
queueWithoutReplayMap.addItem(errorItem, function (err, resp) {
|
|
386
|
+
expect(errorItem).to.not.have.property('replayId');
|
|
387
|
+
done();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the postSpans method in the API 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
|
+
|
|
14
|
+
const Api = require('../../../src/api');
|
|
15
|
+
|
|
16
|
+
describe('Api', function () {
|
|
17
|
+
let api;
|
|
18
|
+
let transport;
|
|
19
|
+
let mockUrl;
|
|
20
|
+
|
|
21
|
+
beforeEach(function () {
|
|
22
|
+
transport = {
|
|
23
|
+
post: sinon
|
|
24
|
+
.stub()
|
|
25
|
+
.callsFake((accessToken, options, payload, callback) => {
|
|
26
|
+
callback(null, { result: 'ok' });
|
|
27
|
+
}),
|
|
28
|
+
postJsonPayload: sinon.stub(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
mockUrl = {
|
|
32
|
+
parse: sinon.stub().returns({
|
|
33
|
+
hostname: 'api.rollbar.com',
|
|
34
|
+
protocol: 'https:',
|
|
35
|
+
port: 443,
|
|
36
|
+
pathname: '/api/1/item/',
|
|
37
|
+
search: null,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
api = new Api(
|
|
42
|
+
{ accessToken: 'test_token' },
|
|
43
|
+
transport,
|
|
44
|
+
mockUrl,
|
|
45
|
+
null, // truncation
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(function () {
|
|
50
|
+
sinon.restore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('postSpans', function () {
|
|
54
|
+
it('should create proper transport options for session endpoint', async function () {
|
|
55
|
+
const spans = [{ id: 'span1' }];
|
|
56
|
+
await api.postSpans(spans);
|
|
57
|
+
|
|
58
|
+
expect(transport.post.called).to.be.true;
|
|
59
|
+
|
|
60
|
+
const options = transport.post.firstCall.args[1];
|
|
61
|
+
expect(options.path).to.include('/session/');
|
|
62
|
+
expect(options.method).to.equal('POST');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should create a payload with resourceSpans', async function () {
|
|
66
|
+
const spans = { resourceSpans: [{ id: 'span1' }] };
|
|
67
|
+
await api.postSpans(spans);
|
|
68
|
+
|
|
69
|
+
expect(transport.post.called).to.be.true;
|
|
70
|
+
|
|
71
|
+
const payload = transport.post.firstCall.args[2];
|
|
72
|
+
expect(payload).to.have.property('resourceSpans');
|
|
73
|
+
expect(payload).to.deep.equal(spans);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return a promise that resolves with the response', async function () {
|
|
77
|
+
const spans = [{ id: 'span1' }];
|
|
78
|
+
const expectedResponse = { result: 'ok' };
|
|
79
|
+
|
|
80
|
+
const result = await api.postSpans(spans);
|
|
81
|
+
expect(result).to.deep.equal(expectedResponse);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle transport errors', async function () {
|
|
85
|
+
const spans = [{ id: 'span1' }];
|
|
86
|
+
const expectedError = new Error('Transport error');
|
|
87
|
+
|
|
88
|
+
transport.post.callsFake((accessToken, options, payload, callback) => {
|
|
89
|
+
callback(expectedError);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await api.postSpans(spans);
|
|
94
|
+
expect.fail('Should have thrown an error');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
expect(error).to.equal(expectedError);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should use the provided access token', async function () {
|
|
101
|
+
const spans = [{ id: 'span1' }];
|
|
102
|
+
await api.postSpans(spans);
|
|
103
|
+
|
|
104
|
+
expect(transport.post.called).to.be.true;
|
|
105
|
+
const accessToken = transport.post.firstCall.args[0];
|
|
106
|
+
expect(accessToken).to.equal('test_token');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should update transport options when API is reconfigured', async function () {
|
|
110
|
+
await api.postSpans([{ id: 'span1' }]);
|
|
111
|
+
|
|
112
|
+
api.configure({
|
|
113
|
+
endpoint: 'https://custom.rollbar.com/api/1/session/',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await api.postSpans([{ id: 'span2' }]);
|
|
117
|
+
|
|
118
|
+
expect(transport.post.callCount).to.equal(2);
|
|
119
|
+
|
|
120
|
+
const optionsAfterConfig = transport.post.secondCall.args[1];
|
|
121
|
+
expect(optionsAfterConfig.hostname).to.equal('api.rollbar.com');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('_postPromise', function () {
|
|
126
|
+
it('should resolve with response when transport succeeds', async function () {
|
|
127
|
+
const expectedResponse = { success: true };
|
|
128
|
+
transport.post.callsFake((accessToken, options, payload, callback) => {
|
|
129
|
+
callback(null, expectedResponse);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const result = await api._postPromise('token', {}, {});
|
|
133
|
+
expect(result).to.deep.equal(expectedResponse);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should reject with error when transport fails', async function () {
|
|
137
|
+
const expectedError = new Error('Transport error');
|
|
138
|
+
transport.post.callsFake((accessToken, options, payload, callback) => {
|
|
139
|
+
callback(expectedError);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await api._postPromise('token', {}, {});
|
|
144
|
+
expect.fail('Should have thrown an error');
|
|
145
|
+
} catch (error) {
|
|
146
|
+
expect(error).to.equal(expectedError);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|