@releval/tracker 1.0.0-bootstrap.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/react/SearchResults.ts","../src/react/context.ts","../src/react/TrackerProvider.ts","../src/react/useResultImpression.ts"],"sourcesContent":["import {\n createContext,\n createElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useMemo,\n} from \"react\";\n\n/** The search that produced the results rendered beneath a {@link SearchResults}. */\nexport type SearchResultsValue = {\n /** The server-issued `query_id` for the search. */\n queryId: string;\n /** The query text as the user entered it, if known. */\n query?: string;\n};\n\nconst SearchResultsContext = createContext<SearchResultsValue | null>(null);\n\n/** Props for {@link SearchResults}. */\nexport type SearchResultsProps = SearchResultsValue & {\n /** The results subtree this search context applies to. */\n children?: ReactNode;\n};\n\n/**\n * Declares which search produced the results rendered beneath it - the SPA\n * twin of `data-query-id` on a server-rendered results container. Result\n * components read it with {@link useSearchResults} (and\n * {@link useResultImpression} reads it automatically) instead of threading\n * `queryId` down as a prop.\n *\n * ```tsx\n * <SearchResults queryId={response.query_id} query={q}>\n * {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}\n * </SearchResults>\n * ```\n */\nexport function SearchResults({\n queryId,\n query,\n children,\n}: SearchResultsProps): ReactElement {\n const value = useMemo(() => ({ queryId, query }), [queryId, query]);\n return createElement(SearchResultsContext.Provider, { value }, children);\n}\n\n/**\n * The current search context, or null outside a {@link SearchResults} (e.g. a\n * card rendered on a home page that did not come from a search).\n */\nexport function useSearchResults(): SearchResultsValue | null {\n return useContext(SearchResultsContext);\n}\n","import { createContext } from \"react\";\nimport type { Tracker } from \"../tracker\";\n\n/**\n * Holds the configured {@link Tracker} for a React tree. Provided by\n * {@link TrackerProvider} and read with {@link useTracker}.\n */\nexport const TrackerContext = createContext<Tracker | null>(null);\n","import {\n createElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Tracker, type TrackerOptions } from \"../tracker\";\nimport { TrackerContext } from \"./context\";\n\n/**\n * Props shared by both {@link TrackerProviderProps} variants (provide\n * `options` OR a pre-built `tracker`).\n */\nexport interface TrackerProviderBaseProps {\n /**\n * Runs exactly once, before the first `start()`. Do one-time wiring here:\n * `addEnricher()`, `addSink()`. It is guarded so React StrictMode's\n * double-mount cannot run the wiring twice.\n */\n onInit?: (tracker: Tracker) => void;\n /**\n * Call `start()` on mount and `stop()` on unmount. Defaults to `true`. Set\n * `false` when an external owner controls the tracker lifecycle and the\n * provider is only used to expose the instance via context.\n */\n autoStart?: boolean;\n /** The tree that can read the tracker via {@link useTracker}. */\n children?: ReactNode;\n}\n\n/**\n * Provide the tracker either by handing over an already-constructed instance\n * (`tracker`) or by passing `options` for the provider to construct. Exactly\n * one of the two is required.\n */\nexport type TrackerProviderProps =\n | (TrackerProviderBaseProps & {\n /** Options the provider constructs its own {@link Tracker} from. */\n options: TrackerOptions;\n /** Not allowed together with `options`. */\n tracker?: never;\n })\n | (TrackerProviderBaseProps & {\n /** A pre-built instance owned outside React, exposed via context. */\n tracker: Tracker;\n /** Not allowed together with `tracker`. */\n options?: never;\n });\n\n/**\n * Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.\n * Starts the tracker on mount and stops it on unmount, so components never\n * import a module singleton and the tracker is torn down cleanly.\n *\n * `options` (and `tracker`) are read ONCE on mount; later prop changes are\n * ignored. Update identity with `useTracker().setUserId(...)` rather than by\n * changing `options.userId`.\n *\n * @example\n * ```tsx\n * <TrackerProvider\n * options={{ application: \"my-app\", siteId: \"...\", endpointHost: \"...\" }}\n * onInit={(t) => t.addEnricher(abTestEnricher)}\n * >\n * <App />\n * </TrackerProvider>\n * ```\n */\nexport function TrackerProvider(props: TrackerProviderProps): ReactElement {\n const { children, onInit, autoStart = true } = props;\n\n // Own the instance once. The lazy initializer keeps its identity stable across\n // renders, so the context value never changes and consumers do not re-render.\n const [tracker] = useState<Tracker>(() =>\n \"tracker\" in props && props.tracker\n ? props.tracker\n : new Tracker((props as { options: TrackerOptions }).options),\n );\n\n // Capture the mount-time onInit; it runs once, so later identity changes are\n // irrelevant and it never needs to be an effect dependency.\n const onInitRef = useRef(onInit);\n const initialized = useRef(false);\n\n useEffect(() => {\n if (!autoStart) return;\n // StrictMode runs mount -> cleanup -> mount. onInit is ref-guarded to run\n // once (one-time wiring such as addSink/addEnricher would otherwise run\n // twice), while start()/stop() are restart-safe and idempotent - so the\n // tracker ends started with the wiring applied exactly once.\n if (!initialized.current) {\n initialized.current = true;\n onInitRef.current?.(tracker);\n }\n tracker.start();\n return () => {\n tracker.stop();\n };\n }, [tracker, autoStart]);\n\n return createElement(TrackerContext.Provider, { value: tracker }, children);\n}\n\n/**\n * Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.\n * Throws when called outside a provider.\n */\nexport function useTracker(): Tracker {\n const tracker = useContext(TrackerContext);\n if (!tracker) {\n throw new Error(\n \"useTracker must be used within a <TrackerProvider>. Wrap your app in \" +\n \"<TrackerProvider options={{ application: '...' }}> ... </TrackerProvider>.\",\n );\n }\n return tracker;\n}\n","import { useCallback, useRef } from \"react\";\nimport type { ResultRef } from \"../tracker\";\nimport { useSearchResults } from \"./SearchResults\";\nimport { useTracker } from \"./TrackerProvider\";\n\n/** Options for {@link useResultImpression}. */\nexport type UseResultImpressionOptions = {\n /** When true, no impression is observed or reported. */\n disabled?: boolean;\n};\n\n/**\n * Returns a callback ref that reports ONE canonical impression for the result\n * the first time its element becomes visible, attributed to the surrounding\n * {@link SearchResults} context.\n *\n * Fires once per query, not once per component instance: a new `queryId`\n * re-arms the hook, so a card that stays mounted across consecutive searches\n * is counted for each query. (A per-instance one-shot undercounts exactly the\n * products that are returned most often, inflating their CTR.) The Tracker\n * additionally dedupes impressions per `(queryId, objectId)` for its\n * lifetime, so remounted instances - virtualized lists, route re-entry -\n * cannot re-fire a pair this page already reported.\n *\n * Extra keys on `result` are persisted under the event's `event_attributes`\n * (see {@link ResultRef}). Changing only extras does not re-arm the\n * observer; the values current when the impression fires are sent.\n *\n * No-ops when there is no search context (nothing joinable to report), when\n * `disabled`, or where `IntersectionObserver` is unavailable.\n *\n * StrictMode-safe by construction: cleanup is driven by the callback ref\n * itself - React calls it with `null` on detach and re-invokes it when its\n * identity changes - not by an effect whose simulated unmount would\n * disconnect the observer without re-arming it.\n *\n * ```tsx\n * const ref = useResultImpression({ objectId: r.sku, ordinal });\n * return <article ref={ref}>...</article>;\n * ```\n */\nexport function useResultImpression(\n result: ResultRef,\n options?: UseResultImpressionOptions,\n): (node: Element | null) => void {\n const tracker = useTracker();\n const search = useSearchResults();\n const firedForQuery = useRef<string | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n // Latest-value ref: extra keys on `result` ride into the fired impression\n // without being callback deps (an inline extras literal would otherwise\n // re-arm the observer every render). Identity-forming fields stay as deps.\n const latestResult = useRef(result);\n latestResult.current = result;\n\n const disabled = options?.disabled ?? false;\n const { objectId, ordinal } = result;\n const queryId = search?.queryId;\n const query = search?.query;\n\n return useCallback(\n (node: Element | null) => {\n // Called with the node on attach and null on detach; also re-invoked\n // when the callback identity changes (e.g. a new queryId arrives).\n observerRef.current?.disconnect();\n observerRef.current = null;\n\n if (!node || disabled || !queryId) return;\n if (firedForQuery.current === queryId) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n if (firedForQuery.current === queryId) continue;\n firedForQuery.current = queryId;\n\n const item: ResultRef = {\n ...latestResult.current,\n objectId,\n ordinal,\n };\n tracker.trackResultImpression({ items: [item], queryId, query });\n\n observer.disconnect();\n observerRef.current = null;\n }\n },\n { threshold: 0 },\n );\n observer.observe(node);\n observerRef.current = observer;\n },\n [tracker, objectId, ordinal, queryId, query, disabled],\n );\n}\n"],"mappings":";;;;;AAiBA,MAAM,uBAAuB,cAAyC,IAAI;;;;;;;;;;;;;;AAqB1E,SAAgB,cAAc,EAC5B,SACA,OACA,YACmC;CACnC,MAAM,QAAQ,eAAe;EAAE;EAAS;CAAM,IAAI,CAAC,SAAS,KAAK,CAAC;CAClE,OAAO,cAAc,qBAAqB,UAAU,EAAE,MAAM,GAAG,QAAQ;AACzE;;;;;AAMA,SAAgB,mBAA8C;CAC5D,OAAO,WAAW,oBAAoB;AACxC;;;;;;;AC9CA,MAAa,iBAAiB,cAA8B,IAAI;;;;;;;;;;;;;;;;;;;;;;ACgEhE,SAAgB,gBAAgB,OAA2C;CACzE,MAAM,EAAE,UAAU,QAAQ,YAAY,SAAS;CAI/C,MAAM,CAAC,WAAW,eAChB,aAAa,SAAS,MAAM,UACxB,MAAM,UACN,IAAI,QAAS,MAAsC,OAAO,CAChE;CAIA,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,cAAc,OAAO,KAAK;CAEhC,gBAAgB;EACd,IAAI,CAAC,WAAW;EAKhB,IAAI,CAAC,YAAY,SAAS;;GACxB,YAAY,UAAU;GACtB,CAAA,qBAAA,UAAU,aAAA,QAAA,uBAAA,KAAA,KAAA,mBAAA,KAAA,WAAU,OAAO;EAC7B;EACA,QAAQ,MAAM;EACd,aAAa;GACX,QAAQ,KAAK;EACf;CACF,GAAG,CAAC,SAAS,SAAS,CAAC;CAEvB,OAAO,cAAc,eAAe,UAAU,EAAE,OAAO,QAAQ,GAAG,QAAQ;AAC5E;;;;;AAMA,SAAgB,aAAsB;CACpC,MAAM,UAAU,WAAW,cAAc;CACzC,IAAI,CAAC,SACH,MAAM,IAAI,MACR,iJAEF;CAEF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9EA,SAAgB,oBACd,QACA,SACgC;;CAChC,MAAM,UAAU,WAAW;CAC3B,MAAM,SAAS,iBAAiB;CAChC,MAAM,gBAAgB,OAAsB,IAAI;CAChD,MAAM,cAAc,OAAoC,IAAI;CAI5D,MAAM,eAAe,OAAO,MAAM;CAClC,aAAa,UAAU;CAEvB,MAAM,YAAA,oBAAA,YAAA,QAAA,YAAA,KAAA,IAAA,KAAA,IAAW,QAAS,cAAA,QAAA,sBAAA,KAAA,IAAA,oBAAY;CACtC,MAAM,EAAE,UAAU,YAAY;CAC9B,MAAM,UAAA,WAAA,QAAA,WAAA,KAAA,IAAA,KAAA,IAAU,OAAQ;CACxB,MAAM,QAAA,WAAA,QAAA,WAAA,KAAA,IAAA,KAAA,IAAQ,OAAQ;CAEtB,OAAO,aACJ,SAAyB;;EAGxB,CAAA,uBAAA,YAAY,aAAA,QAAA,yBAAA,KAAA,KAAA,qBAAS,WAAW;EAChC,YAAY,UAAU;EAEtB,IAAI,CAAC,QAAQ,YAAY,CAAC,SAAS;EACnC,IAAI,cAAc,YAAY,SAAS;EACvC,IAAI,OAAO,yBAAyB,aAAa;EAEjD,MAAM,WAAW,IAAI,sBAClB,YAAY;GACX,KAAK,MAAM,SAAS,SAAS;IAC3B,IAAI,CAAC,MAAM,gBAAgB;IAC3B,IAAI,cAAc,YAAY,SAAS;IACvC,cAAc,UAAU;IAExB,MAAM,OAAkB;KACtB,GAAG,aAAa;KAChB;KACA;IACF;IACA,QAAQ,sBAAsB;KAAE,OAAO,CAAC,IAAI;KAAG;KAAS;IAAM,CAAC;IAE/D,SAAS,WAAW;IACpB,YAAY,UAAU;GACxB;EACF,GACA,EAAE,WAAW,EAAE,CACjB;EACA,SAAS,QAAQ,IAAI;EACrB,YAAY,UAAU;CACxB,GACA;EAAC;EAAS;EAAU;EAAS;EAAS;EAAO;CAAQ,CACvD;AACF"}
@@ -0,0 +1,8 @@
1
+ /*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_tracker = require("./tracker.cjs");
4
+ exports.BatchSink = require_tracker.BatchSink;
5
+ exports.ConsoleLogger = require_tracker.ConsoleLogger;
6
+ exports.ConsoleSink = require_tracker.ConsoleSink;
7
+ exports.Tracker = require_tracker.Tracker;
8
+ exports.readResultData = require_tracker.readResultData;
@@ -0,0 +1,135 @@
1
+ /*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
2
+ import { C as AttributionRecord, S as EventPosition, _ as Sink, a as TrackResultClickOptions, b as EventAttributes, c as TrackSearchOptions, d as TrackerOptions, f as ResolvedResultClick, g as readResultData, h as ResolvedResultData, i as ResultRef, l as Tracker, m as TrackResultClicksOptions, n as ResultImpressionResolve, o as TrackResultEventOptions, p as ResultClickResolve, r as TrackResultImpressionsOptions, s as TrackResultImpressionOptions, t as ResolvedResultImpression, u as TrackerBaseOptions, v as Enricher, w as Logger, x as EventObject, y as Event } from "./ResultImpressionCollector.cjs";
3
+ //#region src/logging/ConsoleLogger.d.ts
4
+ /** Options for {@link ConsoleLogger}. */
5
+ type ConsoleLoggerOptions = {
6
+ /**
7
+ * When true, `debug()` and `info()` are emitted as well. `warn()` and
8
+ * `error()` are always emitted regardless. Defaults to false, so production
9
+ * pages stay quiet except for warnings and errors that indicate lost data.
10
+ */
11
+ readonly verbose?: boolean;
12
+ };
13
+ /**
14
+ * The default {@link Logger}. Routes tracker diagnostics to the browser console
15
+ * so that an integrator can see, without any extra wiring, why events are not
16
+ * arriving (bad site_id, non-retryable server response, storage failures, ...).
17
+ * `debug`/`info` are suppressed unless `verbose` is set.
18
+ */
19
+ declare class ConsoleLogger implements Logger {
20
+ private readonly verbose;
21
+ constructor(options?: ConsoleLoggerOptions);
22
+ debug(msg: string, ...data: any[]): void;
23
+ info(msg: string, ...data: any[]): void;
24
+ warn(msg: string, ...data: any[]): void;
25
+ error(msg: string, ...data: any[]): void;
26
+ }
27
+ //#endregion
28
+ //#region src/sinks/BatchSink.d.ts
29
+ /**
30
+ * Configuration for the BatchSink, which queues events and sends them in bulk for better
31
+ * network efficiency. This is the default sink when `endpointHost` is set in TrackerOptions.
32
+ * Supports automatic retries with exponential backoff, persistent retry queues across page
33
+ * reloads, and reliable delivery via the Beacon API on page unload.
34
+ */
35
+ type BatchSinkOptions = {
36
+ /**
37
+ * Base URL of the API endpoint (e.g., "https://api.example.com"). A path
38
+ * prefix for a reverse proxy is allowed; the fixed Releval track-event
39
+ * path is appended.
40
+ */
41
+ readonly endpointHost: string;
42
+ /** Maximum events per batch before auto-flush. Defaults to 30. */
43
+ readonly flushSize?: number;
44
+ /** Milliseconds between automatic flushes. Defaults to 1000. */
45
+ readonly flushIntervalMs?: number;
46
+ /** Maximum retry attempts for failed batches. Defaults to 5. */
47
+ readonly maxRetries?: number;
48
+ /** Base delay in ms for exponential backoff. Defaults to 1000. */
49
+ readonly retryBaseDelayMs?: number;
50
+ /** Maximum delay in ms for exponential backoff. Defaults to 30000. */
51
+ readonly retryMaxDelayMs?: number;
52
+ /** Storage for persisting the retry queue across page reloads. Defaults to localStorage. */
53
+ readonly storage?: Storage;
54
+ /** Logger for internal diagnostics. */
55
+ readonly logger?: Logger;
56
+ };
57
+ /**
58
+ * A sink that batches events, retries failures with exponential backoff,
59
+ * and uses the Beacon API for reliable delivery on page unload.
60
+ */
61
+ declare class BatchSink implements Sink {
62
+ private readonly url;
63
+ private readonly storageKey;
64
+ private readonly flushSize;
65
+ private readonly flushIntervalMs;
66
+ private readonly maxRetries;
67
+ private readonly retryBaseDelayMs;
68
+ private readonly retryMaxDelayMs;
69
+ private readonly storage?;
70
+ private readonly logger?;
71
+ private queue;
72
+ private flushTimer;
73
+ private readonly pendingRetries;
74
+ private disposed;
75
+ private readonly boundOnVisibilityChange;
76
+ private readonly boundOnPageHide;
77
+ constructor(options: BatchSinkOptions);
78
+ /**
79
+ * Add an event to the batch queue. Triggers a flush if the queue reaches flushSize.
80
+ */
81
+ emit(event: Event): void;
82
+ /**
83
+ * Flush the current queue immediately via fetch.
84
+ * Returns a promise that resolves when the batch is sent (or scheduled for retry).
85
+ */
86
+ flush(): Promise<void>;
87
+ /**
88
+ * Start the flush timer and unload listeners.
89
+ */
90
+ start(): void;
91
+ /**
92
+ * Stop the flush timer, flush remaining events, and clean up.
93
+ */
94
+ dispose(): void;
95
+ /** Visible for testing - returns the current queue length */
96
+ get pendingCount(): number;
97
+ private onVisibilityChange;
98
+ private onPageHide;
99
+ /**
100
+ * Clear and persist any in-flight retry batches so a page discard or unload
101
+ * does not lose them. Empties the map, so it is safe to call more than once.
102
+ */
103
+ private persistPendingRetries;
104
+ /**
105
+ * Flush all queued events using the Beacon API (reliable on page unload).
106
+ */
107
+ private flushViaBeacon;
108
+ private sendViaBeacon;
109
+ /**
110
+ * Fallback delivery on unload when the beacon is unavailable or refused.
111
+ * Persists the batch for the next page load if the fetch fails.
112
+ */
113
+ private sendViaKeepaliveFetch;
114
+ private sendBatch;
115
+ private isRetryable;
116
+ private scheduleRetry;
117
+ private calculateBackoff;
118
+ private persistForRetry;
119
+ private loadStoredRetries;
120
+ private drainStoredRetries;
121
+ }
122
+ //#endregion
123
+ //#region src/sinks/ConsoleSink.d.ts
124
+ /**
125
+ * Logs every event to the browser console. The development default: used
126
+ * automatically when no `endpointHost` is configured and no sink has been
127
+ * added, so events are visible without anything leaving the page. With an
128
+ * endpoint configured, delivery goes through `BatchSink` instead.
129
+ */
130
+ declare class ConsoleSink implements Sink {
131
+ emit(event: Event): void;
132
+ }
133
+ //#endregion
134
+ export { type AttributionRecord, BatchSink, type BatchSinkOptions, ConsoleLogger, type ConsoleLoggerOptions, ConsoleSink, type Enricher, Event, EventAttributes, EventObject, EventPosition, type Logger, type ResolvedResultClick, type ResolvedResultData, type ResolvedResultImpression, type ResultClickResolve, type ResultImpressionResolve, ResultRef, type Sink, TrackResultClickOptions, type TrackResultClicksOptions, TrackResultEventOptions, TrackResultImpressionOptions, type TrackResultImpressionsOptions, TrackSearchOptions, Tracker, TrackerBaseOptions, TrackerOptions, readResultData };
135
+ //# sourceMappingURL=releval-tracker.d.cts.map
@@ -0,0 +1,135 @@
1
+ /*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
2
+ import { C as AttributionRecord, S as EventPosition, _ as Sink, a as TrackResultClickOptions, b as EventAttributes, c as TrackSearchOptions, d as TrackerOptions, f as ResolvedResultClick, g as readResultData, h as ResolvedResultData, i as ResultRef, l as Tracker, m as TrackResultClicksOptions, n as ResultImpressionResolve, o as TrackResultEventOptions, p as ResultClickResolve, r as TrackResultImpressionsOptions, s as TrackResultImpressionOptions, t as ResolvedResultImpression, u as TrackerBaseOptions, v as Enricher, w as Logger, x as EventObject, y as Event } from "./ResultImpressionCollector.mjs";
3
+ //#region src/logging/ConsoleLogger.d.ts
4
+ /** Options for {@link ConsoleLogger}. */
5
+ type ConsoleLoggerOptions = {
6
+ /**
7
+ * When true, `debug()` and `info()` are emitted as well. `warn()` and
8
+ * `error()` are always emitted regardless. Defaults to false, so production
9
+ * pages stay quiet except for warnings and errors that indicate lost data.
10
+ */
11
+ readonly verbose?: boolean;
12
+ };
13
+ /**
14
+ * The default {@link Logger}. Routes tracker diagnostics to the browser console
15
+ * so that an integrator can see, without any extra wiring, why events are not
16
+ * arriving (bad site_id, non-retryable server response, storage failures, ...).
17
+ * `debug`/`info` are suppressed unless `verbose` is set.
18
+ */
19
+ declare class ConsoleLogger implements Logger {
20
+ private readonly verbose;
21
+ constructor(options?: ConsoleLoggerOptions);
22
+ debug(msg: string, ...data: any[]): void;
23
+ info(msg: string, ...data: any[]): void;
24
+ warn(msg: string, ...data: any[]): void;
25
+ error(msg: string, ...data: any[]): void;
26
+ }
27
+ //#endregion
28
+ //#region src/sinks/BatchSink.d.ts
29
+ /**
30
+ * Configuration for the BatchSink, which queues events and sends them in bulk for better
31
+ * network efficiency. This is the default sink when `endpointHost` is set in TrackerOptions.
32
+ * Supports automatic retries with exponential backoff, persistent retry queues across page
33
+ * reloads, and reliable delivery via the Beacon API on page unload.
34
+ */
35
+ type BatchSinkOptions = {
36
+ /**
37
+ * Base URL of the API endpoint (e.g., "https://api.example.com"). A path
38
+ * prefix for a reverse proxy is allowed; the fixed Releval track-event
39
+ * path is appended.
40
+ */
41
+ readonly endpointHost: string;
42
+ /** Maximum events per batch before auto-flush. Defaults to 30. */
43
+ readonly flushSize?: number;
44
+ /** Milliseconds between automatic flushes. Defaults to 1000. */
45
+ readonly flushIntervalMs?: number;
46
+ /** Maximum retry attempts for failed batches. Defaults to 5. */
47
+ readonly maxRetries?: number;
48
+ /** Base delay in ms for exponential backoff. Defaults to 1000. */
49
+ readonly retryBaseDelayMs?: number;
50
+ /** Maximum delay in ms for exponential backoff. Defaults to 30000. */
51
+ readonly retryMaxDelayMs?: number;
52
+ /** Storage for persisting the retry queue across page reloads. Defaults to localStorage. */
53
+ readonly storage?: Storage;
54
+ /** Logger for internal diagnostics. */
55
+ readonly logger?: Logger;
56
+ };
57
+ /**
58
+ * A sink that batches events, retries failures with exponential backoff,
59
+ * and uses the Beacon API for reliable delivery on page unload.
60
+ */
61
+ declare class BatchSink implements Sink {
62
+ private readonly url;
63
+ private readonly storageKey;
64
+ private readonly flushSize;
65
+ private readonly flushIntervalMs;
66
+ private readonly maxRetries;
67
+ private readonly retryBaseDelayMs;
68
+ private readonly retryMaxDelayMs;
69
+ private readonly storage?;
70
+ private readonly logger?;
71
+ private queue;
72
+ private flushTimer;
73
+ private readonly pendingRetries;
74
+ private disposed;
75
+ private readonly boundOnVisibilityChange;
76
+ private readonly boundOnPageHide;
77
+ constructor(options: BatchSinkOptions);
78
+ /**
79
+ * Add an event to the batch queue. Triggers a flush if the queue reaches flushSize.
80
+ */
81
+ emit(event: Event): void;
82
+ /**
83
+ * Flush the current queue immediately via fetch.
84
+ * Returns a promise that resolves when the batch is sent (or scheduled for retry).
85
+ */
86
+ flush(): Promise<void>;
87
+ /**
88
+ * Start the flush timer and unload listeners.
89
+ */
90
+ start(): void;
91
+ /**
92
+ * Stop the flush timer, flush remaining events, and clean up.
93
+ */
94
+ dispose(): void;
95
+ /** Visible for testing - returns the current queue length */
96
+ get pendingCount(): number;
97
+ private onVisibilityChange;
98
+ private onPageHide;
99
+ /**
100
+ * Clear and persist any in-flight retry batches so a page discard or unload
101
+ * does not lose them. Empties the map, so it is safe to call more than once.
102
+ */
103
+ private persistPendingRetries;
104
+ /**
105
+ * Flush all queued events using the Beacon API (reliable on page unload).
106
+ */
107
+ private flushViaBeacon;
108
+ private sendViaBeacon;
109
+ /**
110
+ * Fallback delivery on unload when the beacon is unavailable or refused.
111
+ * Persists the batch for the next page load if the fetch fails.
112
+ */
113
+ private sendViaKeepaliveFetch;
114
+ private sendBatch;
115
+ private isRetryable;
116
+ private scheduleRetry;
117
+ private calculateBackoff;
118
+ private persistForRetry;
119
+ private loadStoredRetries;
120
+ private drainStoredRetries;
121
+ }
122
+ //#endregion
123
+ //#region src/sinks/ConsoleSink.d.ts
124
+ /**
125
+ * Logs every event to the browser console. The development default: used
126
+ * automatically when no `endpointHost` is configured and no sink has been
127
+ * added, so events are visible without anything leaving the page. With an
128
+ * endpoint configured, delivery goes through `BatchSink` instead.
129
+ */
130
+ declare class ConsoleSink implements Sink {
131
+ emit(event: Event): void;
132
+ }
133
+ //#endregion
134
+ export { type AttributionRecord, BatchSink, type BatchSinkOptions, ConsoleLogger, type ConsoleLoggerOptions, ConsoleSink, type Enricher, Event, EventAttributes, EventObject, EventPosition, type Logger, type ResolvedResultClick, type ResolvedResultData, type ResolvedResultImpression, type ResultClickResolve, type ResultImpressionResolve, ResultRef, type Sink, TrackResultClickOptions, type TrackResultClicksOptions, TrackResultEventOptions, TrackResultImpressionOptions, type TrackResultImpressionsOptions, TrackSearchOptions, Tracker, TrackerBaseOptions, TrackerOptions, readResultData };
135
+ //# sourceMappingURL=releval-tracker.d.mts.map
@@ -0,0 +1,2 @@
1
+ /*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
2
+ var Releval=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});let t=(e,t=100,n=!1)=>{let r=null,i=null,a=null,o,s=()=>{r=null,i&&(o=e.apply(a,i),a=i=null)},c=function(...c){a=this,i=c;let l=n&&!r;return r&&clearTimeout(r),l?(o=e.apply(a,i),a=i=null):r=setTimeout(s,t),o};return c.clear=()=>{r&&(clearTimeout(r),r=null)},c.flush=()=>{r&&(o=e.apply(a,i),a=i=null,clearTimeout(r),r=null)},c},n=(e,t,n)=>{let r=(t,n)=>{if(t.nodeType!==Node.ELEMENT_NODE)return;let r=t;r.matches(e)&&n(r);for(let t of r.querySelectorAll(e))n(t)};return new MutationObserver(e=>{for(let i of e)i.type===`childList`&&(i.addedNodes.forEach(e=>{r(e,t)}),i.removedNodes.forEach(e=>{r(e,n)}))})},r=e=>typeof e==`number`&&Number.isInteger(e)&&e>=1,i=!1,a=(e,t,n)=>{let r=e.trim();if(!r.startsWith(`{`)&&!r.startsWith(`[`))return e;try{return JSON.parse(r)}catch{if(!i){i=!0;let e=`data-${t.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}`;n==null||n.warn(`readResultData: the value of ${e} looks like JSON but does not parse; it is kept as a string. Further malformed values are kept silently.`)}return e}},o=(e,t)=>{var n;let i=e.closest(`[data-query-id]`),o=(n=e.dataset.ordinal)==null?void 0:n.trim(),s=o?Number(o):NaN,c={};for(let n of Object.keys(e.dataset))/^event[A-Z]/.test(n)&&(c[n.charAt(5).toLowerCase()+n.slice(6)]=a(e.dataset[n],n,t));return{...c,objectId:e.dataset.objectId,ordinal:r(s)?s:void 0,objectIdField:e.dataset.objectIdField,actionName:e.dataset.actionName,queryId:i==null?void 0:i.dataset.queryId,query:i==null?void 0:i.dataset.query}};var s=class{constructor(e,t){this.hasWarnedUnresolvable=!1,this.hasWarnedBodyScope=!1,this.options=e,this.emit=t}attach(e){var t,n;let{selector:i,ignore:a}=this.options,s=(t=this.options.root)==null?document:t,c=(n=this.options.resolve)==null?(t=>o(t,e.logger)):n,l=t=>{try{var n,o;let u=(n=t.composedPath)==null?void 0:n.call(t)[0],d=u instanceof Element?u:t.target,f=d==null?void 0:d.closest(i);if(!f)return;if(!this.options.resolve&&(f===document.body||f===document.documentElement)){if(!this.hasWarnedBodyScope){var s;this.hasWarnedBodyScope=!0,(s=e.logger)==null||s.warn(`trackResultClicks: the selector "${i}" matched the page body itself; these clicks are skipped. Narrow the selector to your result elements.`)}return}if(a){let e=d==null?void 0:d.closest(a);if(e&&e!==f&&f.contains(e))return}let p=c(f,t);if(!p)return;let m=(o=p.actionName)==null?`click`:o,h=m===`click`&&(!r(p.ordinal)||!p.queryId);if(!p.objectId||h){if(!this.hasWarnedUnresolvable){var l;this.hasWarnedUnresolvable=!0,(l=e.logger)==null||l.warn(`trackResultClicks: skipped a click matching "${i}" because objectId, ordinal or queryId could not be resolved. Add data-object-id and data-ordinal to the result element and data-query-id to an ancestor, or supply a resolve callback. Further unresolvable clicks are skipped silently.`)}return}this.emit({...p,actionName:m})}catch(t){var u;(u=e.logger)==null||u.error(`Error during handler execution: `,t)}};return s.addEventListener(`click`,l,{capture:!0}),()=>{s.removeEventListener(`click`,l,{capture:!0})}}},c=class{constructor(e,t){this.hasWarnedUnresolvable=!1,this.hasWarnedBodyScope=!1,this.options=e,this.emit=t}attach(e){var i,a;let{selector:s}=this.options,c=(i=this.options.root)==null?document:i,l=(a=this.options.resolve)==null?(t=>o(t,e.logger)):a,u=[],d=t(()=>{let t=new Map;for(let e of u){var n;let r=JSON.stringify([e.queryId,(n=e.query)==null?null:n]),i=t.get(r);i||(i={queryId:e.queryId,query:e.query,items:[]},t.set(r,i)),i.items.push(e.item)}u.length=0;for(let n of t.values())try{this.emit(n.items,n.queryId,n.query)}catch(t){var r;(r=e.logger)==null||r.error(`Error emitting impressions: `,t)}},250),f=new Set,p=new IntersectionObserver(t=>{for(let a of t){if(!a.isIntersecting)continue;let t=a.target;p.unobserve(t),f.delete(t);try{let i=l(t);if(!i)continue;if(!i.objectId||!r(i.ordinal)||!i.queryId){if(!this.hasWarnedUnresolvable){var n;this.hasWarnedUnresolvable=!0,(n=e.logger)==null||n.warn(`trackResultImpressions: skipped an impression matching "${s}" because objectId, ordinal or queryId could not be resolved. Add data-object-id and data-ordinal to the result element and data-query-id to an ancestor, or supply a resolve callback. Further unresolvable impressions are skipped silently.`)}continue}let{actionName:a,queryId:o,query:c,...f}=i,p={...f,objectId:i.objectId,ordinal:i.ordinal};i.objectIdField&&(p.objectIdField=i.objectIdField),u.push({item:p,queryId:i.queryId,query:i.query}),d()}catch(t){var i;(i=e.logger)==null||i.error(`Error resolving impression: `,t)}}},{threshold:0}),m=t=>{if(!this.options.resolve&&(t===document.body||t===document.documentElement)){if(!this.hasWarnedBodyScope){var n;this.hasWarnedBodyScope=!0,(n=e.logger)==null||n.warn(`trackResultImpressions: the selector "${s}" matched the page body itself; it is not observed. Narrow the selector to your result elements.`)}return}f.has(t)||(f.add(t),p.observe(t))},h=e=>{f.has(e)&&(p.unobserve(e),f.delete(e))};c.querySelectorAll(s).forEach(m);let g=n(s,m,h);return g.observe(c,{childList:!0,subtree:!0}),()=>{g.disconnect(),d.clear(),p.disconnect(),f.clear()}}},l=class{constructor(e={}){var t;this.verbose=(t=e.verbose)!=null&&t}debug(e,...t){this.verbose&&console.debug(e,...t)}info(e,...t){this.verbose&&console.info(e,...t)}warn(e,...t){console.warn(e,...t)}error(e,...t){console.error(e,...t)}};let u=e=>{try{console.error(`The configured tracker logger threw: `,e)}catch{}},d=e=>({debug(t,...n){try{e.debug(t,...n)}catch(e){u(e)}},info(t,...n){try{e.info(t,...n)}catch(e){u(e)}},warn(t,...n){try{e.warn(t,...n)}catch(e){u(e)}},error(t,...n){try{e.error(t,...n)}catch(e){u(e)}}});var f=class{constructor(e,t){this.sinks=e,this.logger=t}add(e){this.sinks.add(e)}delete(e){this.sinks.delete(e)}emit(e){this.sinks.forEach(t=>{try{t.emit(e)}catch(r){var n;(n=this.logger)==null||n.error(`Error emitting event to sink`,{error:r,sink:t,data:e})}})}},p=class{constructor(e){this.queue=[],this.flushTimer=null,this.pendingRetries=new Map,this.disposed=!1;let{endpointHost:t,flushSize:n=30,flushIntervalMs:r=1e3,maxRetries:i=5,retryBaseDelayMs:a=1e3,retryMaxDelayMs:o=3e4,storage:s,logger:c}=e;this.url=`${t.replace(/\/+$/,``)}/api/v1/ubi/track-event`,this.storageKey=`_ubi_batch_retry_${t.replace(/[^a-zA-Z0-9]+/g,`-`)}_`,this.flushSize=n,this.flushIntervalMs=r,this.maxRetries=i,this.retryBaseDelayMs=a,this.retryMaxDelayMs=o,this.storage=s,this.logger=c,this.boundOnVisibilityChange=this.onVisibilityChange.bind(this),this.boundOnPageHide=this.onPageHide.bind(this),this.start(),this.drainStoredRetries()}emit(e){this.disposed||(this.queue.push(e),this.queue.length>=this.flushSize&&this.flush())}flush(){if(this.queue.length===0)return Promise.resolve();let e=this.queue;return this.queue=[],this.sendBatch(e,0)}start(){this.flushTimer||(this.flushTimer=setInterval(()=>{this.flush()},this.flushIntervalMs),typeof document<`u`&&document.addEventListener(`visibilitychange`,this.boundOnVisibilityChange),typeof window<`u`&&window.addEventListener(`pagehide`,this.boundOnPageHide))}dispose(){this.disposed=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),this.persistPendingRetries(),typeof document<`u`&&document.removeEventListener(`visibilitychange`,this.boundOnVisibilityChange),typeof window<`u`&&window.removeEventListener(`pagehide`,this.boundOnPageHide),this.queue.length>0&&(this.sendViaBeacon(this.queue),this.queue=[])}get pendingCount(){return this.queue.length}onVisibilityChange(){typeof document>`u`||(document.visibilityState===`hidden`?(this.flushViaBeacon(),this.persistPendingRetries()):document.visibilityState===`visible`&&this.drainStoredRetries())}onPageHide(e){this.flushViaBeacon(),e.persisted||this.persistPendingRetries()}persistPendingRetries(){for(let[e,t]of this.pendingRetries)clearTimeout(e),this.persistForRetry(t.events,t.attempt);this.pendingRetries.clear()}flushViaBeacon(){if(this.queue.length===0)return;let e=this.queue;this.queue=[],this.sendViaBeacon(e)}sendViaBeacon(e){if(typeof navigator<`u`&&typeof navigator.sendBeacon==`function`)try{let n=new Blob([JSON.stringify({events:e})],{type:`application/json`});if(navigator.sendBeacon(this.url,n)){var t;(t=this.logger)==null||t.info(`ubi: queued ${e.length} event(s) via beacon to ${this.url}`);return}}catch(e){var n;(n=this.logger)==null||n.error(`Beacon API failed: `,e)}this.sendViaKeepaliveFetch(e)}sendViaKeepaliveFetch(e){try{fetch(this.url,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({events:e}),keepalive:!0}).then(t=>{t.ok||this.persistForRetry(e,0)}).catch(()=>{this.persistForRetry(e,0)})}catch(n){var t;(t=this.logger)==null||t.error(`Keepalive fetch failed: `,n),this.persistForRetry(e,0)}}async sendBatch(e,t){try{let i=await fetch(this.url,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({events:e})});if(i.ok){var n;(n=this.logger)==null||n.info(`ubi: sent ${e.length} event(s) to ${this.url} (${i.status})`)}else if(this.isRetryable(i.status))this.scheduleRetry(e,t);else{var r;(r=this.logger)==null||r.error(`Batch send failed with status ${i.status}, not retrying`)}}catch{this.scheduleRetry(e,t)}}isRetryable(e){return e>=500||e===429}scheduleRetry(e,t){var n;if(this.disposed){this.persistForRetry(e,t);return}if(t>=this.maxRetries){var r;(r=this.logger)==null||r.error(`Batch send failed after ${this.maxRetries} attempts, discarding ${e.length} events`);return}let i=this.calculateBackoff(t);(n=this.logger)==null||n.warn(`Retrying batch (attempt ${t+1}/${this.maxRetries}) in ${i}ms`);let a=setTimeout(()=>{this.pendingRetries.delete(a),this.sendBatch(e,t+1)},i);this.pendingRetries.set(a,{events:e,attempt:t})}calculateBackoff(e){let t=this.retryBaseDelayMs*2**e,n=Math.min(t,this.retryMaxDelayMs);return Math.random()*n}persistForRetry(e,t){if(this.storage)try{let r=this.loadStoredRetries();r.push({events:e,attempt:t,ts:Date.now()});let i=r.slice(-10);if(i.length<r.length){var n;(n=this.logger)==null||n.warn(`ubi: dropped ${r.length-i.length} oldest persisted retry batch(es) (cap 10)`)}this.storage.setItem(this.storageKey,JSON.stringify(i))}catch(e){var r;(r=this.logger)==null||r.error(`Failed to persist retry queue: `,e)}}loadStoredRetries(){if(!this.storage)return[];try{let e=this.storage.getItem(this.storageKey),t=e?JSON.parse(e):[];return Array.isArray(t)?t.filter(e=>Array.isArray(e==null?void 0:e.events)&&typeof(e==null?void 0:e.ts)==`number`&&Date.now()-e.ts<=864e5):[]}catch{return[]}}drainStoredRetries(){var e;if(!this.storage)return;let t=this.loadStoredRetries();if(t.length!==0){this.storage.removeItem(this.storageKey),(e=this.logger)==null||e.info(`ubi: resuming ${t.length} persisted retry batch(es)`);for(let e of t)this.sendBatch(e.events,e.attempt)}}},m=class{emit(e){console.log(e)}};let h=`_ubi_attribution_`;var g=class{constructor(e){this.options=e}register(e,t){let n={queryId:t.queryId,ordinal:t.ordinal,query:t.query,sessionId:this.options.sessionId(),ts:Date.now()};for(let t of[this.options.sessionStorage,this.options.localStorage]){let r=this.pruneStale(this.load(t));e in r||this.enforceCap(r),r[e]=n,this.save(t,r)}}get(e){for(let t of[this.options.sessionStorage,this.options.localStorage]){let n=this.load(t),r=n[e];if(!r)continue;if(this.isStale(r)){delete n[e],this.save(t,n);continue}let i={queryId:r.queryId};return r.ordinal!==void 0&&(i.ordinal=r.ordinal),r.query!==void 0&&(i.query=r.query),i}}isStale(e){return typeof e.queryId!=`string`||e.sessionId!==this.options.sessionId()||Date.now()-e.ts>864e5}pruneStale(e){for(let n of Object.keys(e)){var t;(typeof((t=e[n])==null?void 0:t.ts)!=`number`||this.isStale(e[n]))&&delete e[n]}return e}enforceCap(e){let t=Object.keys(e);t.length>=50&&t.sort((t,n)=>e[t].ts-e[n].ts).slice(0,t.length-50+1).forEach(t=>{delete e[t]})}load(e){try{let t=e.getItem(h);if(!t)return{};let n=JSON.parse(t);return typeof n!=`object`||!n?(e.removeItem(h),{}):n}catch(t){this.logError(`Error reading the attribution store: `,t);try{e.removeItem(h)}catch{}return{}}}save(e,t){try{e.setItem(h,JSON.stringify(t))}catch(e){this.logError(`Error writing the attribution store: `,e)}}logError(e,t){var n,r;(n=(r=this.options).logger)==null||(n=n.call(r))==null||n.error(e,t)}};function _(e){if(!v(e))throw Error(`Parameter was not an error`)}function v(e){return!!e&&typeof e==`object`&&y(e)===`[object Error]`||e instanceof Error}function y(e){return Object.prototype.toString.call(e)}function b(){return`Layerr`}function x(e){let t,n=``;if(e.length===0)t={};else if(v(e[0]))t={cause:e[0]},n=e.slice(1).join(` `)||``;else if(e[0]&&typeof e[0]==`object`)t=Object.assign({},e[0]),n=e.slice(1).join(` `)||``;else if(typeof e[0]==`string`)t={},n=n=e.join(` `)||``;else throw Error(`Invalid arguments passed to Layerr`);return{options:t,shortMessage:n}}var S=class e extends Error{constructor(e,t){let{options:n,shortMessage:r}=x([...arguments]),i=r;if(n.cause&&(i=`${i}: ${n.cause.message}`),super(i),this.message=i,this.name=n.name&&typeof n.name==`string`?n.name:b(),n.cause&&Object.defineProperty(this,"_cause",{value:n.cause}),Object.defineProperty(this,"_info",{value:{}}),n.info&&typeof n.info==`object`&&Object.assign(this._info,n.info),Error.captureStackTrace){let e=n.constructorOpt||this.constructor;Error.captureStackTrace(this,e)}}static cause(e){return _(e),e._cause&&v(e._cause)?e._cause:null}static fullStack(t){var n;_(t);let r=e.cause(t);return r?`${t.stack}\ncaused by: ${e.fullStack(r)}`:(n=t.stack)==null?``:n}static info(t){_(t);let n={},r=e.cause(t);return r&&Object.assign(n,e.info(r)),t._info&&Object.assign(n,t._info),n}toString(){let e=this.name||this.constructor.name||this.constructor.prototype.name;return this.message&&(e=`${e}: ${this.message}`),e}};let C=`0123456789ABCDEFGHJKMNPQRSTVWXYZ`,w=0xffffffffffff,T=Object.freeze({source:`ulid`});function E(e){let t=e||D(),n=t&&(t.crypto||t.msCrypto)||null;if(typeof(n==null?void 0:n.getRandomValues)==`function`)return()=>{let e=new Uint8Array(1);return n.getRandomValues(e),e[0]/255};if(typeof(n==null?void 0:n.randomBytes)==`function`)return()=>n.randomBytes(1).readUInt8()/255;throw new S({info:{code:`PRNG_DETECT`,...T}},`Failed to find a reliable PRNG`)}function D(){return A()?self:typeof window<`u`?window:typeof global<`u`?global:typeof globalThis<`u`?globalThis:null}function O(e,t){let n=``;for(;e>0;e--)n=j(t)+n;return n}function k(e,t){if(isNaN(e))throw new S({info:{code:`ENC_TIME_NAN`,...T}},`Time must be a number: ${e}`);if(e>w)throw new S({info:{code:`ENC_TIME_SIZE_EXCEED`,...T}},`Cannot encode a time larger than ${w}: ${e}`);if(e<0)throw new S({info:{code:`ENC_TIME_NEG`,...T}},`Time must be positive: ${e}`);if(Number.isInteger(e)===!1)throw new S({info:{code:`ENC_TIME_TYPE`,...T}},`Time must be an integer: ${e}`);let n,r=``;for(let i=t;i>0;i--)n=e%32,r=C.charAt(n)+r,e=(e-n)/32;return r}function A(){return typeof WorkerGlobalScope<`u`&&self instanceof WorkerGlobalScope}function j(e){let t=Math.floor(e()*32);return t===32&&(t=31),C.charAt(t)}function M(e,t){let n=t||E();return k(isNaN(e)?Date.now():e,10)+O(16,n)}var N=class{constructor(e,t,n){this.logger=n,this.sink=t,this.enrichers=e}dispatch(e){var t;e.timestamp||(e.timestamp=new Date().toISOString()),e.event_attributes||(e.event_attributes={}),e.event_attributes.event_id||(e.event_attributes.event_id=M()),e.event_attributes.tracker||(e.event_attributes.tracker={version:`1.0.0-bootstrap.0`}),this.enrichers.forEach(t=>{try{t.enrich(e)}catch(e){var n;(n=this.logger)==null||n.error(`Error enriching event: `,e)}}),(t=this.logger)==null||t.debug(`ubi: dispatching event`,e);try{this.sink.emit(e)}catch(e){var n;(n=this.logger)==null||n.error(`Error emitting event to sink: `,e)}}},P=class{enrich(e){e.event_attributes||(e.event_attributes={});let t={...e.event_attributes.browser,user_agent:navigator.userAgent,language:navigator.language,resolution:{width:screen.width,height:screen.height}};navigator.webdriver&&(t.webdriver=!0),e.event_attributes.browser=t}},F=class{constructor(e){this.clientIdProvider=e}enrich(e){e.client_id||(e.client_id=this.clientIdProvider())}},I=class{constructor(e){this.values=e}enrich(e){e.application||(e.application=this.values.application);let t=this.values.userId();!e.user_id&&t&&(e.user_id=t),!e.site_id&&this.values.siteId&&(e.site_id=this.values.siteId)}},L=class{enrich(e){e.event_attributes||(e.event_attributes={}),e.event_attributes.page={...e.event_attributes.page,url:location.href,title:document.title,referrer:document.referrer||``}}},R=class{constructor(e,t){this.sessionIdProvider=e,this.onActivity=t}enrich(e){var t;e.session_id||(e.session_id=this.sessionIdProvider()),(t=this.onActivity)==null||t.call(this)}},z=class{constructor(){this.store=new Map}get length(){return this.store.size}clear(){this.store.clear()}getItem(e){var t;return(t=this.store.get(e))==null?null:t}key(e){var t;return(t=Array.from(this.store.keys())[e])==null?null:t}removeItem(e){this.store.delete(e)}setItem(e,t){this.store.set(e,String(t))}};let B=`_ubi_client_id_`;function V(e,t){let n=null;try{n=e.getItem(B)}catch(e){t==null||t.error(`Error reading the client id: `,e)}if(!n){n=M();try{e.setItem(B,n)}catch(e){t==null||t.error(`Error persisting the client id: `,e)}}return n}let H=`_ubi_session_`,U=3e5;var W=class{constructor(e){var t,n;this.storage=e.storage,this.inactivityTimeoutMs=(t=e.inactivityTimeoutMs)==null?18e5:t,this.maxSessionDurationMs=(n=e.maxSessionDurationMs)==null?864e5:n,this.logger=e.logger,this.session=this.loadOrCreate()}get sessionId(){return this.syncFromStorage(),this.isExpired()&&this.rotate(),this.session.id}syncFromStorage(){try{let e=this.storage.getItem(H);if(!e)return;let t=JSON.parse(e);if(!this.isUsable(t,Date.now()))return;(t.id!==this.session.id||t.lastActivity>this.session.lastActivity)&&(this.session=t)}catch{}}touch(){this.session.lastActivity=Date.now(),this.persist()}activity(){this.sessionId,this.touch()}isUsable(e,t){return typeof e.id==`string`&&e.id.length>0&&Number.isFinite(e.createdAt)&&Number.isFinite(e.lastActivity)&&e.createdAt<=t+U&&e.lastActivity<=t+U}isExpired(){let e=Date.now(),t=e-this.session.lastActivity>this.inactivityTimeoutMs,n=e-this.session.createdAt>this.maxSessionDurationMs;return t||n}rotate(){this.session=this.createSession(),this.persist()}loadOrCreate(){try{let e=this.storage.getItem(H);if(e){let t=JSON.parse(e);if(this.isUsable(t,Date.now()))return t}}catch{}let e=this.createSession();return this.persist(e),e}createSession(){let e=Date.now();return{id:M(),createdAt:e,lastActivity:e}}persist(e){try{this.storage.setItem(H,JSON.stringify(e==null?this.session:e))}catch(e){var t;(t=this.logger)==null||t.error(`Error persisting the session: `,e)}}};function G(e){if(!(typeof window>`u`))try{if(!(e in window))return;let t=window[e];if(!t)return;let n=`_ubi_probe_${Date.now()}`;t.setItem(n,n);let r=t.getItem(n)===n;return t.removeItem(n),r?t:void 0}catch{return}}let K=()=>{var e;return(e=G(`localStorage`))==null?new z:e},q=()=>{var e;return(e=G(`sessionStorage`))==null?new z:e};var J=class{constructor(e){var t;this.hasEverStarted=!1,this.detaches=new Map,this.hasWarnedMissingQueryId=!1,this.hasWarnedUnattributedResult=!1,this.hasWarnedInvalidOrdinal=!1,this.preStartBuffer=[],this.hasWarnedPreStartOverflow=!1,this.seenImpressions=new Set,this.extraSinks=new Set,this.options=e,this.collectors=new Set,this.started=!1,this.logger=d((t=e.logger)==null?new l({verbose:e.debug}):t),this._sessionManager=new W({storage:this.localStorage,inactivityTimeoutMs:e.sessionInactivityTimeoutMs,maxSessionDurationMs:e.maxSessionDurationMs,logger:this.logger}),this._userId=e.userId,this.enrichers=new Set([new I({application:e.application,siteId:e.siteId,userId:()=>this._userId}),new P,new L,new R(()=>this.sessionId,()=>this._sessionManager.touch()),new F(()=>this.clientId)])}get localStorage(){return this._localStorage||(this._localStorage=K()),this._localStorage}get sessionStorage(){return this._sessionStorage||(this._sessionStorage=q()),this._sessionStorage}get sessionId(){return this._sessionManager.sessionId}get clientId(){return this._clientId||(this._clientId=V(this.localStorage,this.logger)),this._clientId}setUserId(e){this._userId=e}get attribution(){return this._attribution||(this._attribution=new g({localStorage:this.localStorage,sessionStorage:this.sessionStorage,sessionId:()=>this.sessionId,logger:()=>this.logger})),this._attribution}getResultAttribution(e){try{return this.attribution.get(e)}catch(e){this.logger.error(`Error reading result attribution: `,e);return}}trackSearch(e){try{let{query:t,queryId:n,...r}=e;!n&&!this.hasWarnedMissingQueryId&&(this.hasWarnedMissingQueryId=!0,this.logger.warn(`Tracker.trackSearch: called without a queryId. The search event is still emitted but result clicks and impressions cannot be attributed back to this query. Pass the query_id issued by your search backend (Releval track-query): tracker.trackSearch({ query, queryId }).`));let i={action_name:`search`,message_type:`QUERY`,user_query:t};n&&(i.query_id=n),Object.keys(r).length>0&&(i.event_attributes={...r}),this.dispatch(i)}catch(e){this.logger.error(`Error tracking search: `,e)}}trackResultEvent(e){try{let{actionName:o,objectId:s,objectIdField:c,ordinal:l,queryId:u,query:d,...f}=e,p=l,m=u,h=d;if(m===void 0||p===void 0||h===void 0){let e=this.attribution.get(s);if(e){if(m===void 0){var t,n;m=e.queryId,p=(t=p)==null?e.ordinal:t,h=(n=h)==null?e.query:n}else if(e.queryId===m){var i,a;p=(i=p)==null?e.ordinal:i,h=(a=h)==null?e.query:a}}}p!==void 0&&!r(p)&&(this.hasWarnedInvalidOrdinal||(this.hasWarnedInvalidOrdinal=!0,this.logger.warn(`Tracker: ignoring invalid ordinal ${String(p)} for "${s}" - an ordinal is a positive integer: (page - 1) * pageSize + positionOnPage.`)),p=void 0),!m&&!this.hasWarnedUnattributedResult&&(this.hasWarnedUnattributedResult=!0,this.logger.warn(`Tracker.trackResultEvent: no queryId was supplied and no attribution is recorded for "${s}" in this session, so the event is sent unattributed. Either pass queryId explicitly or report the originating click with tracker.trackResultClick so conversions resolve automatically.`));let g={object_id:s};c&&(g.object_id_field=c);let _={action_name:o,event_attributes:{...f,object:g}};typeof p==`number`&&(_.event_attributes.position={ordinal:p}),m&&(_.query_id=m),h&&(_.user_query=h),this.dispatch(_)}catch(e){this.logger.error(`Error tracking result event: `,e)}}trackResultClick(e){try{var t;let{objectId:n,ordinal:r,queryId:i,query:a}=e;this.trackResultEvent({...e,actionName:(t=e.actionName)==null?`click`:t}),this.attribution.register(n,{queryId:i,ordinal:r,query:a})}catch(e){this.logger.error(`Error tracking result click: `,e)}}trackResultImpression(e){try{for(let t of e.items){let n=`${e.queryId}\u0000${t.objectId}`;if(!this.seenImpressions.has(n)){if(this.seenImpressions.size>=1e3){let e=this.seenImpressions.values().next().value;e!==void 0&&this.seenImpressions.delete(e)}this.seenImpressions.add(n),this.trackResultEvent({...t,actionName:`impression`,queryId:e.queryId,query:e.query})}}}catch(e){this.logger.error(`Error tracking result impression: `,e)}}addEnricher(e){try{return this.enrichers.add(e),()=>{this.enrichers.delete(e)}}catch(e){return this.logger.error(`Error adding enricher: `,e),()=>{}}}registerCollector(e){try{if(!this.collectors.has(e)){if(this.started)try{this.detaches.set(e,e.attach(this.dispatcher))}catch(e){this.logger.error(`Error attaching collector: `,e)}this.collectors.add(e)}}catch(e){this.logger.error(`Error adding collector: `,e)}return()=>{try{let t=this.detaches.get(e);this.detaches.delete(e),this.collectors.delete(e),t==null||t()}catch(e){this.logger.error(`Error detaching collector: `,e)}}}trackResultClicks(e){try{let t=new s(e,e=>this.emitResolvedResultClick(e));return this.registerCollector(t)}catch(e){return this.logger.error(`Error creating result click collector: `,e),()=>{}}}emitResolvedResultClick(e){let{actionName:t,...n}=e,r=t==null?`click`:t;if(r===`click`&&e.queryId&&typeof e.ordinal==`number`){this.trackResultClick({...n,objectId:e.objectId,ordinal:e.ordinal,queryId:e.queryId});return}this.trackResultEvent({...n,actionName:r})}trackResultImpressions(e){try{let t=new c(e,(e,t,n)=>this.trackResultImpression({items:e,queryId:t,query:n}));return this.registerCollector(t)}catch(e){return this.logger.error(`Error creating result impression collector: `,e),()=>{}}}addSink(e){try{return this.extraSinks.add(e),this.started&&this.sink&&this.sink.add(e),()=>{var t;this.extraSinks.delete(e),(t=this.sink)==null||t.delete(e)}}catch(e){return this.logger.error(`Error adding sink: `,e),()=>{}}}start(){try{if(this.started)return;let{endpointHost:e,siteId:t}=this.options;if(e&&!t&&this.logger.error("Tracker.start: endpointHost is set but siteId is missing. Every event will be dropped by the server. Set `siteId` to the public Site identifier issued by Releval."),this._sessionManager.activity(),this.sink=this.buildSink(),this.dispatcher=new N(this.enrichers,this.sink,this.logger),this.collectors.forEach(e=>{try{this.detaches.set(e,e.attach(this.dispatcher))}catch(e){this.logger.error(`Error attaching collector: `,e)}}),this.started=!0,this.hasEverStarted=!0,this.preStartBuffer.length>0){let e=this.preStartBuffer.splice(0);this.logger.debug(`ubi: replaying ${e.length} event(s) dispatched before start()`);for(let t of e)this.dispatcher.dispatch(t)}}catch(e){this.logger.error(`Error starting tracker: `,e)}}buildSink(){let{endpointHost:e}=this.options,t=new Set;e&&(this.batchSink=new p({endpointHost:e,storage:this.localStorage,logger:this.logger}),t.add(this.batchSink));for(let e of this.extraSinks)t.add(e);return t.size===0&&t.add(new m),new f(t,this.logger)}dispatch(e){if(!this.started||!this.dispatcher){if(this.hasEverStarted){this.logger.debug(`ubi: tracker is stopped; event dropped`,e);return}e.timestamp||(e.timestamp=new Date().toISOString()),!e.user_id&&this._userId&&(e.user_id=this._userId),this.preStartBuffer.length>=100&&(this.hasWarnedPreStartOverflow||(this.hasWarnedPreStartOverflow=!0,this.logger.warn(`More than 100 events were dispatched before start(); dropping the oldest. Call start() earlier.`)),this.preStartBuffer.shift()),this.preStartBuffer.push(e);return}this.dispatcher.dispatch(e)}flush(){try{var e,t;return(e=(t=this.batchSink)==null?void 0:t.flush())==null?Promise.resolve():e}catch(e){return this.logger.error(`Error flushing events: `,e),Promise.resolve()}}stop(){try{if(!this.started)return;this.detaches.forEach(e=>{try{e()}catch(e){this.logger.error(`Error detaching collector: `,e)}}),this.detaches.clear(),this.batchSink&&this.batchSink.dispose(),this.sink=void 0,this.batchSink=void 0,this.dispatcher=void 0,this.started=!1}catch(e){this.logger.error(`Error stopping tracker: `,e)}}};return e.BatchSink=p,e.ConsoleLogger=l,e.ConsoleSink=m,e.Tracker=J,e.readResultData=o,e})({});
@@ -0,0 +1,3 @@
1
+ /*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
2
+ import { a as readResultData, i as ConsoleLogger, n as ConsoleSink, r as BatchSink, t as Tracker } from "./tracker.mjs";
3
+ export { BatchSink, ConsoleLogger, ConsoleSink, Tracker, readResultData };