/**
* 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