@reticlehq/browser 3.9.0 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/actions/action-report.d.ts +14 -0
  2. package/dist/actions/action-report.js +95 -22
  3. package/dist/actions/actions-dom.d.ts +16 -6
  4. package/dist/actions/actions-dom.js +94 -46
  5. package/dist/actions/actions-press.d.ts +10 -0
  6. package/dist/actions/actions-press.js +146 -2
  7. package/dist/actions/actions.js +52 -22
  8. package/dist/actions/insert-text.d.ts +3 -0
  9. package/dist/actions/insert-text.js +90 -0
  10. package/dist/actions/scroll.d.ts +1 -1
  11. package/dist/actions/scroll.js +25 -1
  12. package/dist/actions/value-input.d.ts +2 -0
  13. package/dist/actions/value-input.js +30 -9
  14. package/dist/commands/commands.js +82 -6
  15. package/dist/connect-options.d.ts +11 -1
  16. package/dist/connect-options.js +27 -1
  17. package/dist/dom/a11y.js +4 -3
  18. package/dist/dom/chart-geometry.d.ts +7 -0
  19. package/dist/dom/chart-geometry.js +39 -0
  20. package/dist/dom/occlusion.js +21 -0
  21. package/dist/dom/query-finders.d.ts +1 -1
  22. package/dist/dom/query-finders.js +10 -7
  23. package/dist/dom/query.d.ts +1 -0
  24. package/dist/dom/query.js +78 -51
  25. package/dist/dom/scope-roots.d.ts +8 -0
  26. package/dist/dom/scope-roots.js +44 -0
  27. package/dist/dom/snapshot.d.ts +3 -0
  28. package/dist/dom/snapshot.js +9 -2
  29. package/dist/dom/text-match.js +4 -3
  30. package/dist/dom/theme.js +6 -7
  31. package/dist/dom/visibility.d.ts +2 -0
  32. package/dist/dom/visibility.js +40 -8
  33. package/dist/observers/canvas.d.ts +1 -0
  34. package/dist/observers/canvas.js +10 -1
  35. package/dist/observers/console.js +46 -35
  36. package/dist/observers/dom.js +28 -4
  37. package/dist/observers/net-detail/resource-timing.d.ts +12 -0
  38. package/dist/observers/net-detail/resource-timing.js +69 -0
  39. package/dist/observers/network.js +12 -45
  40. package/dist/observers/perf.js +15 -1
  41. package/dist/presenter/chrome/presenter-verbs.js +1 -0
  42. package/dist/presenter/presenter-config.d.ts +1 -0
  43. package/dist/presenter/presenter-config.js +1 -0
  44. package/dist/presenter/presenter-shell.d.ts +1 -0
  45. package/dist/presenter/presenter-shell.js +6 -3
  46. package/dist/presenter/tour/tour.js +4 -0
  47. package/dist/recorder/recorder.js +82 -39
  48. package/dist/reticle-inject.js +597 -597
  49. package/dist/reticle.d.ts +3 -19
  50. package/dist/reticle.js +74 -108
  51. package/dist/security/serialization.js +35 -0
  52. package/dist/storage-keys.js +2 -1
  53. package/dist/timers/native/native-timers.js +21 -4
  54. package/dist/transport/bridge-url.d.ts +8 -0
  55. package/dist/transport/bridge-url.js +52 -0
  56. package/package.json +10 -10
@@ -25,6 +25,10 @@ export function stalledFade(el) {
25
25
  }
26
26
  const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
27
27
  const OPEN_LAYER = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], dialog[open], [aria-modal="true"]';
28
+ const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
29
+ export function isNonRendered(el) {
30
+ return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
31
+ }
28
32
  const UNPAINTED_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta']);
29
33
  function hiddenByAnOverlay(el) {
30
34
  if (el.hasAttribute(OVERLAY_ARIA_HIDDEN)) {
@@ -57,7 +61,7 @@ function holdsOpenLayer(portal) {
57
61
  }
58
62
  return false;
59
63
  }
60
- function selfCssHidden(el, style) {
64
+ function selfCssHidden(el, style, includeVisibility = true) {
61
65
  if (isHtmlElement(el) && el.hidden) {
62
66
  return true;
63
67
  }
@@ -65,9 +69,7 @@ function selfCssHidden(el, style) {
65
69
  return true;
66
70
  }
67
71
  if (style !== null) {
68
- if ('none' === style.display ||
69
- 'hidden' === style.visibility ||
70
- 'collapse' === style.visibility) {
72
+ if ('none' === style.display || (includeVisibility && visibilityHidden(style))) {
71
73
  return true;
72
74
  }
73
75
  if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
@@ -79,6 +81,9 @@ function selfCssHidden(el, style) {
79
81
  }
80
82
  return false;
81
83
  }
84
+ function visibilityHidden(style) {
85
+ return 'hidden' === style?.visibility || 'collapse' === style?.visibility;
86
+ }
82
87
  function clippedAway(style) {
83
88
  const rect = /^rect\((.*)\)$/.exec(style.getPropertyValue('clip').trim());
84
89
  if (null !== rect && ('absolute' === style.position || 'fixed' === style.position)) {
@@ -106,7 +111,7 @@ function selfHidden(el, style) {
106
111
  if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
107
112
  return true;
108
113
  }
109
- return selfCssHidden(el, style);
114
+ return selfCssHidden(el, style, false);
110
115
  }
111
116
  export function isHiddenByAriaOnly(el) {
112
117
  let foundAriaHidden = false;
@@ -116,7 +121,7 @@ export function isHiddenByAriaOnly(el) {
116
121
  foundAriaHidden = true;
117
122
  }
118
123
  const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
119
- if (selfCssHidden(current, style)) {
124
+ if (selfCssHidden(current, style, current === el)) {
120
125
  return false;
121
126
  }
122
127
  current = parentAcrossShadowBoundary(current);
@@ -130,7 +135,7 @@ export function isRenderedOutsideAriaHidden(el) {
130
135
  return false;
131
136
  }
132
137
  const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
133
- if (selfCssHidden(current, style)) {
138
+ if (selfCssHidden(current, style, current === el)) {
134
139
  return false;
135
140
  }
136
141
  current = parentAcrossShadowBoundary(current);
@@ -235,9 +240,32 @@ function visibilityInfoMemo(memo) {
235
240
  }
236
241
  return info;
237
242
  }
243
+ function underKnownHidden(el, memo) {
244
+ for (let up = parentAcrossShadowBoundary(el); null !== up; up = parentAcrossShadowBoundary(up)) {
245
+ const known = memo.get(up);
246
+ if (known !== undefined) {
247
+ return known.hidden;
248
+ }
249
+ }
250
+ return false;
251
+ }
238
252
  function ownVisibilityInfo(el, memo) {
239
253
  let info = memo.get(el);
240
254
  if (info === undefined) {
255
+ if (underKnownHidden(el, memo)) {
256
+ info = {
257
+ hidden: true,
258
+ style: null,
259
+ parent: null,
260
+ clipX: false,
261
+ clipY: false,
262
+ scrollX: false,
263
+ scrollY: false,
264
+ hasClipping: false,
265
+ };
266
+ memo.set(el, info);
267
+ return info;
268
+ }
241
269
  const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
242
270
  if (selfHidden(el, style)) {
243
271
  info = {
@@ -386,6 +414,9 @@ function collapsed(el, memo) {
386
414
  (own.clipY && 0 === r.height) ||
387
415
  (own.clipX && own.clipY && r.width <= 1 && r.height <= 1)));
388
416
  }
417
+ export function isHiddenSubtree(el, memo) {
418
+ return ownVisibilityInfo(el, visibilityInfoMemo(memo)).hidden;
419
+ }
389
420
  export function isVisible(el, memo) {
390
421
  if (!el.isConnected) {
391
422
  return false;
@@ -395,7 +426,8 @@ export function isVisible(el, memo) {
395
426
  return cached;
396
427
  }
397
428
  const info = visibilityInfoMemo(memo);
398
- const result = visibleBounds(el, info, undefined, true) !== null &&
429
+ const result = !visibilityHidden(ownVisibilityInfo(el, info).style) &&
430
+ visibleBounds(el, info, undefined, true) !== null &&
399
431
  !collapsed(el, info) &&
400
432
  !backfaceHidden(el, info);
401
433
  if (memo !== undefined) {
@@ -13,6 +13,7 @@ export declare function digestPixels(data: Uint8ClampedArray): {
13
13
  interface CanvasOptions {
14
14
  sample?: (canvas: HTMLCanvasElement) => CanvasSample | undefined;
15
15
  every?: (run: () => void, ms: number) => () => void;
16
+ onScreen?: (canvas: HTMLCanvasElement) => boolean;
16
17
  }
17
18
  export declare function canvasAfterAct(): void;
18
19
  export declare function installCanvas(emit: Emit, opts?: CanvasOptions): Teardown;
@@ -1,6 +1,7 @@
1
1
  import { CanvasTrigger, EventType } from '@reticlehq/core';
2
2
  export { CanvasTrigger };
3
3
  import { refs } from '../dom/addressing/refs.js';
4
+ import { isInViewport } from '../dom/visibility.js';
4
5
  import { nativeSetInterval } from '../timers/native/native-timers.js';
5
6
  import { observeSafely, observeValue } from './types.js';
6
7
  const SIDE = 16;
@@ -47,6 +48,7 @@ export function canvasAfterAct() {
47
48
  export function installCanvas(emit, opts = {}) {
48
49
  const sample = opts.sample ?? sampleCanvas;
49
50
  const every = opts.every ?? nativeSetInterval;
51
+ const onScreen = opts.onScreen ?? ((canvas) => isInViewport(canvas));
50
52
  const last = new WeakMap();
51
53
  const look = (trigger) => {
52
54
  observeSafely(() => {
@@ -61,7 +63,14 @@ export function installCanvas(emit, opts = {}) {
61
63
  continue;
62
64
  }
63
65
  last.set(canvas, digest);
64
- emit(EventType.CANVAS_DIGEST, { digest, width: read.width, height: read.height, mean, trigger }, refs.refFor(canvas));
66
+ emit(EventType.CANVAS_DIGEST, {
67
+ digest,
68
+ width: read.width,
69
+ height: read.height,
70
+ mean,
71
+ trigger,
72
+ visible: onScreen(canvas),
73
+ }, refs.refFor(canvas));
65
74
  }
66
75
  });
67
76
  };
@@ -1,4 +1,4 @@
1
- import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
1
+ import { DROPPED_PRE_ATTACH_FIELD, EventType, PRE_ATTACH_FIELD, RETICLE_PRE_ATTACH_ERRORS, TRANSPORT_LIMITS, } from '@reticlehq/core';
2
2
  import { observeSafely } from './types.js';
3
3
  import { safeStringify } from '../security/serialization.js';
4
4
  import { requireCapturedMethod } from '../util/captured-method.js';
@@ -15,44 +15,48 @@ const METHOD_EVENT = {
15
15
  function stringifyArgs(args) {
16
16
  return args
17
17
  .map((a) => {
18
- if ('string' === typeof a)
18
+ if ('string' === typeof a) {
19
19
  return a;
20
- if (a instanceof Error)
20
+ }
21
+ if (a instanceof Error) {
21
22
  return a.message;
23
+ }
22
24
  return safeStringify(a);
23
25
  })
24
26
  .join(' ');
25
27
  }
26
28
  const MAX_STACK_LEN = TRANSPORT_LIMITS.MAX_STACK_LENGTH;
27
29
  function capStack(stack) {
28
- if (stack === undefined || 0 === stack.length)
30
+ if (stack === undefined || 0 === stack.length) {
29
31
  return undefined;
32
+ }
30
33
  return stack.length > MAX_STACK_LEN ? stack.slice(0, MAX_STACK_LEN) : stack;
31
34
  }
32
- function firstErrorStack(args) {
33
- for (const arg of args) {
34
- if (arg instanceof Error)
35
- return capStack(arg.stack);
36
- }
37
- return undefined;
35
+ function stackOf(value) {
36
+ const stack = value instanceof Error ? capStack(value.stack) : undefined;
37
+ return stack === undefined ? {} : { stack };
38
38
  }
39
39
  export function installConsole(emit) {
40
40
  const methods = ['log', 'warn', 'error', 'info', 'debug'];
41
41
  const originals = new Map();
42
42
  const patched = new Map();
43
+ let marks = {};
44
+ const report = (method, args) => {
45
+ observeSafely(() => {
46
+ emit(METHOD_EVENT[method], {
47
+ ...marks,
48
+ message: stringifyArgs(args),
49
+ ...('error' === method ? stackOf(args.find((arg) => arg instanceof Error)) : {}),
50
+ });
51
+ });
52
+ };
43
53
  for (const method of methods) {
44
54
  const original = requireCapturedMethod(console, method);
45
55
  originals.set(method, original);
46
56
  const callOriginal = original.bind(console);
47
57
  const wrapper = (...args) => {
48
58
  callOriginal(...args);
49
- observeSafely(() => {
50
- const stack = 'error' === method ? firstErrorStack(args) : undefined;
51
- emit(METHOD_EVENT[method], {
52
- message: stringifyArgs(args),
53
- ...(stack === undefined ? {} : { stack }),
54
- });
55
- });
59
+ report(method, args);
56
60
  };
57
61
  patched.set(method, wrapper);
58
62
  console[method] = wrapper;
@@ -60,9 +64,11 @@ export function installConsole(emit) {
60
64
  const repeats = createRepeatLimiter();
61
65
  const emitError = (payload) => {
62
66
  const decision = repeats.admit(payload);
63
- if (!decision.emit)
67
+ if (!decision.emit) {
64
68
  return;
69
+ }
65
70
  emit(EventType.ERROR_UNCAUGHT, {
71
+ ...marks,
66
72
  ...payload,
67
73
  ...(undefined === decision.repeats ? {} : { repeats: decision.repeats }),
68
74
  });
@@ -70,8 +76,9 @@ export function installConsole(emit) {
70
76
  const onError = (event) => {
71
77
  if (!(event instanceof ErrorEvent)) {
72
78
  const target = event.target;
73
- if (!(target instanceof Element) || target === document)
79
+ if (!(target instanceof Element) || target === document) {
74
80
  return;
81
+ }
75
82
  const tag = target.tagName.toLowerCase();
76
83
  const url = target.getAttribute('src') ?? target.getAttribute('href') ?? '';
77
84
  observeSafely(() => {
@@ -81,37 +88,32 @@ export function installConsole(emit) {
81
88
  return;
82
89
  }
83
90
  observeSafely(() => {
84
- const stack = capStack(event.error instanceof Error ? event.error.stack : undefined);
85
- const payload = {
91
+ emitError({
86
92
  message: event.message,
87
93
  source: event.filename,
88
94
  line: event.lineno,
89
- ...(stack === undefined ? {} : { stack }),
90
- };
91
- emitError(payload);
95
+ ...stackOf(event.error),
96
+ });
92
97
  });
93
98
  };
94
99
  const onRejection = (event) => {
95
100
  observeSafely(() => {
96
101
  const reason = event.reason;
97
- const stack = capStack(reason instanceof Error ? reason.stack : undefined);
98
- const payload = {
102
+ emitError({
99
103
  message: reason instanceof Error ? reason.message : String(reason),
100
104
  kind: 'unhandledrejection',
101
- ...(stack === undefined ? {} : { stack }),
102
- };
103
- emitError(payload);
105
+ ...stackOf(reason),
106
+ });
104
107
  });
105
108
  };
106
109
  const onViolation = (event) => {
107
- const what = 0 === event.blockedURI.length ? 'inline content' : event.blockedURI;
110
+ const uri = event.blockedURI;
111
+ const what = 0 === uri.length ? 'inline content' : uri;
108
112
  const reportOnly = REPORT_ONLY === event.disposition;
109
113
  emit(reportOnly ? EventType.CONSOLE_WARN : EventType.CONSOLE_ERROR, {
110
- message: reportOnly
111
- ? `[Report Only] Content Security Policy directive: ${event.violatedDirective} would block ${what}`
112
- : `Content Security Policy directive: ${event.violatedDirective} blocked ${what}`,
114
+ message: `${reportOnly ? '[Report Only] ' : ''}Content Security Policy directive: ${event.violatedDirective} ${reportOnly ? 'would block' : 'blocked'} ${what}`,
113
115
  kind: CSP_VIOLATION_EVENT,
114
- ...(0 === event.blockedURI.length ? {} : { source: event.blockedURI }),
116
+ ...(0 === uri.length ? {} : { source: uri }),
115
117
  });
116
118
  };
117
119
  const listeners = new AbortController();
@@ -119,10 +121,19 @@ export function installConsole(emit) {
119
121
  window.addEventListener('error', onError, { capture: true, signal });
120
122
  window.addEventListener('unhandledrejection', onRejection, { signal });
121
123
  window.addEventListener(CSP_VIOLATION_EVENT, onViolation, { capture: true, signal });
124
+ observeSafely(() => {
125
+ const mark = { [PRE_ATTACH_FIELD]: true };
126
+ marks = mark;
127
+ globalThis[RETICLE_PRE_ATTACH_ERRORS]?.drain(onError, onRejection, report, (count) => {
128
+ mark[DROPPED_PRE_ATTACH_FIELD] = Number.isInteger(count) && count > 0 ? count : undefined;
129
+ });
130
+ });
131
+ marks = {};
122
132
  return () => {
123
133
  for (const [method, original] of originals) {
124
- if (console[method] === patched.get(method))
134
+ if (console[method] === patched.get(method)) {
125
135
  console[method] = original;
136
+ }
126
137
  }
127
138
  listeners.abort();
128
139
  };
@@ -1,5 +1,5 @@
1
1
  import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
2
- import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
2
+ import { EventType, LivePoliteness, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
3
3
  import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
4
4
  import { isVisible } from '../dom/visibility.js';
5
5
  import { refs } from '../dom/addressing/refs.js';
@@ -108,6 +108,24 @@ const named = (el) => {
108
108
  const testid = testidNear(el);
109
109
  return testid === undefined ? {} : { testid };
110
110
  };
111
+ const ALERT_REGION = '[role="alert"]';
112
+ const LIVE_REGION = `[aria-live],${ALERT_REGION},[role="status"]`;
113
+ const liveOf = (el) => {
114
+ const region = el.closest(LIVE_REGION);
115
+ if (null === region) {
116
+ return {};
117
+ }
118
+ const level = region.getAttribute('aria-live');
119
+ if (level === LivePoliteness.ASSERTIVE || level === LivePoliteness.POLITE) {
120
+ return { live: level };
121
+ }
122
+ if (level !== null) {
123
+ return {};
124
+ }
125
+ return {
126
+ live: region.matches(ALERT_REGION) ? LivePoliteness.ASSERTIVE : LivePoliteness.POLITE,
127
+ };
128
+ };
111
129
  export function installDom(emit) {
112
130
  const stopRegistry = installShadowRegistry();
113
131
  const observer = new MutationObserver((records) => {
@@ -149,7 +167,7 @@ export function installDom(emit) {
149
167
  ? REDACTED_VALUE
150
168
  : (record.target.textContent ?? '').trim().slice(0, 80);
151
169
  const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
152
- emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
170
+ emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent), ...liveOf(parent) }, refs.refFor(parent));
153
171
  }
154
172
  continue;
155
173
  }
@@ -164,7 +182,7 @@ export function installDom(emit) {
164
182
  const hide = isSensitiveField(parent);
165
183
  const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
166
184
  const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
167
- emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
185
+ emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent), ...liveOf(parent) }, refs.refFor(parent));
168
186
  }
169
187
  }
170
188
  }
@@ -226,7 +244,13 @@ export function installDom(emit) {
226
244
  }
227
245
  const { role, name } = gone;
228
246
  removed += 1;
229
- emit(EventType.DOM_REMOVED, { role, name, region: regionKeyOf(record.target) });
247
+ const testid = readTestId(gone.el) ?? readTestId(node);
248
+ emit(EventType.DOM_REMOVED, {
249
+ role,
250
+ name,
251
+ region: regionKeyOf(record.target),
252
+ ...(null === testid ? {} : { testid }),
253
+ });
230
254
  }
231
255
  }
232
256
  if (dropped > 0) {
@@ -0,0 +1,12 @@
1
+ import { EventType } from '@reticlehq/core';
2
+ type EmitNet = (type: EventType, data: Record<string, unknown>) => void;
3
+ export declare function absoluteUrl(raw: string): string;
4
+ export interface ResourceTimingInput {
5
+ emit: EmitNet;
6
+ installedAt: number;
7
+ nextId: () => string;
8
+ reportedNetUrls: Set<string>;
9
+ patchedNetUrls: ReadonlySet<string>;
10
+ }
11
+ export declare function observeResourceTiming(input: ResourceTimingInput): PerformanceObserver | undefined;
12
+ export {};
@@ -0,0 +1,69 @@
1
+ import { EventType } from '@reticlehq/core';
2
+ import { netUrlFields } from './network-redact.js';
3
+ const RESOURCE_CAP = 200;
4
+ export function absoluteUrl(raw) {
5
+ try {
6
+ return new URL(raw, location.href).href;
7
+ }
8
+ catch {
9
+ return raw;
10
+ }
11
+ }
12
+ function statusFields(entry, withOk) {
13
+ const status = entry.responseStatus ?? 0;
14
+ if (status <= 0) {
15
+ return {};
16
+ }
17
+ return withOk ? { status, ok: status >= 200 && status < 400 } : { status };
18
+ }
19
+ export function observeResourceTiming(input) {
20
+ const { emit, installedAt, nextId, reportedNetUrls, patchedNetUrls } = input;
21
+ if ('undefined' === typeof PerformanceObserver) {
22
+ return undefined;
23
+ }
24
+ let subresourceEvents = 0;
25
+ let unpatchedEvents = 0;
26
+ const onEntry = (entry) => {
27
+ const type = entry.initiatorType || 'other';
28
+ const rawUrl = entry.name;
29
+ const patched = 'fetch' === type || 'xmlhttprequest' === type;
30
+ if (patched && entry.startTime >= installedAt) {
31
+ if (patchedNetUrls.has(absoluteUrl(rawUrl)) || unpatchedEvents >= RESOURCE_CAP) {
32
+ return;
33
+ }
34
+ unpatchedEvents += 1;
35
+ emit(EventType.NET_UNPATCHED, {
36
+ ...netUrlFields(rawUrl),
37
+ initiator: type,
38
+ ...statusFields(entry, false),
39
+ });
40
+ return;
41
+ }
42
+ if (reportedNetUrls.has(rawUrl) || subresourceEvents >= RESOURCE_CAP) {
43
+ return;
44
+ }
45
+ reportedNetUrls.add(rawUrl);
46
+ subresourceEvents += 1;
47
+ emit(EventType.NET_REQUEST, {
48
+ id: nextId(),
49
+ method: 'GET',
50
+ ...netUrlFields(rawUrl),
51
+ durationMs: Math.round(entry.duration),
52
+ initiator: type,
53
+ ...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
54
+ ...statusFields(entry, true),
55
+ });
56
+ };
57
+ try {
58
+ const observer = new PerformanceObserver((list) => {
59
+ for (const raw of list.getEntries()) {
60
+ onEntry(raw);
61
+ }
62
+ });
63
+ observer.observe({ type: 'resource', buffered: true });
64
+ return observer;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
@@ -7,6 +7,7 @@ import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
7
7
  import { watchStreamedBody } from './network-stream.js';
8
8
  import { frameFields, isBridgeSocket } from './net-detail/network-frames.js';
9
9
  import { requireCapturedMethod } from '../util/captured-method.js';
10
+ import { absoluteUrl, observeResourceTiming } from './net-detail/resource-timing.js';
10
11
  export { redactUrl, netUrlFields };
11
12
  const ShapeToken = { NULL: '0', STRING: 's', NUMBER: 'n', BOOLEAN: 'b', OTHER: '?' };
12
13
  function shapeOf(value) {
@@ -240,8 +241,11 @@ export function installNetwork(emit, opts = {}) {
240
241
  }
241
242
  let seq = 0;
242
243
  const nextId = () => `n${++seq}`;
244
+ const reportedNetUrls = new Set();
245
+ const patchedNetUrls = new Set();
243
246
  window.fetch = async (input, init) => {
244
247
  const rawUrl = urlOf(input);
248
+ patchedNetUrls.add(absoluteUrl(rawUrl));
245
249
  if (true === opts.ignore?.(rawUrl)) {
246
250
  return callFetch(input, init);
247
251
  }
@@ -354,6 +358,7 @@ export function installNetwork(emit, opts = {}) {
354
358
  rawUrl: String(url),
355
359
  start: 0,
356
360
  });
361
+ patchedNetUrls.add(absoluteUrl(String(url)));
357
362
  });
358
363
  callOpen.call(this, method, url, ...rest);
359
364
  };
@@ -538,51 +543,13 @@ export function installNetwork(emit, opts = {}) {
538
543
  };
539
544
  navProto.sendBeacon = patchedBeacon;
540
545
  }
541
- const reportedNetUrls = new Set();
542
- let subresourceEvents = 0;
543
- const SUBRESOURCE_CAP = 200;
544
- let subresourceObserver;
545
- if (typeof PerformanceObserver !== 'undefined') {
546
- try {
547
- const observer = new PerformanceObserver((list) => {
548
- for (const raw of list.getEntries()) {
549
- const entry = raw;
550
- if (subresourceEvents >= SUBRESOURCE_CAP) {
551
- return;
552
- }
553
- const type = entry.initiatorType || 'other';
554
- const patched = 'fetch' === type || 'xmlhttprequest' === type;
555
- if (patched && entry.startTime >= installedAt) {
556
- continue;
557
- }
558
- const rawUrl = entry.name;
559
- if (reportedNetUrls.has(rawUrl)) {
560
- continue;
561
- }
562
- reportedNetUrls.add(rawUrl);
563
- subresourceEvents += 1;
564
- emit(EventType.NET_REQUEST, {
565
- id: nextId(),
566
- method: 'GET',
567
- ...netUrlFields(rawUrl),
568
- durationMs: Math.round(entry.duration),
569
- initiator: type,
570
- ...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
571
- ...((entry.responseStatus ?? 0) > 0
572
- ? {
573
- status: entry.responseStatus,
574
- ok: (entry.responseStatus ?? 0) >= 200 && (entry.responseStatus ?? 0) < 400,
575
- }
576
- : {}),
577
- });
578
- }
579
- });
580
- subresourceObserver = observer;
581
- observer.observe({ type: 'resource', buffered: true });
582
- }
583
- catch {
584
- }
585
- }
546
+ const subresourceObserver = observeResourceTiming({
547
+ emit,
548
+ installedAt,
549
+ nextId,
550
+ reportedNetUrls,
551
+ patchedNetUrls,
552
+ });
586
553
  return () => {
587
554
  if (pageSlots()[PAGE_CAPTURE] === owner) {
588
555
  delete pageSlots()[PAGE_CAPTURE];
@@ -45,6 +45,15 @@ function largestShifted(sources) {
45
45
  ? undefined
46
46
  : { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
47
47
  }
48
+ function onlyReticleMoved(sources) {
49
+ if (sources === undefined || 0 === sources.length) {
50
+ return false;
51
+ }
52
+ return sources.every(({ node }) => {
53
+ const el = isElement(node) ? node : node?.parentElement;
54
+ return el !== null && el !== undefined && isReticleOverlay(el);
55
+ });
56
+ }
48
57
  export function installPerf(emit) {
49
58
  if (typeof PerformanceObserver !== 'function')
50
59
  return () => undefined;
@@ -74,11 +83,16 @@ export function installPerf(emit) {
74
83
  const ls = e;
75
84
  if (true === ls.hadRecentInput)
76
85
  return;
77
- cls += ls.value ?? 0;
86
+ if (onlyReticleMoved(ls.sources)) {
87
+ return;
88
+ }
89
+ const shift = ls.value ?? 0;
90
+ cls += shift;
78
91
  const shifted = largestShifted(ls.sources);
79
92
  emit(EventType.PERF, {
80
93
  metric: PerfMetric.CLS,
81
94
  value: cls,
95
+ shift,
82
96
  at: Math.round(e.startTime),
83
97
  ...(shifted === undefined ? {} : { shifted }),
84
98
  });
@@ -6,6 +6,7 @@ export function actionVerb(action) {
6
6
  return 'Clicking';
7
7
  case ActionType.FILL:
8
8
  case ActionType.TYPE:
9
+ case ActionType.INSERT_TEXT:
9
10
  return 'Typing into';
10
11
  case ActionType.HOVER:
11
12
  return 'Hovering';
@@ -54,6 +54,7 @@ export declare const GLOW_OFF = "0";
54
54
  export declare const DATA_ON = "data-on";
55
55
  export declare const STATE_ATTR = "data-reticle-state";
56
56
  export declare const UNREACHABLE_STATE = "unreachable";
57
+ export declare const NARROW_VIEWPORT_PX = 480;
57
58
  export declare const MIN_ATTR = "data-reticle-min";
58
59
  export declare const CHAT_ATTR = "data-reticle-chat";
59
60
  export declare const DOCK_ATTR = "data-reticle-dock";
@@ -35,6 +35,7 @@ export const GLOW_OFF = '0';
35
35
  export const DATA_ON = 'data-on';
36
36
  export const STATE_ATTR = 'data-reticle-state';
37
37
  export const UNREACHABLE_STATE = 'unreachable';
38
+ export const NARROW_VIEWPORT_PX = 480;
38
39
  export const MIN_ATTR = 'data-reticle-min';
39
40
  export const CHAT_ATTR = 'data-reticle-chat';
40
41
  export const DOCK_ATTR = 'data-reticle-dock';
@@ -53,4 +53,5 @@ export declare function wasLeftOpen(): boolean;
53
53
  export declare function rememberLeftOpen(open: boolean): void;
54
54
  export declare function shouldReopenPanel(): boolean;
55
55
  export declare function shouldAutoOpenChat(autoOpenChat: boolean): boolean;
56
+ export declare function isNarrowViewport(): boolean;
56
57
  export {};
@@ -3,7 +3,7 @@ import { PresenterReport, reportPanelHtml } from './presenter-report.js';
3
3
  import { paintToolbarAccount, TOOLBAR_ACCOUNT_ATTR } from './presenter-workspace.js';
4
4
  import { mountAccountControl, TEAM_SLOT_ATTR } from './presenter-account.js';
5
5
  import { paintSettingsAccount } from './presenter-settings.js';
6
- import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, SETTINGS_ATTR, SETTINGS_BTN_ATTR, MINIMISED_STORAGE_KEY, OPEN_STORAGE_KEY, } from './presenter-config.js';
6
+ import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, NARROW_VIEWPORT_PX, SETTINGS_ATTR, SETTINGS_BTN_ATTR, MINIMISED_STORAGE_KEY, OPEN_STORAGE_KEY, } from './presenter-config.js';
7
7
  import { paintCarousel } from './carousel/carousel.js';
8
8
  import { panelSlides } from './carousel/panel-slides.js';
9
9
  import { paintRail, RAIL_HTML, RAIL_PROMO_ATTR, RAIL_SIGNIN_HTML } from './presenter-frame.js';
@@ -513,8 +513,11 @@ export function rememberLeftOpen(open) {
513
513
  }
514
514
  }
515
515
  export function shouldReopenPanel() {
516
- return wasLeftOpen() && !wasMinimised();
516
+ return !isNarrowViewport() && wasLeftOpen() && !wasMinimised();
517
517
  }
518
518
  export function shouldAutoOpenChat(autoOpenChat) {
519
- return autoOpenChat && !wasMinimised();
519
+ return autoOpenChat && !isNarrowViewport() && !wasMinimised();
520
+ }
521
+ export function isNarrowViewport() {
522
+ return globalThis.innerWidth < NARROW_VIEWPORT_PX;
520
523
  }
@@ -225,6 +225,10 @@ export function mountTour(deps) {
225
225
  const onKey = (event) => {
226
226
  if (!open)
227
227
  return;
228
+ if (deps.isDriving()) {
229
+ remove();
230
+ return;
231
+ }
228
232
  if (appModalOpen(doc))
229
233
  return;
230
234
  const step = 'ArrowRight' === event.key ? 1 : 'ArrowLeft' === event.key ? -1 : 0;