rollbar 2.26.3 → 3.0.0-alpha.1
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/.cursor/rules/guidelines.mdc +154 -0
- package/.github/workflows/ci.yml +32 -12
- package/.lgtm.yml +7 -7
- package/.prettierignore +18 -0
- package/.vscode/settings.json +39 -0
- package/CHANGELOG.md +121 -35
- package/CLAUDE.md +201 -0
- package/Gruntfile.js +101 -48
- package/Makefile +3 -3
- package/README.md +2 -4
- package/SECURITY.md +5 -0
- package/babel.config.json +9 -0
- package/bower.json +1 -3
- package/codex.md +148 -0
- package/defaults.js +17 -5
- package/dist/plugins/jquery.min.js +1 -1
- package/dist/rollbar.js +18748 -5375
- 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 +19368 -6000
- package/dist/rollbar.named-amd.js.map +1 -1
- package/dist/rollbar.named-amd.min.js +3 -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 +18749 -5380
- package/dist/rollbar.noconflict.umd.js.map +1 -1
- package/dist/rollbar.noconflict.umd.min.js +3 -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 +19367 -6000
- package/dist/rollbar.umd.js.map +1 -1
- package/dist/rollbar.umd.min.js +3 -1
- package/dist/rollbar.umd.min.js.LICENSE.txt +1 -0
- package/dist/rollbar.umd.min.js.map +1 -1
- package/docs/extension-exceptions.md +35 -30
- package/docs/migration_v0_to_v1.md +41 -38
- package/eslint.config.mjs +33 -0
- package/get_versions.js +33 -0
- package/index.d.ts +270 -231
- package/karma.conf.js +18 -27
- package/package.json +21 -21
- package/prettier.config.js +7 -0
- package/src/api.js +78 -14
- package/src/apiUtility.js +14 -11
- package/src/browser/core.js +138 -72
- package/src/browser/defaults/scrubFields.js +3 -3
- package/src/browser/detection.js +7 -8
- package/src/browser/domUtility.js +18 -8
- package/src/browser/globalSetup.js +12 -6
- package/src/browser/logger.js +1 -1
- package/src/browser/plugins/jquery.js +35 -35
- package/src/browser/predicates.js +1 -1
- package/src/browser/replay/defaults.js +71 -0
- package/src/browser/replay/recorder.js +193 -0
- package/src/browser/replay/replayMap.js +195 -0
- package/src/browser/rollbar.js +12 -8
- package/src/browser/rollbarWrapper.js +8 -5
- package/src/browser/shim.js +43 -19
- package/src/browser/snippet_callback.js +6 -4
- package/src/browser/telemetry.js +573 -361
- package/src/browser/transforms.js +46 -27
- package/src/browser/transport/fetch.js +26 -14
- package/src/browser/transport/xhr.js +41 -14
- package/src/browser/transport.js +93 -33
- package/src/browser/url.js +16 -8
- package/src/browser/wrapGlobals.js +27 -8
- package/src/defaults.js +3 -3
- package/src/errorParser.js +14 -11
- package/src/merge.js +32 -23
- package/src/notifier.js +16 -13
- package/src/predicates.js +43 -23
- package/src/queue.js +133 -40
- package/src/rateLimiter.js +59 -18
- package/src/react-native/logger.js +1 -1
- package/src/react-native/rollbar.js +59 -55
- package/src/react-native/transforms.js +13 -9
- package/src/react-native/transport.js +44 -34
- package/src/rollbar.js +72 -21
- package/src/scrub.js +0 -1
- package/src/server/locals.js +69 -39
- package/src/server/logger.js +4 -4
- package/src/server/parser.js +72 -47
- package/src/server/rollbar.js +135 -56
- package/src/server/sourceMap/stackTrace.js +33 -18
- package/src/server/telemetry/urlHelpers.js +9 -11
- package/src/server/telemetry.js +68 -45
- package/src/server/transforms.js +37 -21
- package/src/server/transport.js +62 -32
- package/src/telemetry.js +162 -33
- 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/transforms.js +33 -21
- package/src/truncation.js +8 -5
- package/src/utility/headers.js +43 -43
- package/src/utility/replace.js +9 -0
- package/src/utility/traverse.js +1 -1
- package/src/utility.js +123 -52
- package/test/api.test.js +88 -41
- package/test/apiUtility.test.js +48 -50
- package/test/browser.core.test.js +142 -141
- package/test/browser.domUtility.test.js +53 -36
- package/test/browser.predicates.test.js +14 -14
- package/test/browser.replay.recorder.test.js +416 -0
- package/test/browser.rollbar.test.js +655 -515
- package/test/browser.telemetry.test.js +46 -39
- package/test/browser.transforms.test.js +164 -139
- package/test/browser.transport.test.js +59 -50
- package/test/browser.url.test.js +13 -12
- package/test/fixtures/locals.fixtures.js +245 -126
- 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 +91 -79
- package/test/predicates.test.js +261 -215
- package/test/queue.test.js +231 -215
- package/test/rateLimiter.test.js +51 -43
- package/test/react-native.rollbar.test.js +150 -116
- package/test/react-native.transforms.test.js +23 -25
- package/test/react-native.transport.test.js +26 -14
- 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 +178 -38
- 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 +70 -68
- package/test/truncation.test.js +57 -55
- package/test/utility.test.js +310 -228
- package/webpack.config.js +36 -70
- package/.eslintignore +0 -7
- package/.gitmodules +0 -3
- package/test/server.lambda.test.js +0 -177
- package/test/server.locals.test.js +0 -841
- package/test/server.parser.test.js +0 -72
- package/test/server.predicates.test.js +0 -89
- package/test/server.rollbar.test.js +0 -676
- package/test/server.telemetry.test.js +0 -318
- package/test/server.transforms.test.js +0 -1099
- package/test/server.transport.test.js +0 -201
|
@@ -3,33 +3,33 @@
|
|
|
3
3
|
/* globals it */
|
|
4
4
|
/* globals sinon */
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import * as d from '../src/browser/domUtility.js';
|
|
7
7
|
|
|
8
8
|
function fullElement() {
|
|
9
9
|
return {
|
|
10
10
|
tagName: 'DIV',
|
|
11
11
|
id: 'myId',
|
|
12
12
|
className: 'a b c',
|
|
13
|
-
getAttribute: function(t) {
|
|
13
|
+
getAttribute: function (t) {
|
|
14
14
|
return {
|
|
15
15
|
type: 'theType',
|
|
16
16
|
name: 'someName',
|
|
17
|
-
other: 'otherAttr'
|
|
17
|
+
other: 'otherAttr',
|
|
18
18
|
}[t];
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function genElement(tag, id, classes, type, name) {
|
|
24
24
|
var elem = {
|
|
25
25
|
tagName: tag,
|
|
26
|
-
getAttribute: function(t) {
|
|
26
|
+
getAttribute: function (t) {
|
|
27
27
|
return {
|
|
28
28
|
type: type,
|
|
29
29
|
name: name,
|
|
30
|
-
other: 'otherAttr'
|
|
30
|
+
other: 'otherAttr',
|
|
31
31
|
}[t];
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
33
|
};
|
|
34
34
|
if (id) {
|
|
35
35
|
elem.id = id;
|
|
@@ -40,41 +40,41 @@ function genElement(tag, id, classes, type, name) {
|
|
|
40
40
|
return elem;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
describe('isDescribedElement', function() {
|
|
44
|
-
it('should match the type without subtypes', function() {
|
|
43
|
+
describe('isDescribedElement', function () {
|
|
44
|
+
it('should match the type without subtypes', function () {
|
|
45
45
|
var e = genElement('div', null, null, 'text');
|
|
46
46
|
expect(d.isDescribedElement(e, 'div')).to.be.ok();
|
|
47
47
|
expect(d.isDescribedElement(e, 'DIV')).to.be.ok();
|
|
48
48
|
expect(d.isDescribedElement(e, 'span')).to.not.be.ok();
|
|
49
49
|
});
|
|
50
|
-
it('should work with subtypes', function() {
|
|
50
|
+
it('should work with subtypes', function () {
|
|
51
51
|
var e = genElement('div', null, null, 'text');
|
|
52
52
|
expect(d.isDescribedElement(e, 'div', ['input', 'text'])).to.be.ok();
|
|
53
53
|
expect(d.isDescribedElement(e, 'div', ['input', 'nottext'])).to.not.be.ok();
|
|
54
54
|
expect(d.isDescribedElement(e, 'div', [])).to.not.be.ok();
|
|
55
55
|
});
|
|
56
|
-
it('should work if element has no type', function() {
|
|
56
|
+
it('should work if element has no type', function () {
|
|
57
57
|
var e = genElement('div');
|
|
58
58
|
expect(d.isDescribedElement(e, 'div', ['input', 'text'])).to.not.be.ok();
|
|
59
59
|
expect(d.isDescribedElement(e, 'div')).to.be.ok();
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
describe('describeElement', function() {
|
|
64
|
-
it('should include the id', function() {
|
|
63
|
+
describe('describeElement', function () {
|
|
64
|
+
it('should include the id', function () {
|
|
65
65
|
var elem = fullElement();
|
|
66
66
|
var description = d.describeElement(elem);
|
|
67
67
|
expect(description.id).to.eql('myId');
|
|
68
68
|
});
|
|
69
|
-
it('should have the right tag name', function() {
|
|
69
|
+
it('should have the right tag name', function () {
|
|
70
70
|
var elem = fullElement();
|
|
71
71
|
var description = d.describeElement(elem);
|
|
72
72
|
expect(description.tagName).to.eql('div');
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
describe('descriptionToString', function() {
|
|
77
|
-
it('should be right', function() {
|
|
76
|
+
describe('descriptionToString', function () {
|
|
77
|
+
it('should be right', function () {
|
|
78
78
|
var elem = fullElement();
|
|
79
79
|
var desc = d.describeElement(elem);
|
|
80
80
|
var str = d.descriptionToString(desc);
|
|
@@ -82,14 +82,14 @@ describe('descriptionToString', function() {
|
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
describe('treeToArray', function() {
|
|
86
|
-
it('should follow parent pointers', function() {
|
|
85
|
+
describe('treeToArray', function () {
|
|
86
|
+
it('should follow parent pointers', function () {
|
|
87
87
|
var base = genElement('span', 'cool');
|
|
88
88
|
base.parentNode = genElement('div', 'parent');
|
|
89
89
|
var arr = d.treeToArray(base);
|
|
90
90
|
expect(arr.length).to.eql(2);
|
|
91
91
|
});
|
|
92
|
-
it('should not stop before html tag', function() {
|
|
92
|
+
it('should not stop before html tag', function () {
|
|
93
93
|
var e1 = genElement('div', 'cool');
|
|
94
94
|
var e2 = genElement('div', null, 'a b');
|
|
95
95
|
var h = genElement('html');
|
|
@@ -98,7 +98,7 @@ describe('treeToArray', function() {
|
|
|
98
98
|
var arr = d.treeToArray(e1);
|
|
99
99
|
expect(arr.length).to.eql(2);
|
|
100
100
|
});
|
|
101
|
-
it('should cap out at 5 elements', function() {
|
|
101
|
+
it('should cap out at 5 elements', function () {
|
|
102
102
|
var e1 = genElement('div', 'cool');
|
|
103
103
|
var e2 = genElement('div', null, 'a b');
|
|
104
104
|
var e3 = genElement('div', null, 'a b');
|
|
@@ -113,7 +113,7 @@ describe('treeToArray', function() {
|
|
|
113
113
|
var arr = d.treeToArray(e1);
|
|
114
114
|
expect(arr.length).to.eql(5);
|
|
115
115
|
});
|
|
116
|
-
it('should put the innermost element last', function() {
|
|
116
|
+
it('should put the innermost element last', function () {
|
|
117
117
|
var e1 = genElement('div', 'id1');
|
|
118
118
|
var e2 = genElement('div', 'id2', 'a b');
|
|
119
119
|
var e3 = genElement('div', 'id3', 'a b');
|
|
@@ -131,39 +131,56 @@ describe('treeToArray', function() {
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
describe('elementArrayToString', function() {
|
|
135
|
-
it('should work with one element', function() {
|
|
136
|
-
var e1 = {tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: []};
|
|
134
|
+
describe('elementArrayToString', function () {
|
|
135
|
+
it('should work with one element', function () {
|
|
136
|
+
var e1 = { tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: [] };
|
|
137
137
|
var arr = [e1];
|
|
138
138
|
var res = d.elementArrayToString(arr);
|
|
139
139
|
expect(res).to.eql('div#id1.a.b');
|
|
140
140
|
});
|
|
141
|
-
it('should work with two elements', function() {
|
|
142
|
-
var e1 = {tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: []};
|
|
143
|
-
var e2 = {
|
|
141
|
+
it('should work with two elements', function () {
|
|
142
|
+
var e1 = { tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: [] };
|
|
143
|
+
var e2 = {
|
|
144
|
+
tagName: 'div',
|
|
145
|
+
id: 'id2',
|
|
146
|
+
classes: ['a', 'b', 'c'],
|
|
147
|
+
attributes: [{ key: 'name', value: 'thing' }],
|
|
148
|
+
};
|
|
144
149
|
var arr = [e1, e2];
|
|
145
150
|
var res = d.elementArrayToString(arr);
|
|
146
151
|
expect(res).to.eql('div#id1.a.b > div#id2.a.b.c[name="thing"]');
|
|
147
152
|
});
|
|
148
|
-
it('should truncate at 80 characters max without breaking within a element', function() {
|
|
149
|
-
var e1 = {tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: []};
|
|
150
|
-
var e2 = {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
it('should truncate at 80 characters max without breaking within a element', function () {
|
|
154
|
+
var e1 = { tagName: 'div', id: 'id1', classes: ['a', 'b'], attributes: [] };
|
|
155
|
+
var e2 = {
|
|
156
|
+
tagName: 'div',
|
|
157
|
+
id: 'id2',
|
|
158
|
+
classes: ['a', 'b', 'c'],
|
|
159
|
+
attributes: [{ key: 'name', value: 'thing2' }],
|
|
160
|
+
};
|
|
161
|
+
var e3 = { tagName: 'div', id: 'id3', classes: ['a', 'b'], attributes: [] };
|
|
162
|
+
var e4 = {
|
|
163
|
+
tagName: 'div',
|
|
164
|
+
id: 'id4',
|
|
165
|
+
classes: ['a', 'b', 'c'],
|
|
166
|
+
attributes: [{ key: 'name', value: 'thing4' }],
|
|
167
|
+
};
|
|
153
168
|
var arr = [e1, e2, e3, e4];
|
|
154
169
|
var res = d.elementArrayToString(arr);
|
|
155
|
-
expect(res).to.eql(
|
|
170
|
+
expect(res).to.eql(
|
|
171
|
+
'... > div#id2.a.b.c[name="thing2"] > div#id3.a.b > div#id4.a.b.c[name="thing4"]',
|
|
172
|
+
);
|
|
156
173
|
});
|
|
157
174
|
});
|
|
158
175
|
|
|
159
|
-
describe('everything', function() {
|
|
160
|
-
it('should work with one element', function() {
|
|
176
|
+
describe('everything', function () {
|
|
177
|
+
it('should work with one element', function () {
|
|
161
178
|
var e = genElement('div', 'id1');
|
|
162
179
|
var description = d.descriptionToString(d.describeElement(e));
|
|
163
180
|
var result = d.elementArrayToString(d.treeToArray(e));
|
|
164
181
|
expect(description).to.eql(result);
|
|
165
182
|
});
|
|
166
|
-
it('should work with many elements', function() {
|
|
183
|
+
it('should work with many elements', function () {
|
|
167
184
|
var e1 = genElement('div', 'id1');
|
|
168
185
|
var e2 = genElement('div', 'id2', 'a b', 'input');
|
|
169
186
|
var e3 = genElement('div', 'id3', 'a b', null, 'thing');
|
|
@@ -3,50 +3,50 @@
|
|
|
3
3
|
/* globals it */
|
|
4
4
|
/* globals sinon */
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import * as p from '../src/browser/predicates.js';
|
|
7
7
|
|
|
8
|
-
describe('checkIgnore', function() {
|
|
9
|
-
it('should return false if is ajax and ignoring ajax errors is on', function() {
|
|
8
|
+
describe('checkIgnore', function () {
|
|
9
|
+
it('should return false if is ajax and ignoring ajax errors is on', function () {
|
|
10
10
|
var item = {
|
|
11
11
|
level: 'critical',
|
|
12
|
-
body: {message: {extra: {isAjax: true}}}
|
|
12
|
+
body: { message: { extra: { isAjax: true } } },
|
|
13
13
|
};
|
|
14
14
|
var settings = {
|
|
15
15
|
reportLevel: 'debug',
|
|
16
|
-
plugins: {jquery: {ignoreAjaxErrors: true}}
|
|
16
|
+
plugins: { jquery: { ignoreAjaxErrors: true } },
|
|
17
17
|
};
|
|
18
18
|
expect(p.checkIgnore(item, settings)).to.not.be.ok();
|
|
19
19
|
});
|
|
20
|
-
it('should return true if is ajax and ignoring ajax errors is off', function() {
|
|
20
|
+
it('should return true if is ajax and ignoring ajax errors is off', function () {
|
|
21
21
|
var item = {
|
|
22
22
|
level: 'critical',
|
|
23
|
-
body: {message: {extra: {isAjax: true}}}
|
|
23
|
+
body: { message: { extra: { isAjax: true } } },
|
|
24
24
|
};
|
|
25
25
|
var settings = {
|
|
26
26
|
reportLevel: 'debug',
|
|
27
|
-
plugins: {jquery: {ignoreAjaxErrors: false}}
|
|
27
|
+
plugins: { jquery: { ignoreAjaxErrors: false } },
|
|
28
28
|
};
|
|
29
29
|
expect(p.checkIgnore(item, settings)).to.be.ok();
|
|
30
30
|
});
|
|
31
|
-
it('should return true if is not ajax and ignoring ajax errors is on', function() {
|
|
31
|
+
it('should return true if is not ajax and ignoring ajax errors is on', function () {
|
|
32
32
|
var item = {
|
|
33
33
|
level: 'critical',
|
|
34
|
-
body: {message: {extra: {isAjax: false}}}
|
|
34
|
+
body: { message: { extra: { isAjax: false } } },
|
|
35
35
|
};
|
|
36
36
|
var settings = {
|
|
37
37
|
reportLevel: 'debug',
|
|
38
|
-
plugins: {jquery: {ignoreAjaxErrors: true}}
|
|
38
|
+
plugins: { jquery: { ignoreAjaxErrors: true } },
|
|
39
39
|
};
|
|
40
40
|
expect(p.checkIgnore(item, settings)).to.be.ok();
|
|
41
41
|
});
|
|
42
|
-
it('should return true if no ajax extra key and ignoring ajax errors is on', function() {
|
|
42
|
+
it('should return true if no ajax extra key and ignoring ajax errors is on', function () {
|
|
43
43
|
var item = {
|
|
44
44
|
level: 'critical',
|
|
45
|
-
body: {message: 'a message'}
|
|
45
|
+
body: { message: 'a message' },
|
|
46
46
|
};
|
|
47
47
|
var settings = {
|
|
48
48
|
reportLevel: 'debug',
|
|
49
|
-
plugins: {jquery: {ignoreAjaxErrors: true}}
|
|
49
|
+
plugins: { jquery: { ignoreAjaxErrors: true } },
|
|
50
50
|
};
|
|
51
51
|
expect(p.checkIgnore(item, settings)).to.be.ok();
|
|
52
52
|
});
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/* globals expect */
|
|
2
|
+
/* globals describe */
|
|
3
|
+
/* globals it */
|
|
4
|
+
/* globals beforeEach */
|
|
5
|
+
/* globals sinon */
|
|
6
|
+
|
|
7
|
+
import { expect } from 'chai';
|
|
8
|
+
import { EventType } from '@rrweb/types';
|
|
9
|
+
|
|
10
|
+
import Recorder from '../src/browser/replay/recorder.js';
|
|
11
|
+
|
|
12
|
+
describe('Recorder', function () {
|
|
13
|
+
let mockTracing;
|
|
14
|
+
let mockSpan;
|
|
15
|
+
let stopFnSpy;
|
|
16
|
+
let recordFnStub;
|
|
17
|
+
let emitCallback;
|
|
18
|
+
let testReplayId;
|
|
19
|
+
|
|
20
|
+
beforeEach(function () {
|
|
21
|
+
mockSpan = {
|
|
22
|
+
addEvent: sinon.spy(),
|
|
23
|
+
setAttribute: sinon.spy(),
|
|
24
|
+
span: { startTime: null },
|
|
25
|
+
end: sinon.spy(),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
mockTracing = {
|
|
29
|
+
startSpan: sinon.stub().returns(mockSpan),
|
|
30
|
+
exporter: {
|
|
31
|
+
toPayload: sinon.stub().returns([{ id: 'span1' }]),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
testReplayId = 'test-replay-id-123';
|
|
36
|
+
stopFnSpy = sinon.spy();
|
|
37
|
+
recordFnStub = sinon.stub().callsFake(function (options) {
|
|
38
|
+
emitCallback = options.emit;
|
|
39
|
+
return stopFnSpy;
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('constructor', function () {
|
|
44
|
+
it('should initialize with default properties', function () {
|
|
45
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
46
|
+
|
|
47
|
+
expect(recorder.isRecording).to.equal(false);
|
|
48
|
+
expect(recorder.options).to.deep.equal({ enabled: undefined, autoStart: undefined, maxSeconds: undefined, triggerOptions:undefined });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should initialize removing disallowed options', function () {
|
|
52
|
+
const options = { enabled: true, checkoutEveryNms: 1000 };
|
|
53
|
+
const recorder = new Recorder(options, recordFnStub);
|
|
54
|
+
|
|
55
|
+
expect(recorder.options).to.deep.equal({ enabled: true, autoStart: undefined, maxSeconds: undefined, triggerOptions:undefined });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error if no record function is passed', function () {
|
|
59
|
+
expect(() => new Recorder({}, null)).to.throw(
|
|
60
|
+
TypeError,
|
|
61
|
+
"Expected 'recordFn' to be provided",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('recording management', function () {
|
|
67
|
+
it('should start recording correctly', function () {
|
|
68
|
+
const recorder = new Recorder(
|
|
69
|
+
{ enabled: true },
|
|
70
|
+
recordFnStub,
|
|
71
|
+
);
|
|
72
|
+
recorder.start();
|
|
73
|
+
|
|
74
|
+
expect(recorder.isRecording).to.be.true;
|
|
75
|
+
expect(recordFnStub.calledOnce).to.be.true;
|
|
76
|
+
expect(stopFnSpy.called).to.be.false;
|
|
77
|
+
|
|
78
|
+
const recordOptions = recordFnStub.firstCall.args[0];
|
|
79
|
+
expect(recordOptions.checkoutEveryNms).to.equal(5000);
|
|
80
|
+
expect(typeof recordOptions.emit).to.equal('function');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should not start if already recording', function () {
|
|
84
|
+
const recorder = new Recorder(
|
|
85
|
+
{ enabled: true },
|
|
86
|
+
recordFnStub,
|
|
87
|
+
);
|
|
88
|
+
recorder.start();
|
|
89
|
+
recorder.start();
|
|
90
|
+
|
|
91
|
+
expect(recordFnStub.calledOnce).to.be.true;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should not start if disabled', function () {
|
|
95
|
+
const recorder = new Recorder(
|
|
96
|
+
{ enabled: false },
|
|
97
|
+
recordFnStub,
|
|
98
|
+
);
|
|
99
|
+
recorder.start();
|
|
100
|
+
|
|
101
|
+
expect(recorder.isRecording).to.be.false;
|
|
102
|
+
expect(recordFnStub.called).to.be.false;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should stop recording correctly', function () {
|
|
106
|
+
const recorder = new Recorder(
|
|
107
|
+
{ enabled: true },
|
|
108
|
+
recordFnStub,
|
|
109
|
+
);
|
|
110
|
+
recorder.start();
|
|
111
|
+
recorder.stop();
|
|
112
|
+
|
|
113
|
+
expect(recorder.isRecording).to.be.false;
|
|
114
|
+
expect(recordFnStub.calledOnce).to.be.true;
|
|
115
|
+
expect(stopFnSpy.calledOnce).to.be.true;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle stop when not recording', function () {
|
|
119
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
120
|
+
recorder.stop();
|
|
121
|
+
|
|
122
|
+
expect(recorder.isRecording).to.be.false;
|
|
123
|
+
expect(stopFnSpy.called).to.be.false;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('event handling', function () {
|
|
128
|
+
it('should handle events correctly', function () {
|
|
129
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
130
|
+
recorder.start();
|
|
131
|
+
|
|
132
|
+
const event1 = { timestamp: 1000, type: 'event1', data: { a: 1 } };
|
|
133
|
+
const event2 = { timestamp: 2000, type: 'event2', data: { b: 2 } };
|
|
134
|
+
|
|
135
|
+
emitCallback(event1, false);
|
|
136
|
+
emitCallback(event2, false);
|
|
137
|
+
|
|
138
|
+
const result = recorder.dump(mockTracing, testReplayId);
|
|
139
|
+
|
|
140
|
+
expect(mockTracing.startSpan.calledOnce).to.be.true;
|
|
141
|
+
expect(mockSpan.addEvent.calledTwice).to.be.true;
|
|
142
|
+
expect(mockSpan.setAttribute.calledOnce).to.be.true;
|
|
143
|
+
expect(mockSpan.setAttribute.calledWith('rollbar.replay.id', testReplayId)).to.be.true;
|
|
144
|
+
|
|
145
|
+
const firstCallData = mockSpan.addEvent.firstCall.args[1];
|
|
146
|
+
expect(firstCallData.eventType).to.equal('event1');
|
|
147
|
+
expect(JSON.parse(firstCallData.json)).to.deep.equal({ a: 1 });
|
|
148
|
+
expect(firstCallData['rollbar.replay.id']).to.equal(testReplayId);
|
|
149
|
+
|
|
150
|
+
const secondCallData = mockSpan.addEvent.secondCall.args[1];
|
|
151
|
+
expect(secondCallData.eventType).to.equal('event2');
|
|
152
|
+
expect(JSON.parse(secondCallData.json)).to.deep.equal({ b: 2 });
|
|
153
|
+
expect(secondCallData['rollbar.replay.id']).to.equal(testReplayId);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle checkout events correctly', function () {
|
|
157
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
158
|
+
recorder.start();
|
|
159
|
+
|
|
160
|
+
// First checkout
|
|
161
|
+
emitCallback({ timestamp: 0, type: EventType.Meta, data: {} }, true);
|
|
162
|
+
emitCallback(
|
|
163
|
+
{ timestamp: 10, type: EventType.FullSnapshot, data: {} },
|
|
164
|
+
true,
|
|
165
|
+
);
|
|
166
|
+
emitCallback(
|
|
167
|
+
{
|
|
168
|
+
timestamp: 1000,
|
|
169
|
+
type: EventType.IncrementalSnapshot,
|
|
170
|
+
data: { a: 1 },
|
|
171
|
+
},
|
|
172
|
+
false,
|
|
173
|
+
);
|
|
174
|
+
emitCallback(
|
|
175
|
+
{
|
|
176
|
+
timestamp: 2000,
|
|
177
|
+
type: EventType.IncrementalSnapshot,
|
|
178
|
+
data: { b: 2 },
|
|
179
|
+
},
|
|
180
|
+
false,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Second checkout
|
|
184
|
+
emitCallback({ timestamp: 3050, type: EventType.Meta, data: {} }, true);
|
|
185
|
+
emitCallback(
|
|
186
|
+
{ timestamp: 3100, type: EventType.FullSnapshot, data: {} },
|
|
187
|
+
true,
|
|
188
|
+
);
|
|
189
|
+
emitCallback(
|
|
190
|
+
{
|
|
191
|
+
timestamp: 4000,
|
|
192
|
+
type: EventType.IncrementalSnapshot,
|
|
193
|
+
data: { c: 3 },
|
|
194
|
+
},
|
|
195
|
+
false,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Third checkout
|
|
199
|
+
emitCallback({ timestamp: 4500, type: EventType.Meta, data: {} }, true);
|
|
200
|
+
emitCallback(
|
|
201
|
+
{ timestamp: 5000, type: EventType.FullSnapshot, data: {} },
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
emitCallback(
|
|
205
|
+
{
|
|
206
|
+
timestamp: 6000,
|
|
207
|
+
type: EventType.IncrementalSnapshot,
|
|
208
|
+
data: { d: 4 },
|
|
209
|
+
},
|
|
210
|
+
false,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
recorder.dump(mockTracing, testReplayId);
|
|
214
|
+
|
|
215
|
+
// 2nd checkout (meta + fs) + event3 + 3rd checkout (meta + fs) + event4
|
|
216
|
+
expect(mockSpan.span.startTime).to.be.deep.equal([3, 50000000]); // otel time
|
|
217
|
+
expect(mockSpan.span.endTime).not.to.be.null;
|
|
218
|
+
|
|
219
|
+
expect(mockSpan.addEvent.callCount).to.equal(6);
|
|
220
|
+
|
|
221
|
+
[
|
|
222
|
+
{
|
|
223
|
+
name: 'rrweb-replay-events',
|
|
224
|
+
attributes: {
|
|
225
|
+
eventType: EventType.Meta,
|
|
226
|
+
json: JSON.stringify({}),
|
|
227
|
+
'rollbar.replay.id': testReplayId,
|
|
228
|
+
},
|
|
229
|
+
timestamp: [3, 50000000],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'rrweb-replay-events',
|
|
233
|
+
attributes: {
|
|
234
|
+
eventType: EventType.FullSnapshot,
|
|
235
|
+
json: JSON.stringify({}),
|
|
236
|
+
'rollbar.replay.id': testReplayId,
|
|
237
|
+
},
|
|
238
|
+
timestamp: [3, 100000000],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'rrweb-replay-events',
|
|
242
|
+
attributes: {
|
|
243
|
+
eventType: EventType.IncrementalSnapshot,
|
|
244
|
+
json: JSON.stringify({ c: 3 }),
|
|
245
|
+
'rollbar.replay.id': testReplayId,
|
|
246
|
+
},
|
|
247
|
+
timestamp: [4, 0],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'rrweb-replay-events',
|
|
251
|
+
attributes: {
|
|
252
|
+
eventType: EventType.Meta,
|
|
253
|
+
json: JSON.stringify({}),
|
|
254
|
+
'rollbar.replay.id': testReplayId,
|
|
255
|
+
},
|
|
256
|
+
timestamp: [4, 500000000],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'rrweb-replay-events',
|
|
260
|
+
attributes: {
|
|
261
|
+
eventType: EventType.FullSnapshot,
|
|
262
|
+
json: JSON.stringify({}),
|
|
263
|
+
'rollbar.replay.id': testReplayId,
|
|
264
|
+
},
|
|
265
|
+
timestamp: [5, 0],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'rrweb-replay-events',
|
|
269
|
+
attributes: {
|
|
270
|
+
eventType: EventType.IncrementalSnapshot,
|
|
271
|
+
json: JSON.stringify({ d: 4 }),
|
|
272
|
+
'rollbar.replay.id': testReplayId,
|
|
273
|
+
},
|
|
274
|
+
timestamp: [6, 0],
|
|
275
|
+
},
|
|
276
|
+
].forEach((expected, index) => {
|
|
277
|
+
const call = mockSpan.addEvent.getCall(index);
|
|
278
|
+
expect(call.args[0]).to.equal(expected.name);
|
|
279
|
+
expect(call.args[1]).to.deep.equal(expected.attributes);
|
|
280
|
+
expect(call.args[2]).to.deep.equal(expected.timestamp);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('dump functionality', function () {
|
|
286
|
+
it('should create a span with events and return formatted payload', function () {
|
|
287
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
288
|
+
recorder.start();
|
|
289
|
+
|
|
290
|
+
emitCallback({ timestamp: 1000, type: 'event1', data: { a: 1 } }, false);
|
|
291
|
+
emitCallback({ timestamp: 2000, type: 'event2', data: { b: 2 } }, false);
|
|
292
|
+
|
|
293
|
+
const result = recorder.dump(mockTracing, testReplayId);
|
|
294
|
+
|
|
295
|
+
expect(result).to.deep.equal([{ id: 'span1' }]);
|
|
296
|
+
expect(mockTracing.startSpan.calledOnce).to.be.true;
|
|
297
|
+
expect(mockSpan.addEvent.calledTwice).to.be.true;
|
|
298
|
+
expect(mockSpan.end.calledOnce).to.be.true;
|
|
299
|
+
expect(mockTracing.exporter.toPayload.calledOnce).to.be.true;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should create a span with the correct span name', function () {
|
|
303
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
304
|
+
recorder.start();
|
|
305
|
+
|
|
306
|
+
emitCallback({ timestamp: 1000, type: 'event1', data: { a: 1 } }, false);
|
|
307
|
+
emitCallback({ timestamp: 2000, type: 'event2', data: { b: 2 } }, false);
|
|
308
|
+
|
|
309
|
+
recorder.dump(mockTracing, testReplayId);
|
|
310
|
+
|
|
311
|
+
expect(mockTracing.startSpan.calledOnce).to.be.true;
|
|
312
|
+
const spanName = mockTracing.startSpan.firstCall.args[0];
|
|
313
|
+
expect(spanName).to.equal('rrweb-replay-recording');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should add events with the correct event name and replayId', function () {
|
|
317
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
318
|
+
recorder.start();
|
|
319
|
+
|
|
320
|
+
emitCallback(
|
|
321
|
+
{ timestamp: 1000, type: 'fullSnapshot', data: { a: 1 } },
|
|
322
|
+
false,
|
|
323
|
+
);
|
|
324
|
+
emitCallback(
|
|
325
|
+
{ timestamp: 2000, type: 'mouseMove', data: { b: 2 } },
|
|
326
|
+
false,
|
|
327
|
+
);
|
|
328
|
+
emitCallback({ timestamp: 3000, type: 'input', data: { c: 3 } }, false);
|
|
329
|
+
|
|
330
|
+
recorder.dump(mockTracing, testReplayId);
|
|
331
|
+
|
|
332
|
+
expect(mockSpan.addEvent.callCount).to.equal(3);
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < mockSpan.addEvent.callCount; i++) {
|
|
335
|
+
const eventName = mockSpan.addEvent.getCall(i).args[0];
|
|
336
|
+
const eventAttrs = mockSpan.addEvent.getCall(i).args[1];
|
|
337
|
+
expect(eventName).to.equal(
|
|
338
|
+
'rrweb-replay-events',
|
|
339
|
+
`Event at index ${i} should have name "rrweb-replay-events"`
|
|
340
|
+
);
|
|
341
|
+
expect(eventAttrs['rollbar.replay.id']).to.equal(testReplayId);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should handle no events', function () {
|
|
346
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
347
|
+
|
|
348
|
+
const result = recorder.dump(mockTracing, testReplayId);
|
|
349
|
+
|
|
350
|
+
expect(result).to.be.null;
|
|
351
|
+
expect(mockTracing.startSpan.called).to.be.false;
|
|
352
|
+
expect(mockTracing.exporter.toPayload.called).to.be.false;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should handle less than 2 events (invalid recording)', function () {
|
|
356
|
+
const recorder = new Recorder({}, recordFnStub);
|
|
357
|
+
recorder.start();
|
|
358
|
+
|
|
359
|
+
emitCallback({ timestamp: 1000, type: 'event1', data: { a: 1 } }, false);
|
|
360
|
+
|
|
361
|
+
const result = recorder.dump(mockTracing, testReplayId);
|
|
362
|
+
|
|
363
|
+
expect(result).to.be.null;
|
|
364
|
+
expect(mockTracing.startSpan.called).to.be.false;
|
|
365
|
+
expect(mockTracing.exporter.toPayload.called).to.be.false;
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('configure', function () {
|
|
370
|
+
it('should update options', function () {
|
|
371
|
+
const recorder = new Recorder(
|
|
372
|
+
{ enabled: true },
|
|
373
|
+
recordFnStub,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
recorder.configure({ enabled: false, maxSeconds: 20 });
|
|
377
|
+
|
|
378
|
+
expect(recorder.options).to.deep.equal({
|
|
379
|
+
enabled: false,
|
|
380
|
+
autoStart: undefined,
|
|
381
|
+
maxSeconds: 20,
|
|
382
|
+
triggerOptions:undefined,
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should set correct checkoutEveryNms', function () {
|
|
387
|
+
const recorder = new Recorder({ enabled: false });
|
|
388
|
+
|
|
389
|
+
recorder.configure({ enabled: true, maxSeconds: 15 });
|
|
390
|
+
|
|
391
|
+
expect(recorder.options).to.deep.equal({
|
|
392
|
+
enabled: true,
|
|
393
|
+
autoStart: undefined,
|
|
394
|
+
maxSeconds: 15,
|
|
395
|
+
triggerOptions:undefined,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(recorder.checkoutEveryNms()).to.equal(7500);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should stop recording if enabled set to false', function () {
|
|
402
|
+
const recorder = new Recorder(
|
|
403
|
+
{ enabled: true },
|
|
404
|
+
recordFnStub,
|
|
405
|
+
);
|
|
406
|
+
recorder.start();
|
|
407
|
+
|
|
408
|
+
expect(recorder.isRecording).to.be.true;
|
|
409
|
+
|
|
410
|
+
recorder.configure({ enabled: false });
|
|
411
|
+
|
|
412
|
+
expect(recorder.isRecording).to.be.false;
|
|
413
|
+
expect(stopFnSpy.calledOnce).to.be.true;
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|