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.
- package/.jscpd-baseline.json +5 -19
- package/CHANGELOG.md +22 -0
- package/README.md +23 -9
- package/eslint.config.js +11 -0
- package/package.json +5 -2
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +215 -36
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +26 -1
- package/src/browser/persistent-session.js +432 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +7 -2
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-adoption.js +96 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +82 -0
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +69 -1
- package/src/cli/commands.js +116 -2
- package/src/cli/high-level-methods.js +47 -3
- package/src/cli/sessions.js +14 -3
- package/src/core/commander-features.js +110 -0
- package/src/core/constants.js +1 -0
- package/src/core/navigation-manager.js +4 -2
- package/src/core/navigation-readiness.js +30 -2
- package/src/core/navigation-safety.js +52 -0
- package/src/core/page-trigger-manager.js +280 -156
- package/src/core/readiness.js +10 -2
- package/src/elements/locators.js +26 -32
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +32 -10
- package/src/factory.js +13 -12
- package/src/index.js +4 -0
- package/src/interactions/click.js +44 -0
- package/src/interactions/overlays.js +73 -0
- package/src/traces/bundle.js +55 -8
- package/src/traces/checkpoints.js +103 -0
- package/src/traces/links.js +157 -2
- package/src/traces/mutation-stream.js +9 -2
- package/src/traces/network.js +309 -0
- package/src/traces/page-capture.js +203 -68
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +253 -247
- package/src/traces/redaction.js +89 -6
- package/src/traces/render.js +168 -0
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +77 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +160 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +32 -0
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/e2e/issue-160.e2e.test.js +291 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -0
- package/tests/unit/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/interactions/overlays.test.js +58 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/issue-160-privacy.test.js +110 -0
- package/tests/unit/traces/issue-160-rotation.test.js +140 -0
- package/tests/unit/traces/network.test.js +149 -0
package/src/traces/recorder.js
CHANGED
|
@@ -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 {
|
|
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
|
|
120
|
+
page,
|
|
100
121
|
output,
|
|
101
|
-
mode
|
|
122
|
+
mode,
|
|
102
123
|
initialCheckpoint,
|
|
103
|
-
screenshots
|
|
104
|
-
dom
|
|
124
|
+
screenshots,
|
|
125
|
+
dom,
|
|
105
126
|
events,
|
|
106
|
-
privacy
|
|
107
|
-
limits
|
|
108
|
-
links
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
detachers
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
}
|