browser-commander 0.28.0 → 0.30.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 +7 -20
- package/CHANGELOG.md +12 -0
- package/README.md +27 -10
- package/package.json +1 -1
- package/scripts/run-tests.mjs +3 -1
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +140 -65
- package/src/browser/launcher.js +19 -8
- package/src/browser/persistent-session.js +110 -38
- package/src/browser/session-adoption.js +96 -0
- package/src/browser/session-worker.js +12 -0
- package/src/cli/args.js +3 -0
- package/src/cli/commands.js +3 -0
- package/src/cli/high-level-methods.js +7 -0
- package/src/cli/sessions.js +3 -0
- package/src/core/commander-features.js +33 -25
- package/src/core/navigation-safety.js +1 -0
- package/src/core/page-trigger-manager.js +173 -134
- package/src/index.js +2 -0
- package/src/interactions/overlays.js +59 -14
- package/src/traces/bundle.js +41 -3
- package/src/traces/checkpoints.js +7 -3
- package/src/traces/commander-actions.js +146 -0
- package/src/traces/debug-session.js +119 -0
- package/src/traces/drain-timer.js +25 -0
- package/src/traces/links.js +44 -5
- package/src/traces/mutation-stream.js +12 -5
- package/src/traces/network.js +65 -13
- package/src/traces/observers.js +50 -46
- package/src/traces/page-capture.js +65 -21
- package/src/traces/recorder.js +79 -28
- package/src/traces/redaction.js +89 -6
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +10 -113
- package/src/traces/stop.js +1 -0
- package/src/traces/user-actions.js +405 -0
- package/tests/e2e/issue-160.e2e.test.js +291 -0
- package/tests/e2e/issue-162-recording.e2e.test.js +175 -0
- package/tests/e2e/traces.e2e.test.js +4 -1
- 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 +4 -4
- 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 +4 -4
- package/tests/fixtures/traces/conformance/scenario.json +1 -0
- package/tests/helpers/e2e-browser.js +2 -1
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- package/tests/unit/core/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -0
- package/tests/unit/interactions/overlays.test.js +58 -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/issue-162-defaults.test.js +223 -0
- package/tests/unit/traces/issue-162-user-actions.test.js +181 -0
- package/tests/unit/traces/network.test.js +55 -3
- package/tests/unit/traces/recorder.test.js +4 -1
|
@@ -182,6 +182,23 @@ export function createPageTriggerManager(options = {}) {
|
|
|
182
182
|
let isActionRunning = false,
|
|
183
183
|
isStopping = false;
|
|
184
184
|
let startQueue = Promise.resolve();
|
|
185
|
+
let pendingStart = null;
|
|
186
|
+
let destroyed = false;
|
|
187
|
+
|
|
188
|
+
function drainPending() {
|
|
189
|
+
if (destroyed || isActionRunning || isStopping || !pendingStart) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const pending = pendingStart;
|
|
193
|
+
pendingStart = null;
|
|
194
|
+
if (
|
|
195
|
+
triggers.includes(pending.trigger) &&
|
|
196
|
+
(!pending.commander.page?.url ||
|
|
197
|
+
pending.commander.page.url() === pending.url)
|
|
198
|
+
) {
|
|
199
|
+
void startAction(pending.url, pending.commander, pending.readyOn);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
185
202
|
|
|
186
203
|
/**
|
|
187
204
|
* Register a page trigger
|
|
@@ -208,7 +225,7 @@ export function createPageTriggerManager(options = {}) {
|
|
|
208
225
|
) {
|
|
209
226
|
throw new RangeError(`Unknown trigger readyOn: ${readyOn}`);
|
|
210
227
|
}
|
|
211
|
-
if (!['skip', 'restart'].includes(concurrency)) {
|
|
228
|
+
if (!['skip', 'restart', 'queue'].includes(concurrency)) {
|
|
212
229
|
throw new RangeError(`Unknown trigger concurrency: ${concurrency}`);
|
|
213
230
|
}
|
|
214
231
|
|
|
@@ -242,6 +259,9 @@ export function createPageTriggerManager(options = {}) {
|
|
|
242
259
|
const index = triggers.indexOf(triggerConfig);
|
|
243
260
|
if (index !== -1) {
|
|
244
261
|
triggers.splice(index, 1);
|
|
262
|
+
if (pendingStart?.trigger === triggerConfig) {
|
|
263
|
+
pendingStart = null;
|
|
264
|
+
}
|
|
245
265
|
log.debug(() => `📋 Unregistered page trigger: "${name}"`);
|
|
246
266
|
}
|
|
247
267
|
};
|
|
@@ -298,7 +318,7 @@ export function createPageTriggerManager(options = {}) {
|
|
|
298
318
|
}
|
|
299
319
|
|
|
300
320
|
// Wait for action to finish (with timeout)
|
|
301
|
-
const timeoutMs = 10000;
|
|
321
|
+
const timeoutMs = options.stopGraceMs ?? 10000;
|
|
302
322
|
let stopTimer;
|
|
303
323
|
const timeoutPromise = new Promise((resolve) => {
|
|
304
324
|
stopTimer = setTimeout(() => {
|
|
@@ -337,6 +357,7 @@ export function createPageTriggerManager(options = {}) {
|
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
log.debug(() => '✅ Action stopped');
|
|
360
|
+
drainPending();
|
|
340
361
|
}
|
|
341
362
|
|
|
342
363
|
/**
|
|
@@ -345,6 +366,9 @@ export function createPageTriggerManager(options = {}) {
|
|
|
345
366
|
* @param {Object} commander - BrowserCommander instance
|
|
346
367
|
*/
|
|
347
368
|
function startAction(url, commander, readyOn = 'networkidle') {
|
|
369
|
+
if (destroyed) {
|
|
370
|
+
return Promise.resolve();
|
|
371
|
+
}
|
|
348
372
|
// Create context for condition checking
|
|
349
373
|
const conditionCtx = {
|
|
350
374
|
url,
|
|
@@ -359,14 +383,18 @@ export function createPageTriggerManager(options = {}) {
|
|
|
359
383
|
return Promise.resolve();
|
|
360
384
|
}
|
|
361
385
|
if (isActionRunning) {
|
|
362
|
-
if (
|
|
386
|
+
if (
|
|
387
|
+
matchingTrigger.concurrency === 'skip' &&
|
|
388
|
+
!isStopping &&
|
|
389
|
+
!currentAbortController?.signal.aborted
|
|
390
|
+
) {
|
|
363
391
|
return Promise.resolve();
|
|
364
392
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
393
|
+
// One latest start bounds memory while preserving readiness during stop.
|
|
394
|
+
pendingStart = { url, commander, readyOn, trigger: matchingTrigger };
|
|
395
|
+
return matchingTrigger.concurrency === 'restart'
|
|
396
|
+
? stopCurrentAction()
|
|
397
|
+
: Promise.resolve();
|
|
370
398
|
}
|
|
371
399
|
|
|
372
400
|
log.debug(() => `🚀 Starting action "${matchingTrigger.name}" for: ${url}`);
|
|
@@ -377,12 +405,15 @@ export function createPageTriggerManager(options = {}) {
|
|
|
377
405
|
isActionRunning = true;
|
|
378
406
|
|
|
379
407
|
// Create action context
|
|
380
|
-
const context = createActionContext(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
408
|
+
const context = createActionContext(
|
|
409
|
+
{
|
|
410
|
+
url,
|
|
411
|
+
abortSignal: currentAbortController.signal,
|
|
412
|
+
commander,
|
|
413
|
+
triggerName: matchingTrigger.name,
|
|
414
|
+
},
|
|
415
|
+
log
|
|
416
|
+
);
|
|
386
417
|
|
|
387
418
|
// Run action
|
|
388
419
|
actionPromise = (async () => {
|
|
@@ -411,6 +442,7 @@ export function createPageTriggerManager(options = {}) {
|
|
|
411
442
|
isActionRunning = false;
|
|
412
443
|
currentTrigger = null;
|
|
413
444
|
currentAbortController = null;
|
|
445
|
+
globalThis.queueMicrotask(drainPending);
|
|
414
446
|
}
|
|
415
447
|
}
|
|
416
448
|
})();
|
|
@@ -423,130 +455,12 @@ export function createPageTriggerManager(options = {}) {
|
|
|
423
455
|
* @param {Object} options
|
|
424
456
|
* @returns {Object} - Action context
|
|
425
457
|
*/
|
|
426
|
-
function createActionContext(options) {
|
|
427
|
-
const { url, abortSignal, commander, triggerName } = options;
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Check if stopped and throw if so
|
|
431
|
-
*/
|
|
432
|
-
function checkStopped() {
|
|
433
|
-
if (abortSignal.aborted) {
|
|
434
|
-
throw new ActionStoppedError(`Action "${triggerName}" stopped`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Wrap async function to check abort before and after
|
|
440
|
-
*/
|
|
441
|
-
function wrapAsync(fn) {
|
|
442
|
-
return async (...args) => {
|
|
443
|
-
checkStopped();
|
|
444
|
-
const result = await fn(...args);
|
|
445
|
-
checkStopped();
|
|
446
|
-
return result;
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Create abort-aware loop helper
|
|
452
|
-
* Use this instead of for/while loops for stoppability
|
|
453
|
-
*/
|
|
454
|
-
async function forEach(items, callback) {
|
|
455
|
-
for (let i = 0; i < items.length; i++) {
|
|
456
|
-
checkStopped();
|
|
457
|
-
await callback(items[i], i, items);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Wait with abort support
|
|
463
|
-
*/
|
|
464
|
-
async function wait(ms) {
|
|
465
|
-
checkStopped();
|
|
466
|
-
await new Promise((resolve, reject) => {
|
|
467
|
-
const timeout = setTimeout(resolve, ms);
|
|
468
|
-
abortSignal.addEventListener(
|
|
469
|
-
'abort',
|
|
470
|
-
() => {
|
|
471
|
-
clearTimeout(timeout);
|
|
472
|
-
reject(new ActionStoppedError());
|
|
473
|
-
},
|
|
474
|
-
{ once: true }
|
|
475
|
-
);
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Register cleanup callback (called when action stops)
|
|
481
|
-
*/
|
|
482
|
-
const cleanupCallbacks = [];
|
|
483
|
-
function onCleanup(callback) {
|
|
484
|
-
const unsubscribe = subscribeCallbacks(cleanupCallbacks, callback);
|
|
485
|
-
const onAbort = async () => {
|
|
486
|
-
try {
|
|
487
|
-
await callback();
|
|
488
|
-
} catch (e) {
|
|
489
|
-
log.debug(() => `⚠️ Cleanup error: ${e.message}`);
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
493
|
-
return () => {
|
|
494
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
495
|
-
unsubscribe();
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Wrap all commander methods to be abort-aware
|
|
500
|
-
const wrappedCommander = {};
|
|
501
|
-
for (const [key, value] of Object.entries(commander)) {
|
|
502
|
-
if (
|
|
503
|
-
typeof value === 'function' &&
|
|
504
|
-
key !== 'destroy' &&
|
|
505
|
-
key !== 'pageTrigger'
|
|
506
|
-
) {
|
|
507
|
-
wrappedCommander[key] = wrapAsync(value);
|
|
508
|
-
} else {
|
|
509
|
-
wrappedCommander[key] = value;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
// URL this action is running for
|
|
515
|
-
url,
|
|
516
|
-
|
|
517
|
-
// Abort signal - use with fetch() or custom abort logic
|
|
518
|
-
abortSignal,
|
|
519
|
-
|
|
520
|
-
// Check if action should stop
|
|
521
|
-
isStopped: () => abortSignal.aborted,
|
|
522
|
-
|
|
523
|
-
// Throw if stopped - call this in loops
|
|
524
|
-
checkStopped,
|
|
525
|
-
|
|
526
|
-
// Abort-aware iteration helper
|
|
527
|
-
forEach,
|
|
528
|
-
|
|
529
|
-
// Abort-aware wait
|
|
530
|
-
wait,
|
|
531
|
-
|
|
532
|
-
// Register cleanup callback
|
|
533
|
-
onCleanup,
|
|
534
|
-
|
|
535
|
-
// Wrapped commander - all methods throw ActionStoppedError if stopped
|
|
536
|
-
commander: wrappedCommander,
|
|
537
|
-
|
|
538
|
-
// Original commander (use carefully)
|
|
539
|
-
rawCommander: commander,
|
|
540
|
-
|
|
541
|
-
// Trigger name for debugging
|
|
542
|
-
triggerName,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
458
|
|
|
546
459
|
/**
|
|
547
460
|
* Handle navigation start - stop current action first
|
|
548
461
|
*/
|
|
549
462
|
async function onNavigationStart() {
|
|
463
|
+
pendingStart = null;
|
|
550
464
|
await stopCurrentAction();
|
|
551
465
|
}
|
|
552
466
|
|
|
@@ -586,13 +500,18 @@ export function createPageTriggerManager(options = {}) {
|
|
|
586
500
|
/**
|
|
587
501
|
* Cleanup
|
|
588
502
|
*/
|
|
589
|
-
const
|
|
503
|
+
const cleanup = createTriggerDestroy({
|
|
590
504
|
subscriptions,
|
|
591
505
|
getQueue: () => startQueue,
|
|
592
506
|
stopCurrentAction,
|
|
593
507
|
triggers,
|
|
594
508
|
log,
|
|
595
509
|
});
|
|
510
|
+
const destroy = async () => {
|
|
511
|
+
destroyed = true;
|
|
512
|
+
pendingStart = null;
|
|
513
|
+
await cleanup();
|
|
514
|
+
};
|
|
596
515
|
|
|
597
516
|
return {
|
|
598
517
|
pageTrigger,
|
|
@@ -652,3 +571,123 @@ function createTriggerDestroy({
|
|
|
652
571
|
log.debug(() => '📋 PageTriggerManager destroyed');
|
|
653
572
|
};
|
|
654
573
|
}
|
|
574
|
+
|
|
575
|
+
function createActionContext(options, log) {
|
|
576
|
+
const { url, abortSignal, commander, triggerName } = options;
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Check if stopped and throw if so
|
|
580
|
+
*/
|
|
581
|
+
function checkStopped() {
|
|
582
|
+
if (abortSignal.aborted) {
|
|
583
|
+
throw new ActionStoppedError(`Action "${triggerName}" stopped`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Wrap async function to check abort before and after
|
|
589
|
+
*/
|
|
590
|
+
function wrapAsync(fn) {
|
|
591
|
+
return async (...args) => {
|
|
592
|
+
checkStopped();
|
|
593
|
+
const result = await fn(...args);
|
|
594
|
+
checkStopped();
|
|
595
|
+
return result;
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Create abort-aware loop helper
|
|
601
|
+
* Use this instead of for/while loops for stoppability
|
|
602
|
+
*/
|
|
603
|
+
async function forEach(items, callback) {
|
|
604
|
+
for (let i = 0; i < items.length; i++) {
|
|
605
|
+
checkStopped();
|
|
606
|
+
await callback(items[i], i, items);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Wait with abort support
|
|
612
|
+
*/
|
|
613
|
+
async function wait(ms) {
|
|
614
|
+
checkStopped();
|
|
615
|
+
await new Promise((resolve, reject) => {
|
|
616
|
+
const timeout = setTimeout(resolve, ms);
|
|
617
|
+
abortSignal.addEventListener(
|
|
618
|
+
'abort',
|
|
619
|
+
() => {
|
|
620
|
+
clearTimeout(timeout);
|
|
621
|
+
reject(new ActionStoppedError());
|
|
622
|
+
},
|
|
623
|
+
{ once: true }
|
|
624
|
+
);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Register cleanup callback (called when action stops)
|
|
630
|
+
*/
|
|
631
|
+
const cleanupCallbacks = [];
|
|
632
|
+
function onCleanup(callback) {
|
|
633
|
+
const unsubscribe = subscribeCallbacks(cleanupCallbacks, callback);
|
|
634
|
+
const onAbort = async () => {
|
|
635
|
+
try {
|
|
636
|
+
await callback();
|
|
637
|
+
} catch (e) {
|
|
638
|
+
log.debug(() => `⚠️ Cleanup error: ${e.message}`);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
642
|
+
return () => {
|
|
643
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
644
|
+
unsubscribe();
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Wrap all commander methods to be abort-aware
|
|
649
|
+
const wrappedCommander = {};
|
|
650
|
+
for (const [key, value] of Object.entries(commander)) {
|
|
651
|
+
if (
|
|
652
|
+
typeof value === 'function' &&
|
|
653
|
+
key !== 'destroy' &&
|
|
654
|
+
key !== 'pageTrigger'
|
|
655
|
+
) {
|
|
656
|
+
wrappedCommander[key] = wrapAsync(value);
|
|
657
|
+
} else {
|
|
658
|
+
wrappedCommander[key] = value;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
// URL this action is running for
|
|
664
|
+
url,
|
|
665
|
+
|
|
666
|
+
// Abort signal - use with fetch() or custom abort logic
|
|
667
|
+
abortSignal,
|
|
668
|
+
|
|
669
|
+
// Check if action should stop
|
|
670
|
+
isStopped: () => abortSignal.aborted,
|
|
671
|
+
|
|
672
|
+
// Throw if stopped - call this in loops
|
|
673
|
+
checkStopped,
|
|
674
|
+
|
|
675
|
+
// Abort-aware iteration helper
|
|
676
|
+
forEach,
|
|
677
|
+
|
|
678
|
+
// Abort-aware wait
|
|
679
|
+
wait,
|
|
680
|
+
|
|
681
|
+
// Register cleanup callback
|
|
682
|
+
onCleanup,
|
|
683
|
+
|
|
684
|
+
// Wrapped commander - all methods throw ActionStoppedError if stopped
|
|
685
|
+
commander: wrappedCommander,
|
|
686
|
+
|
|
687
|
+
// Original commander (use carefully)
|
|
688
|
+
rawCommander: commander,
|
|
689
|
+
|
|
690
|
+
// Trigger name for debugging
|
|
691
|
+
triggerName,
|
|
692
|
+
};
|
|
693
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,28 +1,73 @@
|
|
|
1
|
+
import { withDeadline } from '../traces/deadline.js';
|
|
1
2
|
import { isVisible } from '../elements/visibility.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
async function dismissVisible({ page, engine, selector, timeout = 1000 }) {
|
|
5
|
+
if (engine === 'playwright') {
|
|
6
|
+
const matches = page.locator(selector);
|
|
7
|
+
for (let index = 0; index < (await matches.count()); index++) {
|
|
8
|
+
const target = matches.nth(index);
|
|
9
|
+
if (await target.isVisible()) {
|
|
10
|
+
await target.click({ timeout });
|
|
11
|
+
return 'dismissed';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return 'absent';
|
|
15
|
+
}
|
|
16
|
+
const matches = await page.$$(selector);
|
|
17
|
+
try {
|
|
18
|
+
for (const target of matches) {
|
|
19
|
+
if (await isVisible({ page, engine, selector: target })) {
|
|
20
|
+
await target.click({ timeout });
|
|
21
|
+
return 'dismissed';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return 'absent';
|
|
25
|
+
} finally {
|
|
26
|
+
await Promise.allSettled(
|
|
27
|
+
matches.map(async (target) => await target.dispose?.())
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Dismiss caller-named overlays; a failed dismissal never blocks an action. */
|
|
33
|
+
export async function dismissOverlays({
|
|
34
|
+
page,
|
|
35
|
+
engine,
|
|
36
|
+
overlays = [],
|
|
37
|
+
onDismiss,
|
|
38
|
+
}) {
|
|
5
39
|
if (!Array.isArray(overlays)) {
|
|
6
40
|
throw new TypeError('overlays must be an array');
|
|
7
41
|
}
|
|
42
|
+
const reports = [];
|
|
8
43
|
for (const item of overlays) {
|
|
9
44
|
const options = typeof item === 'string' ? { selector: item } : item;
|
|
10
45
|
if (!options?.selector) {
|
|
11
46
|
throw new TypeError('overlay requires selector');
|
|
12
47
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
48
|
+
const report = { selector: options.selector, status: 'absent' };
|
|
49
|
+
try {
|
|
50
|
+
report.status = await withDeadline(
|
|
51
|
+
dismissVisible({ page, engine, ...options }),
|
|
52
|
+
options.timeout || 1000,
|
|
53
|
+
`overlay ${options.selector}`
|
|
54
|
+
);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
report.status = 'error';
|
|
57
|
+
report.error = error;
|
|
58
|
+
}
|
|
59
|
+
reports.push(report);
|
|
60
|
+
for (const callback of [options.onDismiss, onDismiss]) {
|
|
61
|
+
try {
|
|
62
|
+
await withDeadline(
|
|
63
|
+
Promise.resolve(callback?.(report)),
|
|
64
|
+
options.timeout || 1000,
|
|
65
|
+
'overlay report'
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
// Reporting must not interrupt the caller's requested interaction.
|
|
25
69
|
}
|
|
26
70
|
}
|
|
27
71
|
}
|
|
72
|
+
return reports;
|
|
28
73
|
}
|
package/src/traces/bundle.js
CHANGED
|
@@ -43,6 +43,7 @@ export async function openTraceBundle(options = {}) {
|
|
|
43
43
|
now = () => Date.now(),
|
|
44
44
|
monotonic = () => performance.now(),
|
|
45
45
|
onEvent = null,
|
|
46
|
+
nextSequence = null,
|
|
46
47
|
} = options;
|
|
47
48
|
|
|
48
49
|
if (typeof output !== 'string' || output === '') {
|
|
@@ -70,6 +71,9 @@ export async function openTraceBundle(options = {}) {
|
|
|
70
71
|
let dropped = 0;
|
|
71
72
|
const counts = { checkpoints: 0, events: 0, mutationBatches: 0 };
|
|
72
73
|
const problems = [];
|
|
74
|
+
let mutationIndex = null;
|
|
75
|
+
let mutationBytes = 0;
|
|
76
|
+
let mutationTruncated = false;
|
|
73
77
|
|
|
74
78
|
/** Close the timeline exactly once, after every queued append. */
|
|
75
79
|
function closeEvents() {
|
|
@@ -117,11 +121,13 @@ export async function openTraceBundle(options = {}) {
|
|
|
117
121
|
* @returns {Promise<Object|null>} The event as written
|
|
118
122
|
*/
|
|
119
123
|
async function appendEvent(event, { retry = true } = {}) {
|
|
124
|
+
const number = nextSequence?.() ?? ++sequence;
|
|
120
125
|
const record = {
|
|
121
|
-
sequence:
|
|
126
|
+
sequence: number,
|
|
122
127
|
at: new Date(now()).toISOString(),
|
|
123
128
|
monotonicMs: Math.round(monotonic()),
|
|
124
129
|
...event,
|
|
130
|
+
...(nextSequence ? { sequence: number } : {}),
|
|
125
131
|
};
|
|
126
132
|
const line = `${JSON.stringify(record)}\n`;
|
|
127
133
|
const bytes = Buffer.byteLength(line);
|
|
@@ -275,10 +281,39 @@ export async function openTraceBundle(options = {}) {
|
|
|
275
281
|
return null;
|
|
276
282
|
}
|
|
277
283
|
const member = `${TRACE_FILES.MUTATIONS_DIR}/${sequenceName(index)}.ndjson`;
|
|
278
|
-
|
|
284
|
+
if (mutationIndex !== index) {
|
|
285
|
+
mutationIndex = index;
|
|
286
|
+
mutationBytes = 0;
|
|
287
|
+
mutationTruncated = false;
|
|
288
|
+
}
|
|
289
|
+
if (mutationTruncated) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const ceiling = Math.min(
|
|
293
|
+
limits.maxMutationBytes ?? 4 * 1024 * 1024,
|
|
294
|
+
maxResourceBytes
|
|
295
|
+
);
|
|
296
|
+
const lines = [];
|
|
297
|
+
for (const batch of batches) {
|
|
298
|
+
const line = `${JSON.stringify(batch)}\n`;
|
|
299
|
+
if (mutationBytes + Buffer.byteLength(line) + 256 > ceiling) {
|
|
300
|
+
mutationTruncated = true;
|
|
301
|
+
const marker = `${JSON.stringify({ records: [{ kind: 'truncated', reason: 'mutation interval size limit' }] })}\n`;
|
|
302
|
+
if (mutationBytes + Buffer.byteLength(marker) <= ceiling) {
|
|
303
|
+
lines.push(marker);
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
lines.push(line);
|
|
308
|
+
mutationBytes += Buffer.byteLength(line);
|
|
309
|
+
}
|
|
310
|
+
if (!lines.length) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const body = lines.join('');
|
|
279
314
|
const result = await writeMember(member, body, { append: true });
|
|
280
315
|
if (result) {
|
|
281
|
-
counts.mutationBatches +=
|
|
316
|
+
counts.mutationBatches += lines.length;
|
|
282
317
|
}
|
|
283
318
|
return result ? result.member : null;
|
|
284
319
|
}
|
|
@@ -319,6 +354,9 @@ export async function openTraceBundle(options = {}) {
|
|
|
319
354
|
get bytesWritten() {
|
|
320
355
|
return written;
|
|
321
356
|
},
|
|
357
|
+
get mutationTruncated() {
|
|
358
|
+
return mutationTruncated;
|
|
359
|
+
},
|
|
322
360
|
appendEvent,
|
|
323
361
|
writeMember,
|
|
324
362
|
writeCheckpoint,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { captureSnapshotInPage } from './page-capture.js';
|
|
2
2
|
import { withDeadline } from './deadline.js';
|
|
3
|
-
import { redactValue, redactUrl, REDACTED } from './redaction.js';
|
|
3
|
+
import { redactText, redactValue, redactUrl, REDACTED } from './redaction.js';
|
|
4
4
|
import { TRACE_EVENT, TRACE_DROP_REASON } from './schema.js';
|
|
5
5
|
export function createCheckpointRecorder({
|
|
6
6
|
isStopped,
|
|
@@ -42,13 +42,14 @@ export function createCheckpointRecorder({
|
|
|
42
42
|
evaluateInPage(captureSnapshotInPage, {
|
|
43
43
|
redactSelectors: privacyOptions.redactSelectors,
|
|
44
44
|
redactAttributes: privacyOptions.redactAttributes,
|
|
45
|
+
useDefaults: privacyOptions.useDefaults,
|
|
45
46
|
redacted: REDACTED,
|
|
46
47
|
html: domOptions.html,
|
|
47
48
|
liveControlState: domOptions.liveControlState,
|
|
48
49
|
openShadowRoots: domOptions.openShadowRoots,
|
|
49
50
|
ignoreSelectors: domOptions.ignoreSelectors ?? [],
|
|
50
51
|
captureText: links?.dom === 'text',
|
|
51
|
-
maxHtmlBytes: limits.maxHtmlBytes ??
|
|
52
|
+
maxHtmlBytes: limits.maxHtmlBytes ?? 4 * 1024 * 1024,
|
|
52
53
|
}),
|
|
53
54
|
captureTimeoutMs,
|
|
54
55
|
'trace checkpoint capture'
|
|
@@ -70,7 +71,10 @@ export function createCheckpointRecorder({
|
|
|
70
71
|
|
|
71
72
|
const members = await bundle.writeCheckpoint({
|
|
72
73
|
index,
|
|
73
|
-
html: captured?.html
|
|
74
|
+
html: redactText(captured?.html, privacyOptions, {
|
|
75
|
+
kind: 'dom',
|
|
76
|
+
name: 'html',
|
|
77
|
+
}),
|
|
74
78
|
state: state ? { ...state, name, actor, reason } : undefined,
|
|
75
79
|
screenshot: shot ?? undefined,
|
|
76
80
|
});
|