/** * Rage-click / dead-click detection, live in the browser. * * This is a deliberate port of the server-side detectors in * `trace-compiler/src/detectors.ts` (humanbehavior-v2). Both must answer * "was this click dead?" the same way, because the raw `$rageclick` / * `$deadclick` events this file emits feed the dashboard Health Signals, the * issues dashboard, the replay inspector, the visitor timeline and * `hb_search_replays`, while the compiled `dead_click` / `rage_click` trace * rows feed Issues and `hb_find_friction`. When the two disagree, the product * contradicts itself about the same session. * * Thresholds live in FRICTION below and are asserted against the * trace-compiler constants by a parity test in the monorepo * (`tests/trace-compiler/sdk-friction-parity.test.ts`). Change one, change both. * * Two divergences from trace-compiler remain, and are not fixable from either * side alone: * 1. trace-compiler suppresses "ambient" mutations (tickers, clocks, ad * rotation) that fire with no user interaction, so they don't mask a dead * click. Doing that live would mean scoring every MutationRecord on the * main thread of the customer's app; we accept the false negatives. * 2. `selectionchange` is deliberately NOT treated as a page reaction. The * old SDK rule used it to suppress text-selection false positives; * trace-compiler has no selection signal at all, and the interactive-only * gate plus the two-gesture rule cover those cases instead. * A clickable
whose `cursor: pointer` comes from a stylesheet rather than * an inline style is invisible to BOTH sides: trace-compiler only has the rrweb * snapshot's inline styles, and reading the computed style here would break * target resolution (see hasInlinePointerCursor). */ /** Re-exported as one object for tests and for the cross-repo parity guard. */ export declare const FRICTION: { readonly RAGE_MIN_CLICKS: 4; readonly RAGE_WINDOW_MS: 2000; readonly DEAD_CLICK_REACTION_MS: 1500; readonly DEAD_CLICK_MIN_OCCURRENCES: 2; readonly DEAD_CLICK_GESTURE_MS: 700; readonly MIN_CLICK_SPACING_MS: 30; }; /** What kind of page response a signal represents. */ export type ReactionKind = 'dom' | 'soft'; export interface FrictionClickInfo { /** The resolved control (the interactive ancestor), not the raw event target. */ node: Element; /** clientX/clientY of the first click in the group. */ x: number; y: number; /** Timestamp of the first click in the group. */ tsMs: number; /** Rage: clicks in the burst. Dead: unreacted clicks recorded on this target. */ clickCount: number; /** Dead: distinct gestures (double-clicks collapsed). Undefined for rage. */ occurrences?: number; /** Rage: burst span. Dead: 0. */ durationMs: number; } export interface FrictionClickOptions { /** Called when a rage or dead click is confirmed. */ emit: (kind: 'rage' | 'dead', info: FrictionClickInfo) => void; /** Injectable for tests. */ now?: () => number; setTimeout?: (fn: () => void, ms: number) => number; clearTimeout?: (id: number) => void; /** Injectable for tests; defaults to window.location.href. */ currentUrl?: () => string; } /** * Walk up from a (possibly deeply nested) event target to the control the user * semantically clicked. `interactive: false` means we found no clickable * ancestor — the click landed on plain content. */ export declare function resolveInteractive(target: Element | null): { node: Element; interactive: boolean; } | null; /** True when a click on this element takes effect OUTSIDE the recorded tab. */ export declare function opensElsewhere(node: Element, currentUrl: string): boolean; /** True when the control is already selected/on, so a click is a no-op by design. */ export declare function alreadySelected(node: Element): boolean; /** * Stable per-control identity. Mirror of VDom.targetKey in trace-compiler, so a * React re-render that swaps the DOM node still groups with its earlier clicks. */ export declare function targetKey(node: Element): string; /** * The shared eligibility gate. Returns null when this click can never be rage * or dead. Same five checks, in the same order, as both trace-compiler * detectors. */ export declare function frictionEligible(target: Element | null, currentUrl: string): { node: Element; key: string; } | null; /** * Live rage/dead click detector. Feed it clicks and page reactions; it calls * `emit` when a verdict is reached. Holds no DOM listeners of its own — the * tracker owns those. */ export declare class FrictionClickDetector { private readonly emit; private readonly now; private readonly schedule; private readonly unschedule; private readonly currentUrl; /** Reaction timestamps, sorted, pruned to a bounded recent window. */ private domReactions; private softReactions; private targets; private pendingDead; private nextPendingId; constructor(options: FrictionClickOptions); /** Record a page response. Call on mutation/navigation ('dom') or scroll/typing ('soft'). */ onReaction(kind: ReactionKind, tsMs?: number): void; /** Feed every click. Ineligible targets are dropped here. */ onClick(target: Element | null, x: number, y: number, tsMs?: number): void; /** Drop all pending verdicts (page unload, session end). */ reset(): void; private stateFor; private trackRage; private scheduleRageVerdict; private settleRage; private trackDead; private settleDead; } //# sourceMappingURL=frictionClicks.d.ts.map