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.
Files changed (186) hide show
  1. package/.cursor/rules/guidelines.mdc +154 -0
  2. package/.github/workflows/ci.yml +32 -12
  3. package/.lgtm.yml +7 -7
  4. package/.prettierignore +18 -0
  5. package/.vscode/settings.json +39 -0
  6. package/CHANGELOG.md +121 -35
  7. package/CLAUDE.md +201 -0
  8. package/Gruntfile.js +101 -48
  9. package/Makefile +3 -3
  10. package/README.md +2 -4
  11. package/SECURITY.md +5 -0
  12. package/babel.config.json +9 -0
  13. package/bower.json +1 -3
  14. package/codex.md +148 -0
  15. package/defaults.js +17 -5
  16. package/dist/plugins/jquery.min.js +1 -1
  17. package/dist/rollbar.js +18748 -5375
  18. package/dist/rollbar.js.map +1 -1
  19. package/dist/rollbar.min.js +2 -1
  20. package/dist/rollbar.min.js.LICENSE.txt +1 -0
  21. package/dist/rollbar.min.js.map +1 -1
  22. package/dist/rollbar.named-amd.js +19368 -6000
  23. package/dist/rollbar.named-amd.js.map +1 -1
  24. package/dist/rollbar.named-amd.min.js +3 -1
  25. package/dist/rollbar.named-amd.min.js.LICENSE.txt +1 -0
  26. package/dist/rollbar.named-amd.min.js.map +1 -1
  27. package/dist/rollbar.noconflict.umd.js +18749 -5380
  28. package/dist/rollbar.noconflict.umd.js.map +1 -1
  29. package/dist/rollbar.noconflict.umd.min.js +3 -1
  30. package/dist/rollbar.noconflict.umd.min.js.LICENSE.txt +1 -0
  31. package/dist/rollbar.noconflict.umd.min.js.map +1 -1
  32. package/dist/rollbar.snippet.js +1 -1
  33. package/dist/rollbar.umd.js +19367 -6000
  34. package/dist/rollbar.umd.js.map +1 -1
  35. package/dist/rollbar.umd.min.js +3 -1
  36. package/dist/rollbar.umd.min.js.LICENSE.txt +1 -0
  37. package/dist/rollbar.umd.min.js.map +1 -1
  38. package/docs/extension-exceptions.md +35 -30
  39. package/docs/migration_v0_to_v1.md +41 -38
  40. package/eslint.config.mjs +33 -0
  41. package/get_versions.js +33 -0
  42. package/index.d.ts +270 -231
  43. package/karma.conf.js +18 -27
  44. package/package.json +21 -21
  45. package/prettier.config.js +7 -0
  46. package/src/api.js +78 -14
  47. package/src/apiUtility.js +14 -11
  48. package/src/browser/core.js +138 -72
  49. package/src/browser/defaults/scrubFields.js +3 -3
  50. package/src/browser/detection.js +7 -8
  51. package/src/browser/domUtility.js +18 -8
  52. package/src/browser/globalSetup.js +12 -6
  53. package/src/browser/logger.js +1 -1
  54. package/src/browser/plugins/jquery.js +35 -35
  55. package/src/browser/predicates.js +1 -1
  56. package/src/browser/replay/defaults.js +71 -0
  57. package/src/browser/replay/recorder.js +193 -0
  58. package/src/browser/replay/replayMap.js +195 -0
  59. package/src/browser/rollbar.js +12 -8
  60. package/src/browser/rollbarWrapper.js +8 -5
  61. package/src/browser/shim.js +43 -19
  62. package/src/browser/snippet_callback.js +6 -4
  63. package/src/browser/telemetry.js +573 -361
  64. package/src/browser/transforms.js +46 -27
  65. package/src/browser/transport/fetch.js +26 -14
  66. package/src/browser/transport/xhr.js +41 -14
  67. package/src/browser/transport.js +93 -33
  68. package/src/browser/url.js +16 -8
  69. package/src/browser/wrapGlobals.js +27 -8
  70. package/src/defaults.js +3 -3
  71. package/src/errorParser.js +14 -11
  72. package/src/merge.js +32 -23
  73. package/src/notifier.js +16 -13
  74. package/src/predicates.js +43 -23
  75. package/src/queue.js +133 -40
  76. package/src/rateLimiter.js +59 -18
  77. package/src/react-native/logger.js +1 -1
  78. package/src/react-native/rollbar.js +59 -55
  79. package/src/react-native/transforms.js +13 -9
  80. package/src/react-native/transport.js +44 -34
  81. package/src/rollbar.js +72 -21
  82. package/src/scrub.js +0 -1
  83. package/src/server/locals.js +69 -39
  84. package/src/server/logger.js +4 -4
  85. package/src/server/parser.js +72 -47
  86. package/src/server/rollbar.js +135 -56
  87. package/src/server/sourceMap/stackTrace.js +33 -18
  88. package/src/server/telemetry/urlHelpers.js +9 -11
  89. package/src/server/telemetry.js +68 -45
  90. package/src/server/transforms.js +37 -21
  91. package/src/server/transport.js +62 -32
  92. package/src/telemetry.js +162 -33
  93. package/src/tracing/context.js +24 -0
  94. package/src/tracing/contextManager.js +37 -0
  95. package/src/tracing/defaults.js +7 -0
  96. package/src/tracing/exporter.js +188 -0
  97. package/src/tracing/hrtime.js +98 -0
  98. package/src/tracing/id.js +24 -0
  99. package/src/tracing/session.js +55 -0
  100. package/src/tracing/span.js +92 -0
  101. package/src/tracing/spanProcessor.js +15 -0
  102. package/src/tracing/tracer.js +46 -0
  103. package/src/tracing/tracing.js +89 -0
  104. package/src/transforms.js +33 -21
  105. package/src/truncation.js +8 -5
  106. package/src/utility/headers.js +43 -43
  107. package/src/utility/replace.js +9 -0
  108. package/src/utility/traverse.js +1 -1
  109. package/src/utility.js +123 -52
  110. package/test/api.test.js +88 -41
  111. package/test/apiUtility.test.js +48 -50
  112. package/test/browser.core.test.js +142 -141
  113. package/test/browser.domUtility.test.js +53 -36
  114. package/test/browser.predicates.test.js +14 -14
  115. package/test/browser.replay.recorder.test.js +416 -0
  116. package/test/browser.rollbar.test.js +655 -515
  117. package/test/browser.telemetry.test.js +46 -39
  118. package/test/browser.transforms.test.js +164 -139
  119. package/test/browser.transport.test.js +59 -50
  120. package/test/browser.url.test.js +13 -12
  121. package/test/fixtures/locals.fixtures.js +245 -126
  122. package/test/fixtures/replay/index.js +20 -0
  123. package/test/fixtures/replay/payloads.fixtures.js +229 -0
  124. package/test/fixtures/replay/rrwebEvents.fixtures.js +251 -0
  125. package/test/fixtures/replay/rrwebSyntheticEvents.fixtures.js +328 -0
  126. package/test/notifier.test.js +91 -79
  127. package/test/predicates.test.js +261 -215
  128. package/test/queue.test.js +231 -215
  129. package/test/rateLimiter.test.js +51 -43
  130. package/test/react-native.rollbar.test.js +150 -116
  131. package/test/react-native.transforms.test.js +23 -25
  132. package/test/react-native.transport.test.js +26 -14
  133. package/test/replay/index.js +2 -0
  134. package/test/replay/integration/api.spans.test.js +136 -0
  135. package/test/replay/integration/e2e.test.js +228 -0
  136. package/test/replay/integration/index.js +9 -0
  137. package/test/replay/integration/queue.replayMap.test.js +332 -0
  138. package/test/replay/integration/replayMap.test.js +163 -0
  139. package/test/replay/integration/sessionRecording.test.js +390 -0
  140. package/test/replay/unit/api.postSpans.test.js +150 -0
  141. package/test/replay/unit/index.js +7 -0
  142. package/test/replay/unit/queue.replayMap.test.js +225 -0
  143. package/test/replay/unit/replayMap.test.js +348 -0
  144. package/test/replay/util/index.js +5 -0
  145. package/test/replay/util/mockRecordFn.js +80 -0
  146. package/test/server.lambda.mocha.test.mjs +172 -0
  147. package/test/server.locals.constructor.mocha.test.mjs +80 -0
  148. package/test/server.locals.error-handling.mocha.test.mjs +387 -0
  149. package/test/server.locals.merge.mocha.test.mjs +267 -0
  150. package/test/server.locals.test-utils.mjs +114 -0
  151. package/test/server.parser.mocha.test.mjs +87 -0
  152. package/test/server.predicates.mocha.test.mjs +63 -0
  153. package/test/server.rollbar.constructor.mocha.test.mjs +199 -0
  154. package/test/server.rollbar.handlers.mocha.test.mjs +253 -0
  155. package/test/server.rollbar.logging.mocha.test.mjs +326 -0
  156. package/test/server.rollbar.misc.mocha.test.mjs +44 -0
  157. package/test/server.rollbar.test-utils.mjs +57 -0
  158. package/test/server.telemetry.mocha.test.mjs +377 -0
  159. package/test/server.transforms.data.mocha.test.mjs +163 -0
  160. package/test/server.transforms.error.mocha.test.mjs +199 -0
  161. package/test/server.transforms.request.mocha.test.mjs +208 -0
  162. package/test/server.transforms.scrub.mocha.test.mjs +140 -0
  163. package/test/server.transforms.sourcemaps.mocha.test.mjs +122 -0
  164. package/test/server.transforms.test-utils.mjs +62 -0
  165. package/test/server.transport.mocha.test.mjs +269 -0
  166. package/test/telemetry.test.js +178 -38
  167. package/test/tracing/contextManager.test.js +28 -0
  168. package/test/tracing/exporter.toPayload.test.js +400 -0
  169. package/test/tracing/id.test.js +24 -0
  170. package/test/tracing/span.test.js +183 -0
  171. package/test/tracing/spanProcessor.test.js +73 -0
  172. package/test/tracing/tracing.test.js +105 -0
  173. package/test/transforms.test.js +70 -68
  174. package/test/truncation.test.js +57 -55
  175. package/test/utility.test.js +310 -228
  176. package/webpack.config.js +36 -70
  177. package/.eslintignore +0 -7
  178. package/.gitmodules +0 -3
  179. package/test/server.lambda.test.js +0 -177
  180. package/test/server.locals.test.js +0 -841
  181. package/test/server.parser.test.js +0 -72
  182. package/test/server.predicates.test.js +0 -89
  183. package/test/server.rollbar.test.js +0 -676
  184. package/test/server.telemetry.test.js +0 -318
  185. package/test/server.transforms.test.js +0 -1099
  186. package/test/server.transport.test.js +0 -201
@@ -3,33 +3,33 @@
3
3
  /* globals it */
4
4
  /* globals sinon */
5
5
 
6
- var d = require('../src/browser/domUtility');
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 = {tagName: 'div', id: 'id2', classes: ['a', 'b', 'c'], attributes: [{key: 'name', value: 'thing'}]};
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 = {tagName: 'div', id: 'id2', classes: ['a', 'b', 'c'], attributes: [{key: 'name', value: 'thing2'}]};
151
- var e3 = {tagName: 'div', id: 'id3', classes: ['a', 'b'], attributes: []};
152
- var e4 = {tagName: 'div', id: 'id4', classes: ['a', 'b', 'c'], attributes: [{key: 'name', value: 'thing4'}]};
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('... > div#id2.a.b.c[name="thing2"] > div#id3.a.b > div#id4.a.b.c[name="thing4"]');
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
- var p = require('../src/browser/predicates');
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
+ });