browser-commander 0.29.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 +2 -1
- package/CHANGELOG.md +6 -0
- package/README.md +7 -0
- package/package.json +1 -1
- package/scripts/run-tests.mjs +3 -1
- package/src/browser/launcher.js +19 -8
- package/src/core/commander-features.js +27 -24
- 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/mutation-stream.js +4 -3
- package/src/traces/observers.js +50 -46
- package/src/traces/recorder.js +46 -25
- package/src/traces/user-actions.js +405 -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/events.ndjson +2 -2
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +2 -2
- package/tests/fixtures/traces/conformance/scenario.json +1 -0
- package/tests/helpers/e2e-browser.js +2 -1
- 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/recorder.test.js +4 -1
package/.jscpd-baseline.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"326f1ff9016e1e37": 1,
|
|
59
59
|
"33357ce94e07cc4b": 1,
|
|
60
60
|
"35cf2769645f3234": 1,
|
|
61
|
+
"364e8930fe8fdf71": 1,
|
|
61
62
|
"37bd4114d0089f49": 1,
|
|
62
63
|
"3adca52a3f04837c": 1,
|
|
63
64
|
"3c1017def7b3216c": 1,
|
|
@@ -167,6 +168,7 @@
|
|
|
167
168
|
"a36f66312d5bfbf8": 1,
|
|
168
169
|
"a4c8498d4f2d2191": 1,
|
|
169
170
|
"a5298242b5439858": 1,
|
|
171
|
+
"a5861532b8efced8": 1,
|
|
170
172
|
"a6ad7ab6c05656dc": 1,
|
|
171
173
|
"a6ffab1aacd00af6": 1,
|
|
172
174
|
"a71a4b720d466e32": 1,
|
|
@@ -210,7 +212,6 @@
|
|
|
210
212
|
"c714beebc66381dd": 1,
|
|
211
213
|
"c9cf3c28bed33a5b": 1,
|
|
212
214
|
"ca7e08d1eec2ad8c": 1,
|
|
213
|
-
"cab590a1d04b5a05": 1,
|
|
214
215
|
"cbd444e9a6a9072e": 1,
|
|
215
216
|
"cebff7d1cc87f646": 1,
|
|
216
217
|
"cfc928aa393146b6": 1,
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0abb9c9: Enable complete JavaScript session recording by default for commanders, launches, and explicit traces. Record trusted user actions with private target descriptions alongside commander actions, DOM changes, network metadata, and a self-contained trace.lino export. Use debug: false or BROWSER_COMMANDER_DEBUG=0 to opt out.
|
|
8
|
+
|
|
3
9
|
## 0.29.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -816,6 +816,13 @@ See [capture, debugging and reusable sessions](https://github.com/link-foundatio
|
|
|
816
816
|
for screenshots, recordings, full/text DOM links, network/HAR, rotation,
|
|
817
817
|
persistent browsers, early trigger readiness and engine capability limits.
|
|
818
818
|
|
|
819
|
+
JavaScript commanders and browser launches record complete sessions by default
|
|
820
|
+
under `./browser-commander-traces/`, including trusted user actions, DOM changes,
|
|
821
|
+
network metadata and `trace.lino`. Await `commander.ready` before using its raw
|
|
822
|
+
page, and call `commander.destroy()` or the launch's `close()` to finish the files.
|
|
823
|
+
Use `debug: false` or `BROWSER_COMMANDER_DEBUG=0` to opt out; `debug: { output }`
|
|
824
|
+
chooses the directory. `commander.tracePath` exposes its path.
|
|
825
|
+
|
|
819
826
|
`startTrace()` records a session into a schema-versioned bundle any of the three
|
|
820
827
|
languages can read - the manifest, an ordered NDJSON timeline, per-checkpoint
|
|
821
828
|
DOM snapshots and the mutation batches between them:
|
package/package.json
CHANGED
package/scripts/run-tests.mjs
CHANGED
|
@@ -177,7 +177,9 @@ function runNodeTests(options, testFiles) {
|
|
|
177
177
|
process.execPath,
|
|
178
178
|
buildNodeTestArgs(options, testFiles),
|
|
179
179
|
{
|
|
180
|
-
|
|
180
|
+
// Most fixtures exercise unrelated APIs with incomplete fake pages. The
|
|
181
|
+
// default-recording regressions explicitly unset this harness opt-out.
|
|
182
|
+
env: { BROWSER_COMMANDER_DEBUG: '0', ...process.env, ...options.env },
|
|
181
183
|
stdio: 'inherit',
|
|
182
184
|
}
|
|
183
185
|
);
|
package/src/browser/launcher.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { assertSupportedEngine } from './connector.js';
|
|
9
9
|
import { emulateMedia } from './media.js';
|
|
10
10
|
import { reserveLoopbackPort } from './debugging-port.js';
|
|
11
|
+
import { attachLaunchDebug } from '../traces/debug-session.js';
|
|
11
12
|
import {
|
|
12
13
|
buildPlaywrightLaunchOptions,
|
|
13
14
|
buildPuppeteerLaunchOptions,
|
|
@@ -319,12 +320,16 @@ export async function launchBrowserWithDependencies(
|
|
|
319
320
|
) {
|
|
320
321
|
if (options.keepOpen) {
|
|
321
322
|
const { connectOrLaunch } = await import('./persistent-session.js');
|
|
322
|
-
return
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
return attachLaunchDebug(
|
|
324
|
+
await connectOrLaunch({
|
|
325
|
+
...options,
|
|
326
|
+
userDataDir:
|
|
327
|
+
options.userDataDir ?? (await createTemporaryUserDataDir()),
|
|
328
|
+
remoteDebuggingPort:
|
|
329
|
+
options.remoteDebuggingPort ?? (await reserveLoopbackPort()),
|
|
330
|
+
}),
|
|
331
|
+
options
|
|
332
|
+
);
|
|
328
333
|
}
|
|
329
334
|
sessionPersistencePath(options);
|
|
330
335
|
if (isSafariChannel(options.channel ?? options.browser)) {
|
|
@@ -333,11 +338,17 @@ export async function launchBrowserWithDependencies(
|
|
|
333
338
|
launch: options.launch ?? 'real',
|
|
334
339
|
attach: options.attach,
|
|
335
340
|
});
|
|
336
|
-
return
|
|
341
|
+
return attachLaunchDebug(
|
|
342
|
+
await launchSafari(options, dependencies),
|
|
343
|
+
options
|
|
344
|
+
);
|
|
337
345
|
}
|
|
338
346
|
const result = await launchNonSafariBrowser(options, dependencies);
|
|
339
347
|
try {
|
|
340
|
-
return await
|
|
348
|
+
return await attachLaunchDebug(
|
|
349
|
+
await installSessionPersistence(result, options),
|
|
350
|
+
options
|
|
351
|
+
);
|
|
341
352
|
} catch (error) {
|
|
342
353
|
await result.close();
|
|
343
354
|
throw error;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { startTrace } from '../traces/recorder.js';
|
|
2
|
+
import { debugEnabled, ensureDebugSession } from '../traces/debug-session.js';
|
|
3
|
+
import { observeCommanderActions } from '../traces/commander-actions.js';
|
|
4
|
+
import { TRACED_INTERACTIONS, interactionTarget } from '../traces/observers.js';
|
|
2
5
|
import { touchSessionPage } from '../browser/persistent-session.js';
|
|
3
6
|
import { dismissOverlays } from '../interactions/overlays.js';
|
|
4
7
|
import {
|
|
@@ -15,7 +18,7 @@ const INTERACTIONS = new Set([
|
|
|
15
18
|
'pressKey',
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
|
-
/** Bind capture, persistence and
|
|
21
|
+
/** Bind capture, persistence and automatic debugging to the commander lifecycle. */
|
|
19
22
|
export function attachCommanderFeatures(commander, options, recreate) {
|
|
20
23
|
const { page, engine } = commander;
|
|
21
24
|
const managed = new Set();
|
|
@@ -46,24 +49,10 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
46
49
|
});
|
|
47
50
|
commander.startTrace = async (traceOptions = {}) =>
|
|
48
51
|
track(await startTrace({ commander, page, ...traceOptions }));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let debug;
|
|
54
|
-
const ensureDebug = async () => {
|
|
55
|
-
if (!output || (!options.debug && !process.env.BROWSER_COMMANDER_TRACE)) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
debug ??= commander.startTrace({
|
|
59
|
-
output,
|
|
60
|
-
mode: 'continuous',
|
|
61
|
-
network: { har: true },
|
|
62
|
-
links: { output: `${output}.links`, dom: 'text' },
|
|
63
|
-
checkpointOnNavigation: true,
|
|
64
|
-
});
|
|
65
|
-
await debug;
|
|
66
|
-
};
|
|
52
|
+
let debugSession;
|
|
53
|
+
let detachActions;
|
|
54
|
+
commander.tracePath = null;
|
|
55
|
+
const ensureDebug = () => commander.ready;
|
|
67
56
|
for (const name of [...INTERACTIONS, 'goto', 'evaluate', 'count']) {
|
|
68
57
|
const method = commander[name];
|
|
69
58
|
if (!method) {
|
|
@@ -81,10 +70,10 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
81
70
|
const destroy = commander.destroy;
|
|
82
71
|
commander.destroy = async () => {
|
|
83
72
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
73
|
+
await commander.ready;
|
|
74
|
+
await debugSession?.stop();
|
|
87
75
|
await Promise.all([...managed].map((item) => item.stop()));
|
|
76
|
+
detachActions?.();
|
|
88
77
|
} finally {
|
|
89
78
|
await destroy();
|
|
90
79
|
}
|
|
@@ -104,7 +93,21 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
104
93
|
touchSessionPage(selected);
|
|
105
94
|
return selected;
|
|
106
95
|
};
|
|
107
|
-
if (options
|
|
108
|
-
|
|
96
|
+
if (debugEnabled({ ...options, page })) {
|
|
97
|
+
detachActions = observeCommanderActions({
|
|
98
|
+
commander,
|
|
99
|
+
page,
|
|
100
|
+
names: TRACED_INTERACTIONS,
|
|
101
|
+
targetOf: interactionTarget,
|
|
102
|
+
});
|
|
109
103
|
}
|
|
104
|
+
commander.ready = ensureDebugSession({
|
|
105
|
+
...options,
|
|
106
|
+
page,
|
|
107
|
+
engine,
|
|
108
|
+
commander,
|
|
109
|
+
}).then((session) => {
|
|
110
|
+
debugSession = session;
|
|
111
|
+
commander.tracePath = session?.path ?? null;
|
|
112
|
+
});
|
|
110
113
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/** One wrapper per commander, even when explicit and automatic traces overlap. */
|
|
2
|
+
import { withDeadline } from './deadline.js';
|
|
3
|
+
import { framesOf } from './init-script.js';
|
|
4
|
+
|
|
5
|
+
const commanders = new WeakMap();
|
|
6
|
+
|
|
7
|
+
let actionSequence = 0;
|
|
8
|
+
|
|
9
|
+
function labelActionInPage(argument, elementOptions) {
|
|
10
|
+
const { marker, remove } = elementOptions ?? argument;
|
|
11
|
+
const actions = (window.__browserCommanderActions__ ??= []);
|
|
12
|
+
if (remove) {
|
|
13
|
+
const index = actions.findIndex((action) => action.id === marker.id);
|
|
14
|
+
if (index >= 0) {
|
|
15
|
+
actions.splice(index, 1);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
const focused =
|
|
19
|
+
!marker.selector && marker.types.includes('keydown')
|
|
20
|
+
? document.activeElement
|
|
21
|
+
: null;
|
|
22
|
+
actions.push({
|
|
23
|
+
...marker,
|
|
24
|
+
element: elementOptions
|
|
25
|
+
? argument
|
|
26
|
+
: focused !== document.body && focused !== document.documentElement
|
|
27
|
+
? focused
|
|
28
|
+
: null,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function observeCommanderActions({
|
|
34
|
+
commander,
|
|
35
|
+
page,
|
|
36
|
+
names,
|
|
37
|
+
targetOf,
|
|
38
|
+
record,
|
|
39
|
+
identity,
|
|
40
|
+
now,
|
|
41
|
+
note,
|
|
42
|
+
}) {
|
|
43
|
+
if (!commander) {
|
|
44
|
+
return () => {};
|
|
45
|
+
}
|
|
46
|
+
let state = commanders.get(commander);
|
|
47
|
+
if (!state) {
|
|
48
|
+
state = { observers: new Set(), originals: new Map() };
|
|
49
|
+
commanders.set(commander, state);
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const original = commander[name];
|
|
52
|
+
if (typeof original !== 'function') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const wrapper = async (...args) => {
|
|
56
|
+
await commander.ready;
|
|
57
|
+
const observing = [...state.observers].map((observer) => ({
|
|
58
|
+
observer,
|
|
59
|
+
started: observer.now(),
|
|
60
|
+
actionId: observer.identity?.nextActionId() ?? null,
|
|
61
|
+
}));
|
|
62
|
+
const marker = {
|
|
63
|
+
id: ++actionSequence,
|
|
64
|
+
selector: targetOf(args[0]),
|
|
65
|
+
types: /^(click|check)/.test(name)
|
|
66
|
+
? ['pointerdown', 'click', 'submit', 'contextmenu']
|
|
67
|
+
: /^(fill|typeText|pressKey)/.test(name)
|
|
68
|
+
? ['keydown', 'submit', 'click']
|
|
69
|
+
: [],
|
|
70
|
+
};
|
|
71
|
+
const element = args[0]?.locatorOrElement ?? args[0]?.selector;
|
|
72
|
+
const labelAction = async (remove) => {
|
|
73
|
+
try {
|
|
74
|
+
if (marker.types.length) {
|
|
75
|
+
const config = { marker, remove };
|
|
76
|
+
const frames =
|
|
77
|
+
remove || (!marker.selector && marker.types.includes('keydown'))
|
|
78
|
+
? framesOf(page)
|
|
79
|
+
: [];
|
|
80
|
+
const target = element?.first?.() ?? element;
|
|
81
|
+
const work = frames.length
|
|
82
|
+
? Promise.all(
|
|
83
|
+
frames.map((frame) =>
|
|
84
|
+
frame.evaluate(labelActionInPage, config)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
: !remove && typeof target?.evaluate === 'function'
|
|
88
|
+
? target.evaluate(labelActionInPage, config, {
|
|
89
|
+
timeout: 1000,
|
|
90
|
+
})
|
|
91
|
+
: page.evaluate?.(labelActionInPage, config);
|
|
92
|
+
await withDeadline(
|
|
93
|
+
Promise.resolve(work),
|
|
94
|
+
1000,
|
|
95
|
+
'commander input label'
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
note?.(`could not label commander input: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
await labelAction(false);
|
|
103
|
+
let error;
|
|
104
|
+
try {
|
|
105
|
+
return await original.apply(commander, args);
|
|
106
|
+
} catch (failure) {
|
|
107
|
+
error = failure;
|
|
108
|
+
throw failure;
|
|
109
|
+
} finally {
|
|
110
|
+
await labelAction(true);
|
|
111
|
+
await Promise.all(
|
|
112
|
+
observing.map(({ observer, started, actionId }) =>
|
|
113
|
+
observer.record('interaction', {
|
|
114
|
+
actionId,
|
|
115
|
+
source: 'commander',
|
|
116
|
+
action: name,
|
|
117
|
+
target: targetOf(args[0]),
|
|
118
|
+
durationMs: observer.now() - started,
|
|
119
|
+
ok: !error,
|
|
120
|
+
...(error ? { error: error.message } : {}),
|
|
121
|
+
})
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
state.originals.set(name, { original, wrapper });
|
|
127
|
+
commander[name] = wrapper;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const observer = { record, identity, now };
|
|
131
|
+
if (record) {
|
|
132
|
+
state.observers.add(observer);
|
|
133
|
+
}
|
|
134
|
+
return () => {
|
|
135
|
+
state.observers.delete(observer);
|
|
136
|
+
if (state.observers.size) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const [name, { original, wrapper }] of state.originals) {
|
|
140
|
+
if (commander[name] === wrapper) {
|
|
141
|
+
commander[name] = original;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
commanders.delete(commander);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { startTrace } from './recorder.js';
|
|
4
|
+
|
|
5
|
+
const sessions = new WeakMap();
|
|
6
|
+
const disabledPages = new WeakSet();
|
|
7
|
+
|
|
8
|
+
export function debugEnabled(options = {}) {
|
|
9
|
+
return (
|
|
10
|
+
options.debug !== false &&
|
|
11
|
+
process.env.BROWSER_COMMANDER_DEBUG !== '0' &&
|
|
12
|
+
!(options.debug === undefined && disabledPages.has(options.page))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A launched page and its commander share one recording and one output path. */
|
|
17
|
+
export async function ensureDebugSession(options) {
|
|
18
|
+
const { page, commander } = options;
|
|
19
|
+
const existing = sessions.get(page);
|
|
20
|
+
if (!debugEnabled(options)) {
|
|
21
|
+
disabledPages.add(page);
|
|
22
|
+
await existing?.stop();
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
disabledPages.delete(page);
|
|
26
|
+
let session = existing;
|
|
27
|
+
if (!session) {
|
|
28
|
+
const config =
|
|
29
|
+
options.debug && typeof options.debug === 'object' ? options.debug : {};
|
|
30
|
+
const output = path.resolve(
|
|
31
|
+
config.output ??
|
|
32
|
+
options.output ??
|
|
33
|
+
process.env.BROWSER_COMMANDER_TRACE ??
|
|
34
|
+
path.join(
|
|
35
|
+
'browser-commander-traces',
|
|
36
|
+
`${new Date().toISOString().replaceAll(':', '-')}-${process.pid}-${randomUUID().slice(0, 8)}`
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
session = { path: output };
|
|
40
|
+
sessions.set(page, session);
|
|
41
|
+
session.ready = startTrace({
|
|
42
|
+
...config,
|
|
43
|
+
output,
|
|
44
|
+
page,
|
|
45
|
+
commander: commander ?? {
|
|
46
|
+
page,
|
|
47
|
+
engine: options.engine,
|
|
48
|
+
downloads: options.downloads,
|
|
49
|
+
},
|
|
50
|
+
checkpointOnNavigation: config.checkpointOnNavigation ?? true,
|
|
51
|
+
})
|
|
52
|
+
.then((trace) => {
|
|
53
|
+
console.error(`[browser-commander] Recording session to ${output}`);
|
|
54
|
+
return trace;
|
|
55
|
+
})
|
|
56
|
+
.catch((error) => {
|
|
57
|
+
sessions.delete(page);
|
|
58
|
+
console.warn(
|
|
59
|
+
`[browser-commander] Could not start recording: ${error.message}`
|
|
60
|
+
);
|
|
61
|
+
return null;
|
|
62
|
+
});
|
|
63
|
+
let stopping;
|
|
64
|
+
session.stop = () =>
|
|
65
|
+
(stopping ??= session.ready.then(async (trace) => {
|
|
66
|
+
try {
|
|
67
|
+
return await trace?.stop();
|
|
68
|
+
} finally {
|
|
69
|
+
if (sessions.get(page) === session) {
|
|
70
|
+
sessions.delete(page);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const trace = await session.ready;
|
|
76
|
+
if (!trace) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (commander) {
|
|
80
|
+
trace.attachCommander(commander);
|
|
81
|
+
}
|
|
82
|
+
return session;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Finish trace files before closing/detaching the engine that supplies them. */
|
|
86
|
+
export async function attachLaunchDebug(result, options) {
|
|
87
|
+
const session = await ensureDebugSession({
|
|
88
|
+
...options,
|
|
89
|
+
engine:
|
|
90
|
+
options.engine ??
|
|
91
|
+
(result.page.isWebDriverPage ? 'selenium' : 'playwright'),
|
|
92
|
+
page: result.page,
|
|
93
|
+
downloads: result.downloads,
|
|
94
|
+
});
|
|
95
|
+
result.tracePath = session?.path ?? null;
|
|
96
|
+
if (!session) {
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const wrap = (owner, name) => {
|
|
100
|
+
const method = owner?.[name];
|
|
101
|
+
if (typeof method !== 'function') {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
owner[name] = async (...args) => {
|
|
105
|
+
let result;
|
|
106
|
+
try {
|
|
107
|
+
await session.stop();
|
|
108
|
+
} finally {
|
|
109
|
+
result = await method.apply(owner, args);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
wrap(result, 'close');
|
|
115
|
+
wrap(result, 'detach');
|
|
116
|
+
wrap(result.browser, 'close');
|
|
117
|
+
wrap(result.browser, 'disconnect');
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Serialize background drains and wait for the active drain during teardown. */
|
|
2
|
+
export function startTraceDrain({
|
|
3
|
+
mutations,
|
|
4
|
+
userActions,
|
|
5
|
+
captureState,
|
|
6
|
+
note,
|
|
7
|
+
}) {
|
|
8
|
+
let task;
|
|
9
|
+
const timer = setInterval(() => {
|
|
10
|
+
if (!task) {
|
|
11
|
+
task = Promise.resolve()
|
|
12
|
+
.then(() => userActions.drain())
|
|
13
|
+
.then(() => mutations.drain(captureState.index))
|
|
14
|
+
.catch((error) => note(error.message))
|
|
15
|
+
.finally(() => {
|
|
16
|
+
task = null;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}, 500);
|
|
20
|
+
timer.unref?.();
|
|
21
|
+
return async () => {
|
|
22
|
+
clearInterval(timer);
|
|
23
|
+
await task;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -57,10 +57,11 @@ export function createMutationStream(options) {
|
|
|
57
57
|
} = options;
|
|
58
58
|
|
|
59
59
|
const enabled = Boolean(domOptions.mutations);
|
|
60
|
+
const globalName = `${RECORDER_GLOBAL}${identity.traceId}`;
|
|
60
61
|
|
|
61
62
|
/** What the in-page recorder is built with, wherever it is installed. */
|
|
62
63
|
const recorderOptions = {
|
|
63
|
-
globalName
|
|
64
|
+
globalName,
|
|
64
65
|
redactSelectors: privacyOptions.redactSelectors,
|
|
65
66
|
redactAttributes: privacyOptions.redactAttributes,
|
|
66
67
|
useDefaults: privacyOptions.useDefaults,
|
|
@@ -168,7 +169,7 @@ export function createMutationStream(options) {
|
|
|
168
169
|
}
|
|
169
170
|
try {
|
|
170
171
|
const drained = await withDeadline(
|
|
171
|
-
evaluateInFrames(drainMutationsInPage,
|
|
172
|
+
evaluateInFrames(drainMutationsInPage, globalName),
|
|
172
173
|
captureTimeoutMs,
|
|
173
174
|
'trace mutation drain'
|
|
174
175
|
);
|
|
@@ -229,7 +230,7 @@ export function createMutationStream(options) {
|
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
try {
|
|
232
|
-
await evaluateInFrames(stopMutationRecorderInPage,
|
|
233
|
+
await evaluateInFrames(stopMutationRecorderInPage, globalName);
|
|
233
234
|
} catch (error) {
|
|
234
235
|
note?.(`could not stop the in-page recorder: ${error.message}`);
|
|
235
236
|
}
|
package/src/traces/observers.js
CHANGED
|
@@ -8,11 +8,49 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { TRACE_EVENT } from './schema.js';
|
|
11
|
+
import { observeCommanderActions } from './commander-actions.js';
|
|
12
|
+
|
|
13
|
+
/** Keep page observations when a launched trace is adopted by a commander. */
|
|
14
|
+
export function attachRecorderObservers(options) {
|
|
15
|
+
const ownerSources = ['interaction', 'dialog', 'download', 'navigation'];
|
|
16
|
+
const detachers = attachTimelineObservers({
|
|
17
|
+
...options,
|
|
18
|
+
commander: null,
|
|
19
|
+
eventSources: options.eventSources.filter(
|
|
20
|
+
(source) => !ownerSources.includes(source)
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
let commanderDetachers = [],
|
|
24
|
+
attachedCommander,
|
|
25
|
+
hasCommander = false;
|
|
26
|
+
const attachCommander = (commander) => {
|
|
27
|
+
if (hasCommander && commander === attachedCommander) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
hasCommander = true;
|
|
31
|
+
attachedCommander = commander;
|
|
32
|
+
for (const detach of commanderDetachers) {
|
|
33
|
+
detach();
|
|
34
|
+
}
|
|
35
|
+
commanderDetachers = attachTimelineObservers({
|
|
36
|
+
...options,
|
|
37
|
+
commander,
|
|
38
|
+
eventSources: options.eventSources.filter((source) =>
|
|
39
|
+
ownerSources.includes(source)
|
|
40
|
+
),
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
attachCommander(options.commander);
|
|
44
|
+
detachers.push(() => commanderDetachers.forEach((detach) => detach()));
|
|
45
|
+
return { detachers, attachCommander };
|
|
46
|
+
}
|
|
11
47
|
|
|
12
48
|
/** Commander methods recorded as interactions while a trace is running. */
|
|
13
49
|
export const TRACED_INTERACTIONS = Object.freeze([
|
|
14
50
|
'click',
|
|
15
51
|
'clickButton',
|
|
52
|
+
'clickElement',
|
|
53
|
+
'check',
|
|
16
54
|
'fill',
|
|
17
55
|
'fillTextArea',
|
|
18
56
|
'goto',
|
|
@@ -231,51 +269,17 @@ export function attachTimelineObservers(options) {
|
|
|
231
269
|
};
|
|
232
270
|
});
|
|
233
271
|
|
|
234
|
-
subscribe('interaction', () =>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const startedMs = now();
|
|
247
|
-
const target = interactionTarget(args[0]);
|
|
248
|
-
// Named before the call, so everything the action causes can be traced
|
|
249
|
-
// back to it even when the action itself ends in an exception.
|
|
250
|
-
const actionId = identity?.nextActionId() ?? null;
|
|
251
|
-
try {
|
|
252
|
-
const result = await original(...args);
|
|
253
|
-
await record(TRACE_EVENT.INTERACTION, {
|
|
254
|
-
actionId,
|
|
255
|
-
action: name,
|
|
256
|
-
target,
|
|
257
|
-
durationMs: now() - startedMs,
|
|
258
|
-
ok: true,
|
|
259
|
-
});
|
|
260
|
-
return result;
|
|
261
|
-
} catch (error) {
|
|
262
|
-
await record(TRACE_EVENT.INTERACTION, {
|
|
263
|
-
actionId,
|
|
264
|
-
action: name,
|
|
265
|
-
target,
|
|
266
|
-
durationMs: now() - startedMs,
|
|
267
|
-
ok: false,
|
|
268
|
-
error: error.message,
|
|
269
|
-
});
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
return () => {
|
|
275
|
-
for (const [name, original] of originals) {
|
|
276
|
-
commander[name] = original;
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
});
|
|
272
|
+
subscribe('interaction', () =>
|
|
273
|
+
observeCommanderActions({
|
|
274
|
+
commander,
|
|
275
|
+
page,
|
|
276
|
+
names: TRACED_INTERACTIONS,
|
|
277
|
+
targetOf: interactionTarget,
|
|
278
|
+
record,
|
|
279
|
+
identity,
|
|
280
|
+
now,
|
|
281
|
+
note,
|
|
282
|
+
})
|
|
283
|
+
);
|
|
280
284
|
return detachers;
|
|
281
285
|
}
|