Add userTriggered (#495)

* add `userTriggered`

* update snapshots to add userTriggered

* add `userTriggered`

* update snapshots to add userTriggered

* update snapshot to include userTrigger

* only set userTriggered on `userTriggeredOnInput: true`

* What is user triggered?

* correct snapshot

* add second radio to demonstrate userTriggered
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent a224fdd903
commit 9df2aa8e0e
9 changed files with 1541 additions and 470 deletions

View File

@@ -57,6 +57,7 @@ function record<T = eventWithTime>(
sampling = {},
mousemoveWait,
recordCanvas = false,
userTriggeredOnInput = false,
collectFonts = false,
plugins,
keepIframeSrcFn = () => false,
@@ -379,6 +380,7 @@ function record<T = eventWithTime>(
inlineStylesheet,
sampling,
recordCanvas,
userTriggeredOnInput,
collectFonts,
doc,
maskInputFn,

View File

@@ -337,6 +337,15 @@ function initViewportResizeObserver(
return on('resize', updateDimension, window);
}
function wrapEventWithUserTriggeredFlag(
v: inputValue,
enable: boolean,
): inputValue {
const value = { ...v };
if (!enable) delete value.userTriggered;
return value;
}
export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
function initInputObserver(
@@ -348,9 +357,11 @@ function initInputObserver(
maskInputOptions: MaskInputOptions,
maskInputFn: MaskInputFn | undefined,
sampling: SamplingStrategy,
userTriggeredOnInput: boolean,
): listenerHandler {
function eventHandler(event: Event) {
const target = getEventTarget(event);
const userTriggered = event.isTrusted;
if (
!target ||
!(target as Element).tagName ||
@@ -381,7 +392,13 @@ function initInputObserver(
maskInputFn,
});
}
cbWithDedup(target, { text, isChecked });
cbWithDedup(
target,
wrapEventWithUserTriggeredFlag(
{ text, isChecked, userTriggered },
userTriggeredOnInput,
),
);
// if a radio was checked
// the other radios with the same name attribute will be unchecked.
const name: string | undefined = (target as HTMLInputElement).name;
@@ -390,10 +407,17 @@ function initInputObserver(
.querySelectorAll(`input[type="radio"][name="${name}"]`)
.forEach((el) => {
if (el !== target) {
cbWithDedup(el, {
text: (el as HTMLInputElement).value,
isChecked: !isChecked,
});
cbWithDedup(
el,
wrapEventWithUserTriggeredFlag(
{
text: (el as HTMLInputElement).value,
isChecked: !isChecked,
userTriggered: false,
},
userTriggeredOnInput,
),
);
}
});
}
@@ -763,6 +787,7 @@ export function initObservers(
o.maskInputOptions,
o.maskInputFn,
o.sampling,
o.userTriggeredOnInput,
);
const mediaInteractionHandler = initMediaInteractionObserver(
o.mediaInteractionCb,

View File

@@ -115,6 +115,7 @@ export class Replayer {
triggerFocus: true,
UNSAFE_replayCanvas: false,
pauseAnimation: true,
userTriggeredOnInput: true,
mouseTail: defaultMouseTailConfig,
};
this.config = Object.assign({}, defaultConfig, config);
@@ -501,10 +502,7 @@ export class Replayer {
// events are kept sorted by timestamp, check if this is the last event
let last_index = this.service.state.context.events.length - 1;
if (
event ===
this.service.state.context.events[last_index]
) {
if (event === this.service.state.context.events[last_index]) {
const finish = () => {
if (last_index < this.service.state.context.events.length - 1) {
// more events have been added since the setTimeout

View File

@@ -218,6 +218,7 @@ export type recordOptions<T> = {
packFn?: PackFn;
sampling?: SamplingStrategy;
recordCanvas?: boolean;
userTriggeredOnInput?: boolean;
collectFonts?: boolean;
plugins?: RecordPlugin[];
// departed, please use sampling options
@@ -247,6 +248,7 @@ export type observerParam = {
fontCb: fontCallback;
sampling: SamplingStrategy;
recordCanvas: boolean;
userTriggeredOnInput: boolean;
collectFonts: boolean;
slimDOMOptions: SlimDOMOptions;
doc: Document;
@@ -419,6 +421,12 @@ export type viewportResizeCallback = (d: viewportResizeDimension) => void;
export type inputValue = {
text: string;
isChecked: boolean;
// `userTriggered` indicates if this event was triggered directly by user (userTriggered: true)
// or was triggered indirectly (userTriggered: false)
// Example of `userTriggered` in action:
// User clicks on radio element (userTriggered: true) which triggers the other radio element to change (userTriggered: false)
userTriggered?: boolean;
};
export type inputCallback = (v: inputValue & { id: number }) => void;
@@ -484,6 +492,7 @@ export type playerConfig = {
triggerFocus: boolean;
UNSAFE_replayCanvas: boolean;
pauseAnimation?: boolean;
userTriggeredOnInput: boolean;
mouseTail:
| boolean
| {