browser-commander 0.27.0 → 0.29.0

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 (93) hide show
  1. package/.jscpd-baseline.json +5 -19
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +23 -9
  4. package/eslint.config.js +11 -0
  5. package/package.json +5 -2
  6. package/src/browser/browser-profile-files.js +5 -2
  7. package/src/browser/connector.js +215 -36
  8. package/src/browser/launch-options.js +2 -1
  9. package/src/browser/launch-restrictions.json +33 -2
  10. package/src/browser/launcher.js +26 -1
  11. package/src/browser/persistent-session.js +432 -0
  12. package/src/browser/profile-directory.js +20 -0
  13. package/src/browser/real-browser.js +7 -2
  14. package/src/browser/restrictions.js +13 -4
  15. package/src/browser/session-adoption.js +96 -0
  16. package/src/browser/session-watchdog.js +71 -0
  17. package/src/browser/session-worker.js +82 -0
  18. package/src/browser/webdriver.js +4 -0
  19. package/src/capture/encoding.js +327 -0
  20. package/src/capture/errors.js +12 -0
  21. package/src/capture/files.js +9 -0
  22. package/src/capture/index.js +5 -0
  23. package/src/capture/recording.js +136 -0
  24. package/src/capture/screenshot.js +222 -0
  25. package/src/capture/video.js +186 -0
  26. package/src/capture/viewport.js +68 -0
  27. package/src/capture/worker.js +22 -0
  28. package/src/cli/args.js +69 -1
  29. package/src/cli/commands.js +116 -2
  30. package/src/cli/high-level-methods.js +47 -3
  31. package/src/cli/sessions.js +14 -3
  32. package/src/core/commander-features.js +110 -0
  33. package/src/core/constants.js +1 -0
  34. package/src/core/navigation-manager.js +4 -2
  35. package/src/core/navigation-readiness.js +30 -2
  36. package/src/core/navigation-safety.js +52 -0
  37. package/src/core/page-trigger-manager.js +280 -156
  38. package/src/core/readiness.js +10 -2
  39. package/src/elements/locators.js +26 -32
  40. package/src/elements/selectors.js +37 -81
  41. package/src/elements/visibility.js +32 -10
  42. package/src/factory.js +13 -12
  43. package/src/index.js +4 -0
  44. package/src/interactions/click.js +44 -0
  45. package/src/interactions/overlays.js +73 -0
  46. package/src/traces/bundle.js +55 -8
  47. package/src/traces/checkpoints.js +103 -0
  48. package/src/traces/links.js +157 -2
  49. package/src/traces/mutation-stream.js +9 -2
  50. package/src/traces/network.js +309 -0
  51. package/src/traces/page-capture.js +203 -68
  52. package/src/traces/reader.js +8 -10
  53. package/src/traces/recorder.js +253 -247
  54. package/src/traces/redaction.js +89 -6
  55. package/src/traces/render.js +168 -0
  56. package/src/traces/rolling-bundle.js +240 -0
  57. package/src/traces/rolling.js +77 -0
  58. package/src/traces/start-cleanup.js +15 -0
  59. package/src/traces/stop.js +160 -0
  60. package/src/traces/storage.js +43 -0
  61. package/src/utilities/subprocess.js +32 -0
  62. package/tests/e2e/cli.e2e.test.js +20 -1
  63. package/tests/e2e/click-readiness.e2e.test.js +1 -1
  64. package/tests/e2e/issue-160.e2e.test.js +291 -0
  65. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
  66. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
  67. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
  68. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
  69. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
  70. package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
  71. package/tests/unit/browser/issue-160-connector.test.js +157 -0
  72. package/tests/unit/browser/launch-building-blocks.test.js +5 -1
  73. package/tests/unit/browser/launcher.test.js +5 -1
  74. package/tests/unit/browser/persistent-session.test.js +59 -0
  75. package/tests/unit/browser/quiet-ui.test.js +16 -0
  76. package/tests/unit/browser/tab-selection.test.js +38 -0
  77. package/tests/unit/capture/capture.test.js +52 -0
  78. package/tests/unit/capture/cleanup.test.js +24 -0
  79. package/tests/unit/capture/viewport.test.js +168 -0
  80. package/tests/unit/cli/dispatcher.test.js +25 -0
  81. package/tests/unit/core/interrupted-navigation.test.js +32 -0
  82. package/tests/unit/core/readiness.test.js +21 -0
  83. package/tests/unit/core/trigger-readiness.test.js +39 -0
  84. package/tests/unit/core/trigger-stop-race.test.js +80 -0
  85. package/tests/unit/elements/visible-count.test.js +24 -0
  86. package/tests/unit/elements/wait-navigation.test.js +26 -0
  87. package/tests/unit/interactions/navigation-timeout.test.js +39 -0
  88. package/tests/unit/interactions/overlays.test.js +58 -0
  89. package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
  90. package/tests/unit/traces/bundle.test.js +16 -0
  91. package/tests/unit/traces/issue-160-privacy.test.js +110 -0
  92. package/tests/unit/traces/issue-160-rotation.test.js +140 -0
  93. package/tests/unit/traces/network.test.js +149 -0
@@ -1,3 +1,6 @@
1
+ import { abortTraceStart } from './start-cleanup.js';
2
+ import { createTraceStop } from './stop.js';
3
+ import { openRollingBundle } from './rolling-bundle.js';
1
4
  /**
2
5
  * The trace recorder (issue #87).
3
6
  *
@@ -7,28 +10,23 @@
7
10
  * request, so every caller does not have to.
8
11
  */
9
12
 
10
- import { captureSnapshotInPage } from './page-capture.js';
13
+ import { createCheckpointRecorder } from './checkpoints.js';
11
14
  import { openTraceBundle } from './bundle.js';
12
15
  import { openTraceLinks } from './links.js';
13
16
  import { createTraceIdentity } from './identity.js';
14
17
  import { withDeadline } from './deadline.js';
15
18
  import { createMutationStream } from './mutation-stream.js';
16
19
  import { attachTimelineObservers } from './observers.js';
20
+ import { attachNetwork, validateNetworkOptions } from './network.js';
21
+ import { startRecording } from '../capture/recording.js';
22
+ import { normalizePrivacyOptions, redactValue } from './redaction.js';
17
23
  import {
18
- normalizePrivacyOptions,
19
- redactUrl,
20
- redactValue,
21
- REDACTED,
22
- } from './redaction.js';
23
- import {
24
- createManifest,
25
24
  DEFAULT_CAPTURE_TIMEOUT,
26
25
  TRACE_CHECKPOINT_REASON,
27
26
  TRACE_DROP_REASON,
28
27
  TRACE_EVENT,
29
28
  TRACE_EVENT_SOURCES,
30
29
  TRACE_MODE,
31
- TRACE_OUTCOME,
32
30
  TRACE_SCHEMA_VERSION,
33
31
  } from './schema.js';
34
32
 
@@ -92,27 +90,51 @@ function validateLinksOptions(links) {
92
90
  * @param {Object} options - Recorder options, see `commander.startTrace()`
93
91
  * @returns {Promise<Object>} The running trace
94
92
  */
93
+ function normalizeTraceOptions(options) {
94
+ return {
95
+ ...options,
96
+ page: options.page ?? options.commander?.page,
97
+ mode: options.mode ?? TRACE_MODE.CHECKPOINTS,
98
+ screenshots: options.screenshots ?? 'checkpoints',
99
+ dom: options.dom ?? {},
100
+ privacy: options.privacy ?? {},
101
+ limits: options.limits ?? {},
102
+ links: options.links ?? null,
103
+ network: options.network ?? false,
104
+ strict: options.strict ?? false,
105
+ captureTimeoutMs: options.captureTimeoutMs ?? DEFAULT_CAPTURE_TIMEOUT,
106
+ commanderVersion: options.commanderVersion ?? null,
107
+ log: options.log ?? options.commander?.log,
108
+ now: options.now ?? (() => Date.now()),
109
+ };
110
+ }
111
+
95
112
  export async function startTrace(options = {}) {
113
+ if (options.limits?.rotate) {
114
+ const { startRollingTrace } = await import('./rolling.js');
115
+ return startRollingTrace(options, startTrace);
116
+ }
96
117
  requireTraceSupport(options);
97
118
  const {
98
119
  commander,
99
- page = commander?.page,
120
+ page,
100
121
  output,
101
- mode = TRACE_MODE.CHECKPOINTS,
122
+ mode,
102
123
  initialCheckpoint,
103
- screenshots = 'checkpoints',
104
- dom = {},
124
+ screenshots,
125
+ dom,
105
126
  events,
106
- privacy = {},
107
- limits = {},
108
- links = null,
109
- strict = false,
110
- captureTimeoutMs = DEFAULT_CAPTURE_TIMEOUT,
111
- commanderVersion = null,
112
- log = commander?.log,
113
- now = () => Date.now(),
127
+ privacy,
128
+ limits,
129
+ links,
130
+ network,
131
+ strict,
132
+ captureTimeoutMs,
133
+ commanderVersion,
134
+ log,
135
+ now,
114
136
  monotonic,
115
- } = options;
137
+ } = normalizeTraceOptions(options);
116
138
 
117
139
  if (!page) {
118
140
  throw new Error('startTrace requires a page or a commander');
@@ -139,39 +161,43 @@ export async function startTrace(options = {}) {
139
161
  // added to it uninvited. Passing a string names the base checkpoint.
140
162
  const baseCheckpoint =
141
163
  initialCheckpoint ?? recorderMode === TRACE_MODE.CONTINUOUS;
142
- const baseCheckpointName =
143
- typeof baseCheckpoint === 'string' ? baseCheckpoint : 'initial';
144
164
 
145
165
  // The Links Notation export is a second view of the same records, written
146
166
  // as they are recorded (issue #94), so a run that is killed leaves a
147
167
  // readable export of everything that had happened. The bundle stays
148
168
  // authoritative: nothing is written here that is not written there first.
149
169
  let linksSink = null;
150
- const bundle = await openTraceBundle({
170
+ const bundle = await openRecorderBundle(options, {
151
171
  output,
152
172
  limits,
153
173
  strict,
154
174
  now,
155
175
  monotonic,
156
- onEvent: (event) => linksSink?.event(event),
176
+ onEvent: (event, source) => linksSink?.event(event, source),
157
177
  });
158
178
  const startedAt = new Date(now()).toISOString();
159
179
 
160
- if (links) {
161
- linksSink = await openTraceLinks({
162
- output: links.output,
163
- include: links.include,
164
- bundlePath: bundle.root,
165
- schemaVersion: TRACE_SCHEMA_VERSION,
166
- mode: recorderMode,
167
- engine,
168
- startedAt,
169
- commanderVersion,
170
- });
180
+ try {
181
+ if (links) {
182
+ linksSink = await openTraceLinks({
183
+ output: links.output,
184
+ include: links.include,
185
+ dom: links.dom,
186
+ bundlePath: bundle.root,
187
+ schemaVersion: TRACE_SCHEMA_VERSION,
188
+ mode: recorderMode,
189
+ engine,
190
+ startedAt,
191
+ commanderVersion,
192
+ });
193
+ }
194
+ } catch (error) {
195
+ await bundle.abort();
196
+ throw error;
171
197
  }
172
198
 
173
199
  let stopped = false;
174
- let checkpointIndex = 0;
200
+ const captureState = { index: 0 };
175
201
  const checkpoints = [];
176
202
 
177
203
  const note = (message) => log?.debug?.(`[trace] ${message}`);
@@ -199,7 +225,7 @@ export async function startTrace(options = {}) {
199
225
  async function evaluateInPage(fn, argument) {
200
226
  // Both engines take `(fn, arg)`; the commander's own evaluate already
201
227
  // normalizes the difference, and is used when it is available.
202
- if (commander?.evaluate) {
228
+ if (commander?.evaluate && typeof page.evaluate !== 'function') {
203
229
  return await commander.evaluate(fn, argument);
204
230
  }
205
231
  return await page.evaluate(fn, argument);
@@ -243,233 +269,213 @@ export async function startTrace(options = {}) {
243
269
  }
244
270
  }
245
271
 
246
- /**
247
- * Capture a named checkpoint.
248
- *
249
- * @param {string} name - What this moment is
250
- * @param {Object} [checkpointOptions] - `{actor, reason, screenshots}`
251
- * @returns {Promise<Object|null>} `{index, name, members}`
252
- */
253
- async function checkpoint(name, checkpointOptions = {}) {
254
- if (stopped) {
255
- throw new Error('this trace has already been stopped');
256
- }
257
-
258
- const { actor = 'automation', reason = 'checkpoint' } = checkpointOptions;
259
- const index = ++checkpointIndex;
260
-
261
- // Mutations are drained first so the batches belong to the interval that
262
- // ended here, not to the one that starts now.
263
- await mutations.drain(index - 1 > 0 ? index - 1 : 0);
264
-
265
- let captured = null;
266
- try {
267
- captured = await withDeadline(
268
- evaluateInPage(captureSnapshotInPage, {
269
- redactSelectors: privacyOptions.redactSelectors,
270
- redactAttributes: privacyOptions.redactAttributes,
271
- redacted: REDACTED,
272
- html: domOptions.html,
273
- liveControlState: domOptions.liveControlState,
274
- openShadowRoots: domOptions.openShadowRoots,
275
- maxHtmlBytes: limits.maxHtmlBytes ?? 0,
276
- }),
277
- captureTimeoutMs,
278
- 'trace checkpoint capture'
279
- );
280
- } catch (error) {
281
- await bundle.drop({
282
- reason: /closed/i.test(error.message)
283
- ? TRACE_DROP_REASON.PAGE_CLOSED
284
- : TRACE_DROP_REASON.CAPTURE_FAILED,
285
- member: `checkpoints/${index}`,
286
- detail: error.message,
287
- });
288
- }
289
-
290
- const shot = await screenshot(reason);
291
- const state = captured?.state
292
- ? redactValue(captured.state, privacyOptions)
293
- : null;
294
-
295
- const members = await bundle.writeCheckpoint({
296
- index,
297
- html: captured?.html ?? undefined,
298
- state: state ? { ...state, name, actor, reason } : undefined,
299
- screenshot: shot ?? undefined,
300
- });
301
-
302
- const entry = {
303
- index,
304
- name,
305
- actor,
306
- reason,
307
- url: state ? redactUrl(state.url, privacyOptions) : null,
308
- truncated: Boolean(captured?.truncated),
309
- members,
310
- };
311
- checkpoints.push(entry);
312
- await record(TRACE_EVENT.CHECKPOINT, entry);
313
-
314
- // The init script covers every document created from here on; this covers
315
- // a frame that was attached without one, which is cheap because installing
316
- // over a recorder that is already observing does nothing.
317
- await mutations.install();
318
-
319
- return entry;
320
- }
321
-
322
- const detachers = attachTimelineObservers({
323
- commander,
324
- page,
325
- eventSources,
272
+ const checkpoint = createCheckpointRecorder({
273
+ isStopped: () => Boolean(stopped),
274
+ captureState,
275
+ mutations,
276
+ evaluateInPage,
277
+ privacyOptions,
278
+ domOptions,
279
+ links,
280
+ limits,
281
+ captureTimeoutMs,
282
+ bundle,
283
+ screenshot,
284
+ checkpoints,
326
285
  record,
327
- note,
328
- identity,
329
- now,
330
286
  });
331
287
 
332
- // Registered before the first record, so a navigation that starts in the same
333
- // tick as the trace does is still recorded from its first mutation.
334
- const detachInitScript = await mutations.installPersistent();
335
- if (detachInitScript) {
336
- detachers.push(detachInitScript);
337
- }
338
-
339
- await record(TRACE_EVENT.TRACE_START, {
340
- mode: recorderMode,
341
- engine,
342
- dom: domOptions,
343
- events: eventSources,
344
- initialCheckpoint: Boolean(baseCheckpoint),
345
- });
346
- await mutations.install();
347
-
348
- if (baseCheckpoint) {
349
- await checkpoint(baseCheckpointName, {
350
- actor: 'recorder',
351
- reason: TRACE_CHECKPOINT_REASON.INITIAL,
288
+ let detachers = [],
289
+ detachNetwork = async () => {},
290
+ video = null;
291
+ try {
292
+ detachers = attachTimelineObservers({
293
+ commander,
294
+ page,
295
+ eventSources,
296
+ record,
297
+ note,
298
+ identity,
299
+ now,
352
300
  });
353
- }
354
-
355
- /**
356
- * Stop recording and write the manifest.
357
- *
358
- * @param {Object} [stopOptions] - `{outcome, discard, error}`
359
- * @returns {Promise<Object>} `{path, manifest, checkpoints}`
360
- */
361
- async function stop(stopOptions = {}) {
362
- if (stopped) {
363
- return stopped;
301
+ detachNetwork = attachNetwork({
302
+ page,
303
+ network,
304
+ record,
305
+ note,
306
+ privacy: privacyOptions,
307
+ });
308
+ video = options.video
309
+ ? await startRecording({
310
+ page,
311
+ engine,
312
+ ...(options.video === true ? {} : options.video),
313
+ })
314
+ : null;
315
+ let navigationCapture = Promise.resolve();
316
+ if (options.checkpointOnNavigation) {
317
+ const captureNavigation = () => {
318
+ navigationCapture = navigationCapture
319
+ .then(() =>
320
+ stopped
321
+ ? null
322
+ : checkpoint('navigation', {
323
+ reason: 'navigation',
324
+ actor: 'recorder',
325
+ })
326
+ )
327
+ .catch((error) => note(error.message));
328
+ };
329
+ page.on('load', captureNavigation);
330
+ detachers.push(() => page.off('load', captureNavigation));
364
331
  }
365
- const { discard = false, error = null } = stopOptions;
366
332
 
367
- await mutations.drain(checkpointIndex);
368
- if (error) {
369
- await record(TRACE_EVENT.PAGE_ERROR, {
370
- message: error.message ?? String(error),
371
- stack: error.stack ?? null,
372
- fatal: true,
373
- });
333
+ // Registered before the first record, so a navigation that starts in the same
334
+ // tick as the trace does is still recorded from its first mutation.
335
+ const detachInitScript = await mutations.installPersistent();
336
+ if (detachInitScript) {
337
+ detachers.push(detachInitScript);
374
338
  }
375
- await record(TRACE_EVENT.TRACE_STOP, { discarded: discard });
376
- stopped = true;
377
339
 
378
- // The documents that exist stop observing here; the engine's init-script
379
- // registration is removed with the detachers below.
380
- await mutations.stop();
340
+ await record(TRACE_EVENT.TRACE_START, {
341
+ mode: recorderMode,
342
+ engine,
343
+ dom: domOptions,
344
+ events: eventSources,
345
+ initialCheckpoint: Boolean(baseCheckpoint),
346
+ });
347
+ await mutations.install();
381
348
 
382
- for (const detach of detachers.reverse()) {
383
- try {
384
- await detach();
385
- } catch (detachError) {
386
- note(`could not detach a listener: ${detachError.message}`);
387
- }
349
+ if (baseCheckpoint) {
350
+ await checkpoint(
351
+ typeof baseCheckpoint === 'string' ? baseCheckpoint : 'initial',
352
+ {
353
+ actor: 'recorder',
354
+ reason: TRACE_CHECKPOINT_REASON.INITIAL,
355
+ }
356
+ );
388
357
  }
389
358
 
390
- const manifest = await bundle.close(
391
- createManifest({
392
- mode: recorderMode,
393
- startedAt,
394
- stoppedAt: new Date(now()).toISOString(),
395
- outcome: TRACE_OUTCOME.COMPLETE,
396
- commanderVersion,
397
- engine,
398
- dom: domOptions,
399
- events: eventSources,
400
- replay: {
401
- checkpoints: true,
402
- mutations: Boolean(domOptions.mutations),
403
- childListPositions: Boolean(domOptions.mutations),
404
- liveState:
405
- Boolean(domOptions.mutations) && domOptions.liveState !== false,
406
- identifiers: true,
407
- },
408
- privacy: {
409
- redactSelectors: privacyOptions.redactSelectors,
410
- redactAttributes: privacyOptions.redactAttributes,
411
- redactQueryParams: privacyOptions.redactQueryParams,
412
- hasCallback: Boolean(privacyOptions.redact),
413
- },
414
- limits,
415
- })
416
- );
417
-
418
- // Closed after the manifest, because the closing link reports the outcome
419
- // the manifest settled on, and the control diffs are read back out of the
420
- // finished bundle rather than kept in memory for the length of a run.
421
- if (linksSink) {
422
- await linksSink.close({ manifest, bundlePath: bundle.root });
359
+ let drainTask;
360
+ if (domOptions.mutations) {
361
+ const timer = setInterval(() => {
362
+ if (!drainTask) {
363
+ drainTask = mutations
364
+ .drain(captureState.index)
365
+ .catch((error) => note(error.message))
366
+ .finally(() => {
367
+ drainTask = null;
368
+ });
369
+ }
370
+ }, 500);
371
+ timer.unref?.();
372
+ detachers.push(async () => {
373
+ clearInterval(timer);
374
+ await drainTask;
375
+ });
423
376
  }
424
377
 
425
- const result = {
378
+ /**
379
+ * Stop recording and write the manifest.
380
+ *
381
+ * @param {Object} [stopOptions] - `{outcome, discard, error}`
382
+ * @returns {Promise<Object>} `{path, manifest, checkpoints}`
383
+ */
384
+ const stop = createTraceStop({
385
+ getStopped: () => stopped,
386
+ setStopped: (value) => {
387
+ stopped = value;
388
+ },
389
+ options,
390
+ detachers,
391
+ navigationCapture: () => navigationCapture,
392
+ video,
393
+ detachNetwork,
394
+ mutations,
395
+ captureState,
396
+ record,
397
+ note,
398
+ bundle,
399
+ recorderMode,
400
+ startedAt,
401
+ now,
402
+ commanderVersion,
403
+ engine,
404
+ domOptions,
405
+ eventSources,
406
+ privacyOptions,
407
+ limits,
408
+ linksSink,
409
+ network,
410
+ checkpoints,
411
+ });
412
+
413
+ return {
426
414
  path: bundle.root,
427
- manifest,
428
- checkpoints: [...checkpoints],
429
- problems: [...bundle.problems, ...(linksSink?.problems ?? [])],
415
+ get currentPath() {
416
+ return bundle.currentPath ?? bundle.root;
417
+ },
418
+ get segments() {
419
+ return bundle.segments;
420
+ },
430
421
  links: linksSink?.path ?? null,
422
+ mode: recorderMode,
423
+ get bytesWritten() {
424
+ return bundle.bytesWritten;
425
+ },
426
+ checkpoint,
427
+ /**
428
+ * Record an event a caller cares about on the same timeline.
429
+ *
430
+ * @param {string} name - What happened
431
+ * @param {Object} [data] - Details, redacted before writing
432
+ * @returns {Promise<Object|null>} The written event
433
+ */
434
+ event: (name, data = {}) =>
435
+ record(TRACE_EVENT.INTERACTION, {
436
+ action: name,
437
+ actor: 'caller',
438
+ ...data,
439
+ }),
440
+ stop,
441
+ get stopped() {
442
+ return Boolean(stopped);
443
+ },
444
+ get checkpoints() {
445
+ return [...checkpoints];
446
+ },
431
447
  };
432
- stopped = result;
433
-
434
- if (discard) {
435
- const fs = await import('node:fs/promises');
436
- await fs.rm(bundle.root, { recursive: true, force: true });
437
- // An export of a bundle that no longer exists points at nothing.
438
- await linksSink?.discard();
439
- result.discarded = true;
440
- }
441
-
442
- return result;
448
+ } catch (error) {
449
+ await abortTraceStart({
450
+ detachers,
451
+ detachNetwork,
452
+ video,
453
+ mutations,
454
+ linksSink,
455
+ bundle,
456
+ });
457
+ throw error;
443
458
  }
444
-
445
- return {
446
- path: bundle.root,
447
- links: linksSink?.path ?? null,
448
- mode: recorderMode,
449
- checkpoint,
450
- /**
451
- * Record an event a caller cares about on the same timeline.
452
- *
453
- * @param {string} name - What happened
454
- * @param {Object} [data] - Details, redacted before writing
455
- * @returns {Promise<Object|null>} The written event
456
- */
457
- event: (name, data = {}) =>
458
- record(TRACE_EVENT.INTERACTION, {
459
- action: name,
460
- actor: 'caller',
461
- ...data,
462
- }),
463
- stop,
464
- get stopped() {
465
- return Boolean(stopped);
466
- },
467
- get checkpoints() {
468
- return [...checkpoints];
469
- },
470
- };
471
459
  }
472
460
 
473
461
  function requireTraceSupport(options) {
462
+ validateNetworkOptions(options.network);
474
463
  (options.page ?? options.commander?.page)?.requireFeature?.('tracing');
475
464
  }
465
+
466
+ function openRecorderBundle(options, settings) {
467
+ const writer =
468
+ options.openBundle ??
469
+ (options.mode === TRACE_MODE.CONTINUOUS
470
+ ? (config) =>
471
+ openRollingBundle(
472
+ config,
473
+ {
474
+ lazy: true,
475
+ maxBytes: settings.limits.maxBundleBytes ?? 256 * 1024 * 1024,
476
+ },
477
+ options
478
+ )
479
+ : openTraceBundle);
480
+ return writer(settings);
481
+ }