react-auto-tracking 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hank Lin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # react-auto-tracking
2
+
3
+ Global user interaction tracking for React apps (16–19). Captures clicks, form events, and ambient events via DOM event delegation with automatic React fiber introspection.
4
+
5
+ ## Features
6
+
7
+ - **Zero-config** — works with any React 16–19 app, no provider or HOC needed
8
+ - **Fiber introspection** — automatically resolves the nearest React component name and props
9
+ - **Smart filtering** — only tracks interactive elements (buttons, links, ARIA roles, elements with React handlers)
10
+ - **Three event categories** — Pointer (click), Form (input/change/focus/blur), Ambient (scroll/keydown/keyup)
11
+ - **Listener options** — debounce, throttle, once, CSS selector filtering
12
+ - **Tree-shakeable** — ESM + CJS, zero runtime dependencies
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install react-auto-tracking
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```ts
23
+ import { init } from 'react-auto-tracking'
24
+
25
+ const tracker = init()
26
+
27
+ // Track all clicks on interactive elements
28
+ const unsubscribe = tracker.on('click', (event) => {
29
+ console.log(event.targetElement.tagName) // 'BUTTON'
30
+ console.log(event.fiber?.componentName) // 'SubmitButton'
31
+ console.log(event.fiber?.props) // { variant: 'primary', onClick: [Function] }
32
+ })
33
+
34
+ // Clean up
35
+ unsubscribe() // remove this listener
36
+ tracker.destroy() // remove all listeners and DOM bindings
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `init(config?): Tracker`
42
+
43
+ Creates a tracker instance.
44
+
45
+ ```ts
46
+ const tracker = init({
47
+ enabled: true, // toggle tracking on/off
48
+ ignoreSelectors: ['.no-track', '[data-private]'],
49
+ debug: false, // log events to console.debug
50
+ })
51
+ ```
52
+
53
+ ### `tracker.on(eventType, callback, options?): unsubscribe`
54
+
55
+ Register a listener for a DOM event type. Returns an unsubscribe function.
56
+
57
+ ```ts
58
+ // Basic
59
+ tracker.on('click', (event) => { ... })
60
+
61
+ // With options
62
+ tracker.on('scroll', handler, { throttle: 200 })
63
+ tracker.on('input', handler, { debounce: 300 })
64
+ tracker.on('click', handler, { once: true })
65
+ tracker.on('click', handler, { selector: 'nav a' })
66
+ ```
67
+
68
+ Multiple listeners can be registered for the same event type — each fires independently, similar to `addEventListener`.
69
+
70
+ ### `tracker.getLastEvent(): TrackEvent | null`
71
+
72
+ Returns the most recent tracked event, or `null` if none. Useful with `visibilitychange` or `beforeunload` to capture the last interaction before the user leaves:
73
+
74
+ ```ts
75
+ document.addEventListener('visibilitychange', () => {
76
+ if (document.visibilityState === 'hidden') {
77
+ const last = tracker.getLastEvent()
78
+ if (last) {
79
+ navigator.sendBeacon('/analytics', JSON.stringify({
80
+ component: last.fiber?.componentName,
81
+ element: last.targetElement.tagName,
82
+ }))
83
+ }
84
+ }
85
+ })
86
+ ```
87
+
88
+ ### `tracker.destroy()`
89
+
90
+ Removes all listeners and DOM event bindings. The tracker instance is inert after this call.
91
+
92
+ ## TrackEvent
93
+
94
+ Each callback receives a `TrackEvent`:
95
+
96
+ ```ts
97
+ interface TrackEvent {
98
+ nativeEvent: Event // original DOM event
99
+ targetElement: Element // resolved trackable element (may differ from nativeEvent.target)
100
+ fiber: FiberInfo | null
101
+ }
102
+
103
+ interface FiberInfo {
104
+ componentName: string | null // nearest React component
105
+ props: Readonly<Record<string, unknown>> // component props (original types, not stringified)
106
+ }
107
+ ```
108
+
109
+ The library intentionally keeps `TrackEvent` minimal — use `nativeEvent` and `targetElement` to extract any DOM information you need, and `fiber.props` to access rich, typed data from React components (avoiding the `[object Object]` problem of HTML `data-*` attributes).
110
+
111
+ ## Event Categories
112
+
113
+ | Category | Events | Behavior |
114
+ |----------|--------|----------|
115
+ | **Pointer** | `click`, `touchstart`, `touchend` | Walks DOM ancestors to find interactive element (button, link, ARIA role, React handler) |
116
+ | **Form** | `input`, `change`, `focus`, `blur`, `submit` | Tracks target directly, skips disabled elements |
117
+ | **Ambient** | `scroll`, `keydown`, `keyup`, `copy`, `paste`, `resize`, `popstate`, `hashchange` | Tracks target directly, does not skip disabled |
118
+
119
+ ## How It Works
120
+
121
+ 1. **DOM delegation** — attaches a single capture-phase listener per event type on `document`
122
+ 2. **Filtering** — determines if the target (or ancestor) is interactive based on HTML semantics, ARIA roles, or React fiber props
123
+ 3. **Fiber resolution** — finds the nearest React component and extracts its name and props
124
+ 4. **Dispatch** — invokes all registered callbacks with the `TrackEvent`
125
+
126
+ Capture phase (`addEventListener(..., true)`) ensures events are caught even if `stopPropagation()` is called downstream.
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ pnpm install
132
+ pnpm test # run tests
133
+ pnpm test:watch # watch mode
134
+ pnpm test:coverage # coverage report (80% threshold)
135
+ pnpm lint # oxlint
136
+ pnpm format # oxfmt (write)
137
+ pnpm format:check # oxfmt (check only)
138
+ pnpm typecheck # tsc --noEmit
139
+ pnpm build # ESM + CJS + .d.ts via tsdown
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,415 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/filter/event-categories.ts
4
+ const EventCategory = {
5
+ Pointer: "pointer",
6
+ Form: "form",
7
+ Ambient: "ambient"
8
+ };
9
+ const EVENT_HANDLER_MAP = {
10
+ click: [
11
+ "onClick",
12
+ "onMouseDown",
13
+ "onMouseUp",
14
+ "onPointerDown",
15
+ "onPointerUp"
16
+ ],
17
+ touchstart: ["onTouchStart"],
18
+ touchend: ["onTouchEnd"],
19
+ input: ["onChange", "onInput"],
20
+ change: ["onChange"],
21
+ focus: ["onFocus"],
22
+ blur: ["onBlur"],
23
+ submit: ["onSubmit"],
24
+ scroll: ["onScroll"],
25
+ keydown: ["onKeyDown"],
26
+ keyup: ["onKeyUp"],
27
+ copy: ["onCopy"],
28
+ paste: ["onPaste"]
29
+ };
30
+ const CATEGORY_MAP = {
31
+ click: EventCategory.Pointer,
32
+ touchstart: EventCategory.Pointer,
33
+ touchend: EventCategory.Pointer,
34
+ input: EventCategory.Form,
35
+ change: EventCategory.Form,
36
+ focus: EventCategory.Form,
37
+ blur: EventCategory.Form,
38
+ submit: EventCategory.Form,
39
+ scroll: EventCategory.Ambient,
40
+ keydown: EventCategory.Ambient,
41
+ keyup: EventCategory.Ambient,
42
+ copy: EventCategory.Ambient,
43
+ paste: EventCategory.Ambient,
44
+ resize: EventCategory.Ambient,
45
+ popstate: EventCategory.Ambient,
46
+ hashchange: EventCategory.Ambient
47
+ };
48
+ function getEventCategory(eventType) {
49
+ return CATEGORY_MAP[eventType] ?? EventCategory.Ambient;
50
+ }
51
+ function getHandlersForEvent(eventType) {
52
+ return EVENT_HANDLER_MAP[eventType] ?? [];
53
+ }
54
+
55
+ //#endregion
56
+ //#region src/extract/fiber.ts
57
+ const FIBER_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
58
+ const MAX_PARENT_DEPTH = 10;
59
+ let cachedKey = null;
60
+ function resolveFiber(element) {
61
+ let current = element;
62
+ let depth = 0;
63
+ while (current !== null && depth <= MAX_PARENT_DEPTH) {
64
+ const fiber = getFiberFromElement(current);
65
+ if (fiber !== null) return fiber;
66
+ current = current.parentElement;
67
+ depth++;
68
+ }
69
+ return null;
70
+ }
71
+ function getFiberFromElement(element) {
72
+ if (cachedKey !== null) {
73
+ const fiber = element[cachedKey];
74
+ if (fiber != null) return fiber;
75
+ return null;
76
+ }
77
+ for (const key of Object.keys(element)) for (const prefix of FIBER_PREFIXES) if (key.startsWith(prefix)) {
78
+ cachedKey = key;
79
+ return element[key];
80
+ }
81
+ return null;
82
+ }
83
+ const MAX_STACK_DEPTH = 50;
84
+ function extractFiberInfo(rawFiber) {
85
+ if (rawFiber === null) return null;
86
+ const fiber = rawFiber;
87
+ return {
88
+ componentName: findNearestComponentName(fiber),
89
+ props: fiber.memoizedProps ?? {}
90
+ };
91
+ }
92
+ function findNearestComponentName(fiber) {
93
+ let current = fiber.return;
94
+ let depth = 0;
95
+ while (current !== null && depth < MAX_STACK_DEPTH) {
96
+ const name = getComponentName(current);
97
+ if (name !== null) return name;
98
+ current = current.return;
99
+ depth++;
100
+ }
101
+ return null;
102
+ }
103
+ function getComponentName(fiber) {
104
+ const type = fiber.type;
105
+ if (typeof type === "string") return null;
106
+ if (typeof type === "function") return type.displayName ?? type.name ?? null;
107
+ return null;
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/utils/safe-matches.ts
112
+ function safeMatches(element, selector) {
113
+ try {
114
+ return element.matches(selector);
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ //#endregion
121
+ //#region src/filter/filter-engine.ts
122
+ const INTERACTIVE_TAGS = new Set([
123
+ "BUTTON",
124
+ "A",
125
+ "INPUT",
126
+ "SELECT",
127
+ "TEXTAREA",
128
+ "SUMMARY",
129
+ "DETAILS"
130
+ ]);
131
+ const INTERACTIVE_ROLES = new Set([
132
+ "button",
133
+ "link",
134
+ "menuitem",
135
+ "tab",
136
+ "checkbox",
137
+ "radio",
138
+ "combobox",
139
+ "listbox",
140
+ "option",
141
+ "switch",
142
+ "slider",
143
+ "spinbutton",
144
+ "menuitemcheckbox",
145
+ "menuitemradio",
146
+ "treeitem",
147
+ "gridcell",
148
+ "textbox",
149
+ "searchbox"
150
+ ]);
151
+ const MAX_ANCESTOR_DEPTH = 10;
152
+ function findTrackableElement(params) {
153
+ const { target, ignoreSelectors, eventType } = params;
154
+ switch (getEventCategory(eventType)) {
155
+ case EventCategory.Pointer: return findPointerTarget(target, ignoreSelectors, eventType);
156
+ case EventCategory.Form: return findFormTarget(target, ignoreSelectors);
157
+ case EventCategory.Ambient: return findAmbientTarget(target, ignoreSelectors);
158
+ }
159
+ }
160
+ function findPointerTarget(target, ignoreSelectors, eventType) {
161
+ let current = target;
162
+ let depth = 0;
163
+ while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {
164
+ if (isIgnored(current, ignoreSelectors)) return null;
165
+ if (isDisabled(current)) return null;
166
+ const rawFiber = findInteractiveFiber(current, eventType);
167
+ if (rawFiber !== void 0) return {
168
+ element: current,
169
+ fiber: extractFiberInfo(rawFiber)
170
+ };
171
+ current = current.parentElement;
172
+ depth++;
173
+ }
174
+ return null;
175
+ }
176
+ function findFormTarget(target, ignoreSelectors) {
177
+ if (isIgnored(target, ignoreSelectors)) return null;
178
+ if (isDisabled(target)) return null;
179
+ return {
180
+ element: target,
181
+ fiber: extractFiberInfo(resolveFiber(target))
182
+ };
183
+ }
184
+ function findAmbientTarget(target, ignoreSelectors) {
185
+ if (isIgnored(target, ignoreSelectors)) return null;
186
+ return {
187
+ element: target,
188
+ fiber: extractFiberInfo(resolveFiber(target))
189
+ };
190
+ }
191
+ /**
192
+ * Returns the raw fiber if the element is interactive, or undefined if not.
193
+ * null means "interactive but no fiber found" (semantic tag / ARIA role).
194
+ */
195
+ function findInteractiveFiber(el, eventType) {
196
+ if (INTERACTIVE_TAGS.has(el.tagName)) return resolveFiber(el);
197
+ const role = el.getAttribute("role");
198
+ if (role !== null && INTERACTIVE_ROLES.has(role)) return resolveFiber(el);
199
+ const handlers = getHandlersForEvent(eventType);
200
+ if (handlers.length > 0) {
201
+ const fiber = resolveFiber(el);
202
+ if (fiber !== null) {
203
+ const props = fiber.memoizedProps;
204
+ if (props !== null && props !== void 0) {
205
+ for (const handler of handlers) if (typeof props[handler] === "function") return fiber;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ function isIgnored(element, ignoreSelectors) {
211
+ return ignoreSelectors.some((selector) => safeMatches(element, selector));
212
+ }
213
+ function isDisabled(el) {
214
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
215
+ }
216
+
217
+ //#endregion
218
+ //#region src/utils/debounce.ts
219
+ function debounce(fn, ms) {
220
+ let timeoutId = null;
221
+ const debounced = (...args) => {
222
+ if (timeoutId !== null) clearTimeout(timeoutId);
223
+ timeoutId = setTimeout(() => {
224
+ timeoutId = null;
225
+ fn(...args);
226
+ }, ms);
227
+ };
228
+ debounced.cancel = () => {
229
+ if (timeoutId !== null) {
230
+ clearTimeout(timeoutId);
231
+ timeoutId = null;
232
+ }
233
+ };
234
+ return debounced;
235
+ }
236
+
237
+ //#endregion
238
+ //#region src/utils/throttle.ts
239
+ function throttle(fn, ms) {
240
+ let lastCallTime = 0;
241
+ let timeoutId = null;
242
+ let lastArgs = null;
243
+ const throttled = (...args) => {
244
+ const now = Date.now();
245
+ const elapsed = now - lastCallTime;
246
+ if (elapsed >= ms) {
247
+ lastCallTime = now;
248
+ fn(...args);
249
+ } else {
250
+ lastArgs = args;
251
+ if (timeoutId === null) timeoutId = setTimeout(() => {
252
+ timeoutId = null;
253
+ lastCallTime = Date.now();
254
+ if (lastArgs !== null) {
255
+ fn(...lastArgs);
256
+ lastArgs = null;
257
+ }
258
+ }, ms - elapsed);
259
+ }
260
+ };
261
+ throttled.cancel = () => {
262
+ if (timeoutId !== null) {
263
+ clearTimeout(timeoutId);
264
+ timeoutId = null;
265
+ }
266
+ lastArgs = null;
267
+ };
268
+ return throttled;
269
+ }
270
+
271
+ //#endregion
272
+ //#region src/core/registry.ts
273
+ function createRegistry() {
274
+ let entries = [];
275
+ function createEntry(eventType, callback, options) {
276
+ const wrappedCallback = wrapCallback(callback, options);
277
+ const entry = {
278
+ eventType,
279
+ wrappedCallback,
280
+ options,
281
+ unsubscribe: () => {
282
+ wrappedCallback.cancel?.();
283
+ entries = entries.filter((e) => e !== entry);
284
+ }
285
+ };
286
+ return entry;
287
+ }
288
+ return {
289
+ add(eventType, callback, options) {
290
+ const entry = createEntry(eventType, callback, options);
291
+ entries = [...entries, entry];
292
+ return entry.unsubscribe;
293
+ },
294
+ invoke(event) {
295
+ for (const entry of entries) {
296
+ if (entry.eventType !== event.nativeEvent.type) continue;
297
+ if (entry.options.selector != null) {
298
+ if (!safeMatches(event.targetElement, entry.options.selector)) continue;
299
+ }
300
+ entry.wrappedCallback(event);
301
+ if (entry.options.once === true) entry.unsubscribe();
302
+ }
303
+ },
304
+ getEventTypes() {
305
+ return new Set(entries.map((e) => e.eventType));
306
+ },
307
+ clear() {
308
+ for (const entry of entries) entry.wrappedCallback.cancel?.();
309
+ entries = [];
310
+ }
311
+ };
312
+ }
313
+ function wrapCallback(callback, options) {
314
+ if (options.debounce != null) return debounce(callback, options.debounce);
315
+ if (options.throttle != null) return throttle(callback, options.throttle);
316
+ return callback;
317
+ }
318
+
319
+ //#endregion
320
+ //#region src/core/pipeline.ts
321
+ function createPipeline(config) {
322
+ const registry = createRegistry();
323
+ let lastEvent = null;
324
+ return {
325
+ handleEvent(domEvent) {
326
+ const target = domEvent.target;
327
+ if (!(target instanceof Element)) return;
328
+ const result = findTrackableElement({
329
+ target,
330
+ ignoreSelectors: config.ignoreSelectors,
331
+ eventType: domEvent.type
332
+ });
333
+ if (result === null) return;
334
+ const trackEvent = {
335
+ nativeEvent: domEvent,
336
+ targetElement: result.element,
337
+ fiber: result.fiber
338
+ };
339
+ registry.invoke(trackEvent);
340
+ lastEvent = trackEvent;
341
+ },
342
+ getLastEvent() {
343
+ return lastEvent;
344
+ },
345
+ addListener(eventType, callback, options) {
346
+ return registry.add(eventType, callback, options);
347
+ },
348
+ getEventTypes() {
349
+ return registry.getEventTypes();
350
+ },
351
+ clear() {
352
+ registry.clear();
353
+ }
354
+ };
355
+ }
356
+
357
+ //#endregion
358
+ //#region src/core/tracker.ts
359
+ function createTracker(config) {
360
+ const enabled = config?.enabled ?? true;
361
+ const ignoreSelectors = config?.ignoreSelectors ?? [];
362
+ const debug = config?.debug ?? false;
363
+ if (!enabled) return {
364
+ on: () => () => {},
365
+ getLastEvent: () => null,
366
+ destroy: () => {}
367
+ };
368
+ const pipeline = createPipeline({ ignoreSelectors });
369
+ const domListeners = /* @__PURE__ */ new Map();
370
+ let destroyed = false;
371
+ function ensureDomListener(eventType) {
372
+ if (domListeners.has(eventType)) return;
373
+ const handler = (event) => {
374
+ pipeline.handleEvent(event);
375
+ if (debug) {
376
+ const lastEvent = pipeline.getLastEvent();
377
+ if (lastEvent?.nativeEvent === event) console.debug("[react-auto-tracking]", lastEvent);
378
+ }
379
+ };
380
+ document.addEventListener(eventType, handler, true);
381
+ domListeners.set(eventType, handler);
382
+ }
383
+ function removeDomListener(eventType) {
384
+ const handler = domListeners.get(eventType);
385
+ if (handler === void 0) return;
386
+ if (!pipeline.getEventTypes().has(eventType)) {
387
+ document.removeEventListener(eventType, handler, true);
388
+ domListeners.delete(eventType);
389
+ }
390
+ }
391
+ return {
392
+ on(eventType, callback, options) {
393
+ if (destroyed) return () => {};
394
+ ensureDomListener(eventType);
395
+ const unsub = pipeline.addListener(eventType, callback, options ?? {});
396
+ return () => {
397
+ unsub();
398
+ removeDomListener(eventType);
399
+ };
400
+ },
401
+ getLastEvent() {
402
+ return pipeline.getLastEvent();
403
+ },
404
+ destroy() {
405
+ if (destroyed) return;
406
+ destroyed = true;
407
+ pipeline.clear();
408
+ for (const [eventType, handler] of domListeners) document.removeEventListener(eventType, handler, true);
409
+ domListeners.clear();
410
+ }
411
+ };
412
+ }
413
+
414
+ //#endregion
415
+ exports.init = createTracker;
@@ -0,0 +1,33 @@
1
+ //#region src/types.d.ts
2
+ interface TrackerConfig {
3
+ readonly enabled?: boolean;
4
+ readonly ignoreSelectors?: readonly string[];
5
+ readonly debug?: boolean;
6
+ }
7
+ interface Tracker {
8
+ on(eventType: string, callback: TrackCallback, options?: ListenerOptions): () => void;
9
+ getLastEvent(): TrackEvent | null;
10
+ destroy(): void;
11
+ }
12
+ interface ListenerOptions {
13
+ readonly debounce?: number;
14
+ readonly throttle?: number;
15
+ readonly once?: boolean;
16
+ readonly selector?: string;
17
+ }
18
+ interface TrackEvent {
19
+ readonly nativeEvent: Event;
20
+ readonly targetElement: Element;
21
+ readonly fiber: FiberInfo | null;
22
+ }
23
+ interface FiberInfo {
24
+ readonly componentName: string | null;
25
+ readonly props: Readonly<Record<string, unknown>>;
26
+ }
27
+ type TrackCallback = (event: TrackEvent) => void;
28
+ //#endregion
29
+ //#region src/core/tracker.d.ts
30
+ declare function createTracker(config?: TrackerConfig): Tracker;
31
+ //#endregion
32
+ export { type FiberInfo, type ListenerOptions, type TrackCallback, type TrackEvent, type Tracker, type TrackerConfig, createTracker as init };
33
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/core/tracker.ts"],"mappings":";UAEiB,aAAA;EAAA,SACN,OAAA;EAAA,SACA,eAAA;EAAA,SACA,KAAA;AAAA;AAAA,UAGM,OAAA;EACf,EAAA,CAAG,SAAA,UAAmB,QAAA,EAAU,aAAA,EAAe,OAAA,GAAU,eAAA;EACzD,YAAA,IAAgB,UAAA;EAChB,OAAA;AAAA;AAAA,UAGe,eAAA;EAAA,SACN,QAAA;EAAA,SACA,QAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;AAAA;AAAA,UAGM,UAAA;EAAA,SACN,WAAA,EAAa,KAAA;EAAA,SACb,aAAA,EAAe,OAAA;EAAA,SACf,KAAA,EAAO,SAAA;AAAA;AAAA,UAGD,SAAA;EAAA,SACN,aAAA;EAAA,SACA,KAAA,EAAO,QAAA,CAAS,MAAA;AAAA;AAAA,KAGf,aAAA,IAAiB,KAAA,EAAO,UAAA;;;iBC7BpB,aAAA,CAAc,MAAA,GAAS,aAAA,GAAgB,OAAA"}
@@ -0,0 +1,33 @@
1
+ //#region src/types.d.ts
2
+ interface TrackerConfig {
3
+ readonly enabled?: boolean;
4
+ readonly ignoreSelectors?: readonly string[];
5
+ readonly debug?: boolean;
6
+ }
7
+ interface Tracker {
8
+ on(eventType: string, callback: TrackCallback, options?: ListenerOptions): () => void;
9
+ getLastEvent(): TrackEvent | null;
10
+ destroy(): void;
11
+ }
12
+ interface ListenerOptions {
13
+ readonly debounce?: number;
14
+ readonly throttle?: number;
15
+ readonly once?: boolean;
16
+ readonly selector?: string;
17
+ }
18
+ interface TrackEvent {
19
+ readonly nativeEvent: Event;
20
+ readonly targetElement: Element;
21
+ readonly fiber: FiberInfo | null;
22
+ }
23
+ interface FiberInfo {
24
+ readonly componentName: string | null;
25
+ readonly props: Readonly<Record<string, unknown>>;
26
+ }
27
+ type TrackCallback = (event: TrackEvent) => void;
28
+ //#endregion
29
+ //#region src/core/tracker.d.ts
30
+ declare function createTracker(config?: TrackerConfig): Tracker;
31
+ //#endregion
32
+ export { type FiberInfo, type ListenerOptions, type TrackCallback, type TrackEvent, type Tracker, type TrackerConfig, createTracker as init };
33
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/core/tracker.ts"],"mappings":";UAEiB,aAAA;EAAA,SACN,OAAA;EAAA,SACA,eAAA;EAAA,SACA,KAAA;AAAA;AAAA,UAGM,OAAA;EACf,EAAA,CAAG,SAAA,UAAmB,QAAA,EAAU,aAAA,EAAe,OAAA,GAAU,eAAA;EACzD,YAAA,IAAgB,UAAA;EAChB,OAAA;AAAA;AAAA,UAGe,eAAA;EAAA,SACN,QAAA;EAAA,SACA,QAAA;EAAA,SACA,IAAA;EAAA,SACA,QAAA;AAAA;AAAA,UAGM,UAAA;EAAA,SACN,WAAA,EAAa,KAAA;EAAA,SACb,aAAA,EAAe,OAAA;EAAA,SACf,KAAA,EAAO,SAAA;AAAA;AAAA,UAGD,SAAA;EAAA,SACN,aAAA;EAAA,SACA,KAAA,EAAO,QAAA,CAAS,MAAA;AAAA;AAAA,KAGf,aAAA,IAAiB,KAAA,EAAO,UAAA;;;iBC7BpB,aAAA,CAAc,MAAA,GAAS,aAAA,GAAgB,OAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,414 @@
1
+ //#region src/filter/event-categories.ts
2
+ const EventCategory = {
3
+ Pointer: "pointer",
4
+ Form: "form",
5
+ Ambient: "ambient"
6
+ };
7
+ const EVENT_HANDLER_MAP = {
8
+ click: [
9
+ "onClick",
10
+ "onMouseDown",
11
+ "onMouseUp",
12
+ "onPointerDown",
13
+ "onPointerUp"
14
+ ],
15
+ touchstart: ["onTouchStart"],
16
+ touchend: ["onTouchEnd"],
17
+ input: ["onChange", "onInput"],
18
+ change: ["onChange"],
19
+ focus: ["onFocus"],
20
+ blur: ["onBlur"],
21
+ submit: ["onSubmit"],
22
+ scroll: ["onScroll"],
23
+ keydown: ["onKeyDown"],
24
+ keyup: ["onKeyUp"],
25
+ copy: ["onCopy"],
26
+ paste: ["onPaste"]
27
+ };
28
+ const CATEGORY_MAP = {
29
+ click: EventCategory.Pointer,
30
+ touchstart: EventCategory.Pointer,
31
+ touchend: EventCategory.Pointer,
32
+ input: EventCategory.Form,
33
+ change: EventCategory.Form,
34
+ focus: EventCategory.Form,
35
+ blur: EventCategory.Form,
36
+ submit: EventCategory.Form,
37
+ scroll: EventCategory.Ambient,
38
+ keydown: EventCategory.Ambient,
39
+ keyup: EventCategory.Ambient,
40
+ copy: EventCategory.Ambient,
41
+ paste: EventCategory.Ambient,
42
+ resize: EventCategory.Ambient,
43
+ popstate: EventCategory.Ambient,
44
+ hashchange: EventCategory.Ambient
45
+ };
46
+ function getEventCategory(eventType) {
47
+ return CATEGORY_MAP[eventType] ?? EventCategory.Ambient;
48
+ }
49
+ function getHandlersForEvent(eventType) {
50
+ return EVENT_HANDLER_MAP[eventType] ?? [];
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/extract/fiber.ts
55
+ const FIBER_PREFIXES = ["__reactFiber$", "__reactInternalInstance$"];
56
+ const MAX_PARENT_DEPTH = 10;
57
+ let cachedKey = null;
58
+ function resolveFiber(element) {
59
+ let current = element;
60
+ let depth = 0;
61
+ while (current !== null && depth <= MAX_PARENT_DEPTH) {
62
+ const fiber = getFiberFromElement(current);
63
+ if (fiber !== null) return fiber;
64
+ current = current.parentElement;
65
+ depth++;
66
+ }
67
+ return null;
68
+ }
69
+ function getFiberFromElement(element) {
70
+ if (cachedKey !== null) {
71
+ const fiber = element[cachedKey];
72
+ if (fiber != null) return fiber;
73
+ return null;
74
+ }
75
+ for (const key of Object.keys(element)) for (const prefix of FIBER_PREFIXES) if (key.startsWith(prefix)) {
76
+ cachedKey = key;
77
+ return element[key];
78
+ }
79
+ return null;
80
+ }
81
+ const MAX_STACK_DEPTH = 50;
82
+ function extractFiberInfo(rawFiber) {
83
+ if (rawFiber === null) return null;
84
+ const fiber = rawFiber;
85
+ return {
86
+ componentName: findNearestComponentName(fiber),
87
+ props: fiber.memoizedProps ?? {}
88
+ };
89
+ }
90
+ function findNearestComponentName(fiber) {
91
+ let current = fiber.return;
92
+ let depth = 0;
93
+ while (current !== null && depth < MAX_STACK_DEPTH) {
94
+ const name = getComponentName(current);
95
+ if (name !== null) return name;
96
+ current = current.return;
97
+ depth++;
98
+ }
99
+ return null;
100
+ }
101
+ function getComponentName(fiber) {
102
+ const type = fiber.type;
103
+ if (typeof type === "string") return null;
104
+ if (typeof type === "function") return type.displayName ?? type.name ?? null;
105
+ return null;
106
+ }
107
+
108
+ //#endregion
109
+ //#region src/utils/safe-matches.ts
110
+ function safeMatches(element, selector) {
111
+ try {
112
+ return element.matches(selector);
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ //#endregion
119
+ //#region src/filter/filter-engine.ts
120
+ const INTERACTIVE_TAGS = new Set([
121
+ "BUTTON",
122
+ "A",
123
+ "INPUT",
124
+ "SELECT",
125
+ "TEXTAREA",
126
+ "SUMMARY",
127
+ "DETAILS"
128
+ ]);
129
+ const INTERACTIVE_ROLES = new Set([
130
+ "button",
131
+ "link",
132
+ "menuitem",
133
+ "tab",
134
+ "checkbox",
135
+ "radio",
136
+ "combobox",
137
+ "listbox",
138
+ "option",
139
+ "switch",
140
+ "slider",
141
+ "spinbutton",
142
+ "menuitemcheckbox",
143
+ "menuitemradio",
144
+ "treeitem",
145
+ "gridcell",
146
+ "textbox",
147
+ "searchbox"
148
+ ]);
149
+ const MAX_ANCESTOR_DEPTH = 10;
150
+ function findTrackableElement(params) {
151
+ const { target, ignoreSelectors, eventType } = params;
152
+ switch (getEventCategory(eventType)) {
153
+ case EventCategory.Pointer: return findPointerTarget(target, ignoreSelectors, eventType);
154
+ case EventCategory.Form: return findFormTarget(target, ignoreSelectors);
155
+ case EventCategory.Ambient: return findAmbientTarget(target, ignoreSelectors);
156
+ }
157
+ }
158
+ function findPointerTarget(target, ignoreSelectors, eventType) {
159
+ let current = target;
160
+ let depth = 0;
161
+ while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {
162
+ if (isIgnored(current, ignoreSelectors)) return null;
163
+ if (isDisabled(current)) return null;
164
+ const rawFiber = findInteractiveFiber(current, eventType);
165
+ if (rawFiber !== void 0) return {
166
+ element: current,
167
+ fiber: extractFiberInfo(rawFiber)
168
+ };
169
+ current = current.parentElement;
170
+ depth++;
171
+ }
172
+ return null;
173
+ }
174
+ function findFormTarget(target, ignoreSelectors) {
175
+ if (isIgnored(target, ignoreSelectors)) return null;
176
+ if (isDisabled(target)) return null;
177
+ return {
178
+ element: target,
179
+ fiber: extractFiberInfo(resolveFiber(target))
180
+ };
181
+ }
182
+ function findAmbientTarget(target, ignoreSelectors) {
183
+ if (isIgnored(target, ignoreSelectors)) return null;
184
+ return {
185
+ element: target,
186
+ fiber: extractFiberInfo(resolveFiber(target))
187
+ };
188
+ }
189
+ /**
190
+ * Returns the raw fiber if the element is interactive, or undefined if not.
191
+ * null means "interactive but no fiber found" (semantic tag / ARIA role).
192
+ */
193
+ function findInteractiveFiber(el, eventType) {
194
+ if (INTERACTIVE_TAGS.has(el.tagName)) return resolveFiber(el);
195
+ const role = el.getAttribute("role");
196
+ if (role !== null && INTERACTIVE_ROLES.has(role)) return resolveFiber(el);
197
+ const handlers = getHandlersForEvent(eventType);
198
+ if (handlers.length > 0) {
199
+ const fiber = resolveFiber(el);
200
+ if (fiber !== null) {
201
+ const props = fiber.memoizedProps;
202
+ if (props !== null && props !== void 0) {
203
+ for (const handler of handlers) if (typeof props[handler] === "function") return fiber;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ function isIgnored(element, ignoreSelectors) {
209
+ return ignoreSelectors.some((selector) => safeMatches(element, selector));
210
+ }
211
+ function isDisabled(el) {
212
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
213
+ }
214
+
215
+ //#endregion
216
+ //#region src/utils/debounce.ts
217
+ function debounce(fn, ms) {
218
+ let timeoutId = null;
219
+ const debounced = (...args) => {
220
+ if (timeoutId !== null) clearTimeout(timeoutId);
221
+ timeoutId = setTimeout(() => {
222
+ timeoutId = null;
223
+ fn(...args);
224
+ }, ms);
225
+ };
226
+ debounced.cancel = () => {
227
+ if (timeoutId !== null) {
228
+ clearTimeout(timeoutId);
229
+ timeoutId = null;
230
+ }
231
+ };
232
+ return debounced;
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/utils/throttle.ts
237
+ function throttle(fn, ms) {
238
+ let lastCallTime = 0;
239
+ let timeoutId = null;
240
+ let lastArgs = null;
241
+ const throttled = (...args) => {
242
+ const now = Date.now();
243
+ const elapsed = now - lastCallTime;
244
+ if (elapsed >= ms) {
245
+ lastCallTime = now;
246
+ fn(...args);
247
+ } else {
248
+ lastArgs = args;
249
+ if (timeoutId === null) timeoutId = setTimeout(() => {
250
+ timeoutId = null;
251
+ lastCallTime = Date.now();
252
+ if (lastArgs !== null) {
253
+ fn(...lastArgs);
254
+ lastArgs = null;
255
+ }
256
+ }, ms - elapsed);
257
+ }
258
+ };
259
+ throttled.cancel = () => {
260
+ if (timeoutId !== null) {
261
+ clearTimeout(timeoutId);
262
+ timeoutId = null;
263
+ }
264
+ lastArgs = null;
265
+ };
266
+ return throttled;
267
+ }
268
+
269
+ //#endregion
270
+ //#region src/core/registry.ts
271
+ function createRegistry() {
272
+ let entries = [];
273
+ function createEntry(eventType, callback, options) {
274
+ const wrappedCallback = wrapCallback(callback, options);
275
+ const entry = {
276
+ eventType,
277
+ wrappedCallback,
278
+ options,
279
+ unsubscribe: () => {
280
+ wrappedCallback.cancel?.();
281
+ entries = entries.filter((e) => e !== entry);
282
+ }
283
+ };
284
+ return entry;
285
+ }
286
+ return {
287
+ add(eventType, callback, options) {
288
+ const entry = createEntry(eventType, callback, options);
289
+ entries = [...entries, entry];
290
+ return entry.unsubscribe;
291
+ },
292
+ invoke(event) {
293
+ for (const entry of entries) {
294
+ if (entry.eventType !== event.nativeEvent.type) continue;
295
+ if (entry.options.selector != null) {
296
+ if (!safeMatches(event.targetElement, entry.options.selector)) continue;
297
+ }
298
+ entry.wrappedCallback(event);
299
+ if (entry.options.once === true) entry.unsubscribe();
300
+ }
301
+ },
302
+ getEventTypes() {
303
+ return new Set(entries.map((e) => e.eventType));
304
+ },
305
+ clear() {
306
+ for (const entry of entries) entry.wrappedCallback.cancel?.();
307
+ entries = [];
308
+ }
309
+ };
310
+ }
311
+ function wrapCallback(callback, options) {
312
+ if (options.debounce != null) return debounce(callback, options.debounce);
313
+ if (options.throttle != null) return throttle(callback, options.throttle);
314
+ return callback;
315
+ }
316
+
317
+ //#endregion
318
+ //#region src/core/pipeline.ts
319
+ function createPipeline(config) {
320
+ const registry = createRegistry();
321
+ let lastEvent = null;
322
+ return {
323
+ handleEvent(domEvent) {
324
+ const target = domEvent.target;
325
+ if (!(target instanceof Element)) return;
326
+ const result = findTrackableElement({
327
+ target,
328
+ ignoreSelectors: config.ignoreSelectors,
329
+ eventType: domEvent.type
330
+ });
331
+ if (result === null) return;
332
+ const trackEvent = {
333
+ nativeEvent: domEvent,
334
+ targetElement: result.element,
335
+ fiber: result.fiber
336
+ };
337
+ registry.invoke(trackEvent);
338
+ lastEvent = trackEvent;
339
+ },
340
+ getLastEvent() {
341
+ return lastEvent;
342
+ },
343
+ addListener(eventType, callback, options) {
344
+ return registry.add(eventType, callback, options);
345
+ },
346
+ getEventTypes() {
347
+ return registry.getEventTypes();
348
+ },
349
+ clear() {
350
+ registry.clear();
351
+ }
352
+ };
353
+ }
354
+
355
+ //#endregion
356
+ //#region src/core/tracker.ts
357
+ function createTracker(config) {
358
+ const enabled = config?.enabled ?? true;
359
+ const ignoreSelectors = config?.ignoreSelectors ?? [];
360
+ const debug = config?.debug ?? false;
361
+ if (!enabled) return {
362
+ on: () => () => {},
363
+ getLastEvent: () => null,
364
+ destroy: () => {}
365
+ };
366
+ const pipeline = createPipeline({ ignoreSelectors });
367
+ const domListeners = /* @__PURE__ */ new Map();
368
+ let destroyed = false;
369
+ function ensureDomListener(eventType) {
370
+ if (domListeners.has(eventType)) return;
371
+ const handler = (event) => {
372
+ pipeline.handleEvent(event);
373
+ if (debug) {
374
+ const lastEvent = pipeline.getLastEvent();
375
+ if (lastEvent?.nativeEvent === event) console.debug("[react-auto-tracking]", lastEvent);
376
+ }
377
+ };
378
+ document.addEventListener(eventType, handler, true);
379
+ domListeners.set(eventType, handler);
380
+ }
381
+ function removeDomListener(eventType) {
382
+ const handler = domListeners.get(eventType);
383
+ if (handler === void 0) return;
384
+ if (!pipeline.getEventTypes().has(eventType)) {
385
+ document.removeEventListener(eventType, handler, true);
386
+ domListeners.delete(eventType);
387
+ }
388
+ }
389
+ return {
390
+ on(eventType, callback, options) {
391
+ if (destroyed) return () => {};
392
+ ensureDomListener(eventType);
393
+ const unsub = pipeline.addListener(eventType, callback, options ?? {});
394
+ return () => {
395
+ unsub();
396
+ removeDomListener(eventType);
397
+ };
398
+ },
399
+ getLastEvent() {
400
+ return pipeline.getLastEvent();
401
+ },
402
+ destroy() {
403
+ if (destroyed) return;
404
+ destroyed = true;
405
+ pipeline.clear();
406
+ for (const [eventType, handler] of domListeners) document.removeEventListener(eventType, handler, true);
407
+ domListeners.clear();
408
+ }
409
+ };
410
+ }
411
+
412
+ //#endregion
413
+ export { createTracker as init };
414
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/filter/event-categories.ts","../src/extract/fiber.ts","../src/utils/safe-matches.ts","../src/filter/filter-engine.ts","../src/utils/debounce.ts","../src/utils/throttle.ts","../src/core/registry.ts","../src/core/pipeline.ts","../src/core/tracker.ts"],"sourcesContent":["export const EventCategory = {\n Pointer: 'pointer',\n Form: 'form',\n Ambient: 'ambient',\n} as const\n\nexport type EventCategory = (typeof EventCategory)[keyof typeof EventCategory]\n\n// Maps DOM event types to React handler prop names used to detect interactivity.\n// For click, we include mouse/pointer handlers because an element with only\n// onMouseDown (no onClick) is still interactive from a tracking perspective.\nconst EVENT_HANDLER_MAP: Readonly<Record<string, readonly string[]>> = {\n // Pointer — includes related mouse/pointer handlers to catch all interactive patterns\n click: ['onClick', 'onMouseDown', 'onMouseUp', 'onPointerDown', 'onPointerUp'],\n touchstart: ['onTouchStart'],\n touchend: ['onTouchEnd'],\n\n // Form\n input: ['onChange', 'onInput'],\n change: ['onChange'],\n focus: ['onFocus'],\n blur: ['onBlur'],\n submit: ['onSubmit'],\n\n // Ambient\n scroll: ['onScroll'],\n keydown: ['onKeyDown'],\n keyup: ['onKeyUp'],\n copy: ['onCopy'],\n paste: ['onPaste'],\n}\n\nconst CATEGORY_MAP: Readonly<Record<string, EventCategory>> = {\n click: EventCategory.Pointer,\n touchstart: EventCategory.Pointer,\n touchend: EventCategory.Pointer,\n\n input: EventCategory.Form,\n change: EventCategory.Form,\n focus: EventCategory.Form,\n blur: EventCategory.Form,\n submit: EventCategory.Form,\n\n scroll: EventCategory.Ambient,\n keydown: EventCategory.Ambient,\n keyup: EventCategory.Ambient,\n copy: EventCategory.Ambient,\n paste: EventCategory.Ambient,\n resize: EventCategory.Ambient,\n popstate: EventCategory.Ambient,\n hashchange: EventCategory.Ambient,\n}\n\nexport function getEventCategory(eventType: string): EventCategory {\n return CATEGORY_MAP[eventType] ?? EventCategory.Ambient\n}\n\nexport function getHandlersForEvent(eventType: string): readonly string[] {\n return EVENT_HANDLER_MAP[eventType] ?? []\n}\n","import type { FiberInfo } from '../types'\n\n// === Resolver: DOM element → raw fiber node ===\n\nconst FIBER_PREFIXES = ['__reactFiber$', '__reactInternalInstance$'] as const\nconst MAX_PARENT_DEPTH = 10\n\nlet cachedKey: string | null = null\n\nexport function resolveFiber(element: Element): object | null {\n let current: Element | null = element\n let depth = 0\n\n while (current !== null && depth <= MAX_PARENT_DEPTH) {\n const fiber = getFiberFromElement(current)\n if (fiber !== null) {\n return fiber\n }\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction getFiberFromElement(element: Element): object | null {\n if (cachedKey !== null) {\n const fiber = (element as any)[cachedKey]\n if (fiber != null) return fiber as object\n return null\n }\n\n for (const key of Object.keys(element)) {\n for (const prefix of FIBER_PREFIXES) {\n if (key.startsWith(prefix)) {\n cachedKey = key\n return (element as any)[key] as object\n }\n }\n }\n\n return null\n}\n\nexport function resetFiberKeyCache(): void {\n cachedKey = null\n}\n\n// === Extractor: raw fiber node → FiberInfo ===\n\nconst MAX_STACK_DEPTH = 50\n\ninterface FiberNode {\n type: unknown\n memoizedProps: Record<string, unknown> | null\n return: FiberNode | null\n}\n\nexport function extractFiberInfo(rawFiber: object | null): FiberInfo | null {\n if (rawFiber === null) return null\n\n const fiber = rawFiber as FiberNode\n return {\n componentName: findNearestComponentName(fiber),\n props: fiber.memoizedProps ?? {},\n }\n}\n\nfunction findNearestComponentName(fiber: FiberNode): string | null {\n let current: FiberNode | null = fiber.return\n let depth = 0\n\n while (current !== null && depth < MAX_STACK_DEPTH) {\n const name = getComponentName(current)\n if (name !== null) return name\n current = current.return\n depth++\n }\n\n return null\n}\n\nfunction getComponentName(fiber: FiberNode): string | null {\n const type = fiber.type\n if (typeof type === 'string') return null\n if (typeof type === 'function') {\n return (type as any).displayName ?? type.name ?? null\n }\n return null\n}\n","export function safeMatches(element: Element, selector: string): boolean {\n try {\n return element.matches(selector)\n } catch {\n return false\n }\n}\n","import type { FiberInfo } from '../types'\nimport { getEventCategory, getHandlersForEvent, EventCategory } from './event-categories'\nimport { resolveFiber, extractFiberInfo } from '../extract/fiber'\nimport { safeMatches } from '../utils/safe-matches'\n\nconst INTERACTIVE_TAGS = new Set([\n 'BUTTON',\n 'A',\n 'INPUT',\n 'SELECT',\n 'TEXTAREA',\n 'SUMMARY',\n 'DETAILS',\n])\n\nconst INTERACTIVE_ROLES = new Set([\n // Original widget roles\n 'button',\n 'link',\n 'menuitem',\n 'tab',\n 'checkbox',\n 'radio',\n 'combobox',\n 'listbox',\n 'option',\n 'switch',\n 'slider',\n 'spinbutton',\n // Composite widget variants\n 'menuitemcheckbox',\n 'menuitemradio',\n 'treeitem',\n 'gridcell',\n // Input widget roles\n 'textbox',\n 'searchbox',\n])\n\nconst MAX_ANCESTOR_DEPTH = 10\n\nexport interface FilterResult {\n readonly element: Element\n readonly fiber: FiberInfo | null\n}\n\ninterface FindTrackableParams {\n readonly target: Element\n readonly ignoreSelectors: readonly string[]\n readonly eventType: string\n}\n\nexport function findTrackableElement(params: FindTrackableParams): FilterResult | null {\n const { target, ignoreSelectors, eventType } = params\n const category = getEventCategory(eventType)\n\n switch (category) {\n case EventCategory.Pointer:\n return findPointerTarget(target, ignoreSelectors, eventType)\n case EventCategory.Form:\n return findFormTarget(target, ignoreSelectors)\n case EventCategory.Ambient:\n return findAmbientTarget(target, ignoreSelectors)\n }\n}\n\nfunction findPointerTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n eventType: string,\n): FilterResult | null {\n let current: Element | null = target\n let depth = 0\n\n while (current !== null && depth <= MAX_ANCESTOR_DEPTH) {\n if (isIgnored(current, ignoreSelectors)) return null\n if (isDisabled(current)) return null\n\n const rawFiber = findInteractiveFiber(current, eventType)\n if (rawFiber !== undefined) {\n return { element: current, fiber: extractFiberInfo(rawFiber) }\n }\n\n current = current.parentElement\n depth++\n }\n\n return null\n}\n\nfunction findFormTarget(target: Element, ignoreSelectors: readonly string[]): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n if (isDisabled(target)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\nfunction findAmbientTarget(\n target: Element,\n ignoreSelectors: readonly string[],\n): FilterResult | null {\n if (isIgnored(target, ignoreSelectors)) return null\n return { element: target, fiber: extractFiberInfo(resolveFiber(target)) }\n}\n\n/**\n * Returns the raw fiber if the element is interactive, or undefined if not.\n * null means \"interactive but no fiber found\" (semantic tag / ARIA role).\n */\nfunction findInteractiveFiber(el: Element, eventType: string): object | null | undefined {\n // 1. Semantic tag\n if (INTERACTIVE_TAGS.has(el.tagName)) return resolveFiber(el)\n\n // 2. ARIA role\n const role = el.getAttribute('role')\n if (role !== null && INTERACTIVE_ROLES.has(role)) return resolveFiber(el)\n\n // 3. React event handler via fiber\n const handlers = getHandlersForEvent(eventType)\n if (handlers.length > 0) {\n const fiber = resolveFiber(el)\n if (fiber !== null) {\n const props = (fiber as any).memoizedProps\n if (props !== null && props !== undefined) {\n for (const handler of handlers) {\n if (typeof props[handler] === 'function') return fiber\n }\n }\n }\n }\n\n return undefined\n}\n\nexport function isIgnored(element: Element, ignoreSelectors: readonly string[]): boolean {\n return ignoreSelectors.some((selector) => safeMatches(element, selector))\n}\n\nexport function isDisabled(el: Element): boolean {\n return el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true'\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface DebouncedFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): DebouncedFn<T> {\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n\n const debounced = (...args: Parameters<T>): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n }\n timeoutId = setTimeout(() => {\n timeoutId = null\n fn(...args)\n }, ms)\n }\n\n debounced.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n }\n\n return debounced\n}\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\ninterface ThrottledFn<T extends (...args: any[]) => void> {\n (...args: Parameters<T>): void\n cancel(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function throttle<T extends (...args: any[]) => void>(fn: T, ms: number): ThrottledFn<T> {\n let lastCallTime = 0\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n let lastArgs: Parameters<T> | null = null\n\n const throttled = (...args: Parameters<T>): void => {\n const now = Date.now()\n const elapsed = now - lastCallTime\n\n if (elapsed >= ms) {\n lastCallTime = now\n fn(...args)\n } else {\n lastArgs = args\n if (timeoutId === null) {\n timeoutId = setTimeout(() => {\n timeoutId = null\n lastCallTime = Date.now()\n if (lastArgs !== null) {\n fn(...lastArgs)\n lastArgs = null\n }\n }, ms - elapsed)\n }\n }\n }\n\n throttled.cancel = (): void => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n lastArgs = null\n }\n\n return throttled\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { debounce } from '../utils/debounce'\nimport { throttle } from '../utils/throttle'\nimport { safeMatches } from '../utils/safe-matches'\n\ninterface RegistryEntry {\n readonly eventType: string\n readonly wrappedCallback: TrackCallback & { cancel?: () => void }\n readonly options: ListenerOptions\n readonly unsubscribe: () => void\n}\n\nexport interface Registry {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n invoke(event: TrackEvent): void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport function createRegistry(): Registry {\n let entries: RegistryEntry[] = []\n\n function createEntry(\n eventType: string,\n callback: TrackCallback,\n options: ListenerOptions,\n ): RegistryEntry {\n const wrappedCallback = wrapCallback(callback, options)\n const entry: RegistryEntry = {\n eventType,\n wrappedCallback,\n options,\n unsubscribe: () => {\n wrappedCallback.cancel?.()\n entries = entries.filter((e) => e !== entry)\n },\n }\n return entry\n }\n\n return {\n add(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n const entry = createEntry(eventType, callback, options)\n entries = [...entries, entry]\n return entry.unsubscribe\n },\n\n invoke(event: TrackEvent): void {\n for (const entry of entries) {\n if (entry.eventType !== event.nativeEvent.type) continue\n\n if (entry.options.selector != null) {\n if (!safeMatches(event.targetElement, entry.options.selector)) {\n continue\n }\n }\n\n entry.wrappedCallback(event)\n\n // once: auto-unsubscribe after first fire\n if (entry.options.once === true) {\n entry.unsubscribe()\n }\n }\n },\n\n getEventTypes(): Set<string> {\n return new Set(entries.map((e) => e.eventType))\n },\n\n clear(): void {\n for (const entry of entries) {\n entry.wrappedCallback.cancel?.()\n }\n entries = []\n },\n }\n}\n\nfunction wrapCallback(\n callback: TrackCallback,\n options: ListenerOptions,\n): TrackCallback & { cancel?: () => void } {\n if (options.debounce != null) {\n return debounce(callback, options.debounce)\n }\n if (options.throttle != null) {\n return throttle(callback, options.throttle)\n }\n return callback\n}\n","import type { TrackEvent, TrackCallback, ListenerOptions } from '../types'\nimport { findTrackableElement } from '../filter/filter-engine'\nimport { createRegistry } from './registry'\n\nexport interface Pipeline {\n handleEvent(domEvent: Event): void\n getLastEvent(): TrackEvent | null\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void\n getEventTypes(): Set<string>\n clear(): void\n}\n\nexport interface PipelineConfig {\n readonly ignoreSelectors: readonly string[]\n}\n\nexport function createPipeline(config: PipelineConfig): Pipeline {\n const registry = createRegistry()\n let lastEvent: TrackEvent | null = null\n\n return {\n handleEvent(domEvent: Event): void {\n const target = domEvent.target\n if (!(target instanceof Element)) return\n\n const result = findTrackableElement({\n target,\n ignoreSelectors: config.ignoreSelectors,\n eventType: domEvent.type,\n })\n if (result === null) return\n\n const trackEvent: TrackEvent = {\n nativeEvent: domEvent,\n targetElement: result.element,\n fiber: result.fiber,\n }\n\n registry.invoke(trackEvent)\n lastEvent = trackEvent\n },\n\n getLastEvent(): TrackEvent | null {\n return lastEvent\n },\n\n addListener(eventType: string, callback: TrackCallback, options: ListenerOptions): () => void {\n return registry.add(eventType, callback, options)\n },\n\n getEventTypes(): Set<string> {\n return registry.getEventTypes()\n },\n\n clear(): void {\n registry.clear()\n },\n }\n}\n","import type { Tracker, TrackerConfig, TrackCallback, ListenerOptions } from '../types'\nimport { createPipeline } from './pipeline'\n\nexport function createTracker(config?: TrackerConfig): Tracker {\n const enabled = config?.enabled ?? true\n const ignoreSelectors = config?.ignoreSelectors ?? []\n const debug = config?.debug ?? false\n\n if (!enabled) {\n return {\n on: () => () => {},\n getLastEvent: () => null,\n destroy: () => {},\n }\n }\n\n const pipeline = createPipeline({ ignoreSelectors })\n const domListeners = new Map<string, (event: Event) => void>()\n let destroyed = false\n\n // Lazily attaches one capture-phase listener per event type on document.\n // Multiple on() calls for the same type share a single DOM listener;\n // the pipeline fans out to all registered callbacks internally.\n function ensureDomListener(eventType: string): void {\n if (domListeners.has(eventType)) return\n\n const handler = (event: Event): void => {\n pipeline.handleEvent(event)\n\n if (debug) {\n const lastEvent = pipeline.getLastEvent()\n if (lastEvent?.nativeEvent === event) {\n console.debug('[react-auto-tracking]', lastEvent)\n }\n }\n }\n\n document.addEventListener(eventType, handler, true)\n domListeners.set(eventType, handler)\n }\n\n function removeDomListener(eventType: string): void {\n const handler = domListeners.get(eventType)\n if (handler === undefined) return\n\n // Only remove if no more listeners for this type\n if (!pipeline.getEventTypes().has(eventType)) {\n document.removeEventListener(eventType, handler, true)\n domListeners.delete(eventType)\n }\n }\n\n return {\n on(eventType: string, callback: TrackCallback, options?: ListenerOptions): () => void {\n if (destroyed) return () => {}\n\n ensureDomListener(eventType)\n const unsub = pipeline.addListener(eventType, callback, options ?? {})\n\n return () => {\n unsub()\n removeDomListener(eventType)\n }\n },\n\n getLastEvent() {\n return pipeline.getLastEvent()\n },\n\n destroy(): void {\n if (destroyed) return\n destroyed = true\n\n pipeline.clear()\n\n for (const [eventType, handler] of domListeners) {\n document.removeEventListener(eventType, handler, true)\n }\n domListeners.clear()\n },\n }\n}\n"],"mappings":";AAAA,MAAa,gBAAgB;CAC3B,SAAS;CACT,MAAM;CACN,SAAS;CACV;AAOD,MAAM,oBAAiE;CAErE,OAAO;EAAC;EAAW;EAAe;EAAa;EAAiB;EAAc;CAC9E,YAAY,CAAC,eAAe;CAC5B,UAAU,CAAC,aAAa;CAGxB,OAAO,CAAC,YAAY,UAAU;CAC9B,QAAQ,CAAC,WAAW;CACpB,OAAO,CAAC,UAAU;CAClB,MAAM,CAAC,SAAS;CAChB,QAAQ,CAAC,WAAW;CAGpB,QAAQ,CAAC,WAAW;CACpB,SAAS,CAAC,YAAY;CACtB,OAAO,CAAC,UAAU;CAClB,MAAM,CAAC,SAAS;CAChB,OAAO,CAAC,UAAU;CACnB;AAED,MAAM,eAAwD;CAC5D,OAAO,cAAc;CACrB,YAAY,cAAc;CAC1B,UAAU,cAAc;CAExB,OAAO,cAAc;CACrB,QAAQ,cAAc;CACtB,OAAO,cAAc;CACrB,MAAM,cAAc;CACpB,QAAQ,cAAc;CAEtB,QAAQ,cAAc;CACtB,SAAS,cAAc;CACvB,OAAO,cAAc;CACrB,MAAM,cAAc;CACpB,OAAO,cAAc;CACrB,QAAQ,cAAc;CACtB,UAAU,cAAc;CACxB,YAAY,cAAc;CAC3B;AAED,SAAgB,iBAAiB,WAAkC;AACjE,QAAO,aAAa,cAAc,cAAc;;AAGlD,SAAgB,oBAAoB,WAAsC;AACxE,QAAO,kBAAkB,cAAc,EAAE;;;;;ACtD3C,MAAM,iBAAiB,CAAC,iBAAiB,2BAA2B;AACpE,MAAM,mBAAmB;AAEzB,IAAI,YAA2B;AAE/B,SAAgB,aAAa,SAAiC;CAC5D,IAAI,UAA0B;CAC9B,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,SAAS,kBAAkB;EACpD,MAAM,QAAQ,oBAAoB,QAAQ;AAC1C,MAAI,UAAU,KACZ,QAAO;AAET,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,oBAAoB,SAAiC;AAC5D,KAAI,cAAc,MAAM;EACtB,MAAM,QAAS,QAAgB;AAC/B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO;;AAGT,MAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,CACpC,MAAK,MAAM,UAAU,eACnB,KAAI,IAAI,WAAW,OAAO,EAAE;AAC1B,cAAY;AACZ,SAAQ,QAAgB;;AAK9B,QAAO;;AAST,MAAM,kBAAkB;AAQxB,SAAgB,iBAAiB,UAA2C;AAC1E,KAAI,aAAa,KAAM,QAAO;CAE9B,MAAM,QAAQ;AACd,QAAO;EACL,eAAe,yBAAyB,MAAM;EAC9C,OAAO,MAAM,iBAAiB,EAAE;EACjC;;AAGH,SAAS,yBAAyB,OAAiC;CACjE,IAAI,UAA4B,MAAM;CACtC,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,QAAQ,iBAAiB;EAClD,MAAM,OAAO,iBAAiB,QAAQ;AACtC,MAAI,SAAS,KAAM,QAAO;AAC1B,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,iBAAiB,OAAiC;CACzD,MAAM,OAAO,MAAM;AACnB,KAAI,OAAO,SAAS,SAAU,QAAO;AACrC,KAAI,OAAO,SAAS,WAClB,QAAQ,KAAa,eAAe,KAAK,QAAQ;AAEnD,QAAO;;;;;ACxFT,SAAgB,YAAY,SAAkB,UAA2B;AACvE,KAAI;AACF,SAAO,QAAQ,QAAQ,SAAS;SAC1B;AACN,SAAO;;;;;;ACCX,MAAM,mBAAmB,IAAI,IAAI;CAC/B;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,MAAM,oBAAoB,IAAI,IAAI;CAEhC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CAEA;CACA;CACD,CAAC;AAEF,MAAM,qBAAqB;AAa3B,SAAgB,qBAAqB,QAAkD;CACrF,MAAM,EAAE,QAAQ,iBAAiB,cAAc;AAG/C,SAFiB,iBAAiB,UAAU,EAE5C;EACE,KAAK,cAAc,QACjB,QAAO,kBAAkB,QAAQ,iBAAiB,UAAU;EAC9D,KAAK,cAAc,KACjB,QAAO,eAAe,QAAQ,gBAAgB;EAChD,KAAK,cAAc,QACjB,QAAO,kBAAkB,QAAQ,gBAAgB;;;AAIvD,SAAS,kBACP,QACA,iBACA,WACqB;CACrB,IAAI,UAA0B;CAC9B,IAAI,QAAQ;AAEZ,QAAO,YAAY,QAAQ,SAAS,oBAAoB;AACtD,MAAI,UAAU,SAAS,gBAAgB,CAAE,QAAO;AAChD,MAAI,WAAW,QAAQ,CAAE,QAAO;EAEhC,MAAM,WAAW,qBAAqB,SAAS,UAAU;AACzD,MAAI,aAAa,OACf,QAAO;GAAE,SAAS;GAAS,OAAO,iBAAiB,SAAS;GAAE;AAGhE,YAAU,QAAQ;AAClB;;AAGF,QAAO;;AAGT,SAAS,eAAe,QAAiB,iBAAyD;AAChG,KAAI,UAAU,QAAQ,gBAAgB,CAAE,QAAO;AAC/C,KAAI,WAAW,OAAO,CAAE,QAAO;AAC/B,QAAO;EAAE,SAAS;EAAQ,OAAO,iBAAiB,aAAa,OAAO,CAAC;EAAE;;AAG3E,SAAS,kBACP,QACA,iBACqB;AACrB,KAAI,UAAU,QAAQ,gBAAgB,CAAE,QAAO;AAC/C,QAAO;EAAE,SAAS;EAAQ,OAAO,iBAAiB,aAAa,OAAO,CAAC;EAAE;;;;;;AAO3E,SAAS,qBAAqB,IAAa,WAA8C;AAEvF,KAAI,iBAAiB,IAAI,GAAG,QAAQ,CAAE,QAAO,aAAa,GAAG;CAG7D,MAAM,OAAO,GAAG,aAAa,OAAO;AACpC,KAAI,SAAS,QAAQ,kBAAkB,IAAI,KAAK,CAAE,QAAO,aAAa,GAAG;CAGzE,MAAM,WAAW,oBAAoB,UAAU;AAC/C,KAAI,SAAS,SAAS,GAAG;EACvB,MAAM,QAAQ,aAAa,GAAG;AAC9B,MAAI,UAAU,MAAM;GAClB,MAAM,QAAS,MAAc;AAC7B,OAAI,UAAU,QAAQ,UAAU,QAC9B;SAAK,MAAM,WAAW,SACpB,KAAI,OAAO,MAAM,aAAa,WAAY,QAAO;;;;;AAS3D,SAAgB,UAAU,SAAkB,iBAA6C;AACvF,QAAO,gBAAgB,MAAM,aAAa,YAAY,SAAS,SAAS,CAAC;;AAG3E,SAAgB,WAAW,IAAsB;AAC/C,QAAO,GAAG,aAAa,WAAW,IAAI,GAAG,aAAa,gBAAgB,KAAK;;;;;ACnI7E,SAAgB,SAA6C,IAAO,IAA4B;CAC9F,IAAI,YAAkD;CAEtD,MAAM,aAAa,GAAG,SAA8B;AAClD,MAAI,cAAc,KAChB,cAAa,UAAU;AAEzB,cAAY,iBAAiB;AAC3B,eAAY;AACZ,MAAG,GAAG,KAAK;KACV,GAAG;;AAGR,WAAU,eAAqB;AAC7B,MAAI,cAAc,MAAM;AACtB,gBAAa,UAAU;AACvB,eAAY;;;AAIhB,QAAO;;;;;ACpBT,SAAgB,SAA6C,IAAO,IAA4B;CAC9F,IAAI,eAAe;CACnB,IAAI,YAAkD;CACtD,IAAI,WAAiC;CAErC,MAAM,aAAa,GAAG,SAA8B;EAClD,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,UAAU,MAAM;AAEtB,MAAI,WAAW,IAAI;AACjB,kBAAe;AACf,MAAG,GAAG,KAAK;SACN;AACL,cAAW;AACX,OAAI,cAAc,KAChB,aAAY,iBAAiB;AAC3B,gBAAY;AACZ,mBAAe,KAAK,KAAK;AACzB,QAAI,aAAa,MAAM;AACrB,QAAG,GAAG,SAAS;AACf,gBAAW;;MAEZ,KAAK,QAAQ;;;AAKtB,WAAU,eAAqB;AAC7B,MAAI,cAAc,MAAM;AACtB,gBAAa,UAAU;AACvB,eAAY;;AAEd,aAAW;;AAGb,QAAO;;;;;ACvBT,SAAgB,iBAA2B;CACzC,IAAI,UAA2B,EAAE;CAEjC,SAAS,YACP,WACA,UACA,SACe;EACf,MAAM,kBAAkB,aAAa,UAAU,QAAQ;EACvD,MAAM,QAAuB;GAC3B;GACA;GACA;GACA,mBAAmB;AACjB,oBAAgB,UAAU;AAC1B,cAAU,QAAQ,QAAQ,MAAM,MAAM,MAAM;;GAE/C;AACD,SAAO;;AAGT,QAAO;EACL,IAAI,WAAmB,UAAyB,SAAsC;GACpF,MAAM,QAAQ,YAAY,WAAW,UAAU,QAAQ;AACvD,aAAU,CAAC,GAAG,SAAS,MAAM;AAC7B,UAAO,MAAM;;EAGf,OAAO,OAAyB;AAC9B,QAAK,MAAM,SAAS,SAAS;AAC3B,QAAI,MAAM,cAAc,MAAM,YAAY,KAAM;AAEhD,QAAI,MAAM,QAAQ,YAAY,MAC5B;SAAI,CAAC,YAAY,MAAM,eAAe,MAAM,QAAQ,SAAS,CAC3D;;AAIJ,UAAM,gBAAgB,MAAM;AAG5B,QAAI,MAAM,QAAQ,SAAS,KACzB,OAAM,aAAa;;;EAKzB,gBAA6B;AAC3B,UAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,UAAU,CAAC;;EAGjD,QAAc;AACZ,QAAK,MAAM,SAAS,QAClB,OAAM,gBAAgB,UAAU;AAElC,aAAU,EAAE;;EAEf;;AAGH,SAAS,aACP,UACA,SACyC;AACzC,KAAI,QAAQ,YAAY,KACtB,QAAO,SAAS,UAAU,QAAQ,SAAS;AAE7C,KAAI,QAAQ,YAAY,KACtB,QAAO,SAAS,UAAU,QAAQ,SAAS;AAE7C,QAAO;;;;;ACzET,SAAgB,eAAe,QAAkC;CAC/D,MAAM,WAAW,gBAAgB;CACjC,IAAI,YAA+B;AAEnC,QAAO;EACL,YAAY,UAAuB;GACjC,MAAM,SAAS,SAAS;AACxB,OAAI,EAAE,kBAAkB,SAAU;GAElC,MAAM,SAAS,qBAAqB;IAClC;IACA,iBAAiB,OAAO;IACxB,WAAW,SAAS;IACrB,CAAC;AACF,OAAI,WAAW,KAAM;GAErB,MAAM,aAAyB;IAC7B,aAAa;IACb,eAAe,OAAO;IACtB,OAAO,OAAO;IACf;AAED,YAAS,OAAO,WAAW;AAC3B,eAAY;;EAGd,eAAkC;AAChC,UAAO;;EAGT,YAAY,WAAmB,UAAyB,SAAsC;AAC5F,UAAO,SAAS,IAAI,WAAW,UAAU,QAAQ;;EAGnD,gBAA6B;AAC3B,UAAO,SAAS,eAAe;;EAGjC,QAAc;AACZ,YAAS,OAAO;;EAEnB;;;;;ACtDH,SAAgB,cAAc,QAAiC;CAC7D,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,kBAAkB,QAAQ,mBAAmB,EAAE;CACrD,MAAM,QAAQ,QAAQ,SAAS;AAE/B,KAAI,CAAC,QACH,QAAO;EACL,gBAAgB;EAChB,oBAAoB;EACpB,eAAe;EAChB;CAGH,MAAM,WAAW,eAAe,EAAE,iBAAiB,CAAC;CACpD,MAAM,+BAAe,IAAI,KAAqC;CAC9D,IAAI,YAAY;CAKhB,SAAS,kBAAkB,WAAyB;AAClD,MAAI,aAAa,IAAI,UAAU,CAAE;EAEjC,MAAM,WAAW,UAAuB;AACtC,YAAS,YAAY,MAAM;AAE3B,OAAI,OAAO;IACT,MAAM,YAAY,SAAS,cAAc;AACzC,QAAI,WAAW,gBAAgB,MAC7B,SAAQ,MAAM,yBAAyB,UAAU;;;AAKvD,WAAS,iBAAiB,WAAW,SAAS,KAAK;AACnD,eAAa,IAAI,WAAW,QAAQ;;CAGtC,SAAS,kBAAkB,WAAyB;EAClD,MAAM,UAAU,aAAa,IAAI,UAAU;AAC3C,MAAI,YAAY,OAAW;AAG3B,MAAI,CAAC,SAAS,eAAe,CAAC,IAAI,UAAU,EAAE;AAC5C,YAAS,oBAAoB,WAAW,SAAS,KAAK;AACtD,gBAAa,OAAO,UAAU;;;AAIlC,QAAO;EACL,GAAG,WAAmB,UAAyB,SAAuC;AACpF,OAAI,UAAW,cAAa;AAE5B,qBAAkB,UAAU;GAC5B,MAAM,QAAQ,SAAS,YAAY,WAAW,UAAU,WAAW,EAAE,CAAC;AAEtE,gBAAa;AACX,WAAO;AACP,sBAAkB,UAAU;;;EAIhC,eAAe;AACb,UAAO,SAAS,cAAc;;EAGhC,UAAgB;AACd,OAAI,UAAW;AACf,eAAY;AAEZ,YAAS,OAAO;AAEhB,QAAK,MAAM,CAAC,WAAW,YAAY,aACjC,UAAS,oBAAoB,WAAW,SAAS,KAAK;AAExD,gBAAa,OAAO;;EAEvB"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "react-auto-tracking",
3
+ "version": "0.1.0",
4
+ "description": "Global user interaction tracking for React.js",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage",
29
+ "lint": "oxlint src/",
30
+ "format": "oxfmt --write src/",
31
+ "format:check": "oxfmt --check src/",
32
+ "typecheck": "tsc --noEmit",
33
+ "check": "pnpm lint && pnpm format:check && pnpm typecheck"
34
+ },
35
+ "simple-git-hooks": {
36
+ "pre-push": "pnpm check"
37
+ },
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "keywords": [
43
+ "react",
44
+ "tracking",
45
+ "analytics",
46
+ "interaction",
47
+ "fiber"
48
+ ],
49
+ "devDependencies": {
50
+ "@changesets/cli": "^2.29.8",
51
+ "@vitest/coverage-v8": "^4.0.18",
52
+ "jsdom": "^28.1.0",
53
+ "oxfmt": "^0.32.0",
54
+ "oxlint": "^1.47.0",
55
+ "simple-git-hooks": "^2.13.1",
56
+ "tsdown": "^0.20.3",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.18"
59
+ }
60
+ }