@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.
- package/LICENSE +190 -0
- package/README.md +366 -0
- package/dist/ResultImpressionCollector.d.cts +762 -0
- package/dist/ResultImpressionCollector.d.mts +762 -0
- package/dist/react.cjs +180 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +139 -0
- package/dist/react.d.mts +139 -0
- package/dist/react.mjs +174 -0
- package/dist/react.mjs.map +1 -0
- package/dist/releval-tracker.cjs +8 -0
- package/dist/releval-tracker.d.cts +135 -0
- package/dist/releval-tracker.d.mts +135 -0
- package/dist/releval-tracker.global.js +2 -0
- package/dist/releval-tracker.mjs +3 -0
- package/dist/tracker.cjs +1663 -0
- package/dist/tracker.cjs.map +1 -0
- package/dist/tracker.mjs +1634 -0
- package/dist/tracker.mjs.map +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracker.mjs","names":["STORAGE_KEY"],"sources":["../src/utils/index.ts","../src/collectors/resolveResult.ts","../src/collectors/ResultClickCollector.ts","../src/collectors/ResultImpressionCollector.ts","../src/logging/ConsoleLogger.ts","../src/logging/safeLogger.ts","../src/sinks/AggregateSink.ts","../src/sinks/BatchSink.ts","../src/sinks/ConsoleSink.ts","../src/attribution/AttributionStore.ts","../src/dispatcher.ts","../src/enrichers/BrowserEnricher.ts","../src/enrichers/ClientIdEnricher.ts","../src/enrichers/OptionsEnricher.ts","../src/enrichers/PageEnricher.ts","../src/enrichers/SessionEnricher.ts","../src/storage/MemoryStorage.ts","../src/storage/ClientId.ts","../src/storage/SessionManager.ts","../src/storage/index.ts","../src/tracker.ts"],"sourcesContent":["export type DebouncedFunction<T extends (...args: any[]) => any> = {\n (...args: Parameters<T>): ReturnType<T> | undefined;\n clear: () => void;\n flush: () => void;\n};\n\nexport const debounce = <T extends (...args: any[]) => any>(\n func: T,\n wait = 100,\n immediate = false,\n): DebouncedFunction<T> => {\n let timeout: NodeJS.Timeout | null = null;\n let args: Parameters<T> | null = null;\n let context: any = null;\n let result: ReturnType<T> | undefined;\n\n const later = () => {\n timeout = null;\n if (args) {\n result = func.apply(context, args);\n context = args = null;\n }\n };\n\n const debounced = function (this: any, ...callArgs: Parameters<T>) {\n context = this;\n args = callArgs;\n const callNow = immediate && !timeout;\n if (timeout) {\n clearTimeout(timeout);\n }\n\n if (callNow) {\n result = func.apply(context, args);\n context = args = null;\n } else {\n timeout = setTimeout(later, wait);\n }\n\n return result;\n } as DebouncedFunction<T>;\n\n debounced.clear = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n debounced.flush = () => {\n if (timeout) {\n result = func.apply(context, args as Parameters<T>);\n context = args = null;\n\n clearTimeout(timeout);\n timeout = null;\n }\n };\n\n return debounced;\n};\n\n/**\n * Callback invoked by a MutationObserver when a matching element is added to or removed\n * from the DOM. Used internally by collectors to attach/detach event listeners as elements\n * appear and disappear.\n */\nexport type Handler<T extends Element> = (element: T) => void;\n\n/**\n * Creates a MutationObserver that invokes `addHandler`/`removeHandler` for every element\n * matching `selector` that is added to or removed from the DOM - including matches nested\n * inside an added/removed subtree. When a container is inserted or removed as a unit, only\n * its root appears in the mutation record, so its descendants must be scanned too (otherwise\n * results rendered as a subtree would be missed and subtree removals would leak listeners).\n */\nexport const createMutationObserver = <T extends Element>(\n selector: string,\n addHandler: Handler<T>,\n removeHandler: Handler<T>,\n): MutationObserver => {\n const forEachMatch = (node: Node, handler: Handler<T>) => {\n if (node.nodeType !== Node.ELEMENT_NODE) return;\n const element = node as Element;\n if (element.matches(selector)) {\n handler(element as T);\n }\n for (const descendant of element.querySelectorAll(selector)) {\n handler(descendant as T);\n }\n };\n\n return new MutationObserver((mutations: MutationRecord[]) => {\n for (const mutation of mutations) {\n if (mutation.type !== \"childList\") continue;\n mutation.addedNodes.forEach((node) => {\n forEachMatch(node, addHandler);\n });\n mutation.removedNodes.forEach((node) => {\n forEachMatch(node, removeHandler);\n });\n }\n });\n};\n\n/**\n * A valid UBI rank: a positive integer (1-based, absolute across pages:\n * `(page - 1) * pageSize + positionOnPage`). Anything else - 0, negative,\n * fractional, NaN - is not a rank and must never be sent: a fabricated\n * ordinal is indistinguishable from data downstream.\n */\nexport const isValidOrdinal = (value: unknown): value is number =>\n typeof value === \"number\" && Number.isInteger(value) && value >= 1;\n","import type { Logger } from \"../logging\";\nimport { isValidOrdinal } from \"../utils\";\n\n/**\n * The result data resolved from a DOM element by the default data-attribute\n * convention shared by the declarative collectors.\n */\nexport type ResolvedResultData = {\n /** The identifier of the result object (e.g. product/SKU) - `data-object-id`. */\n objectId: string;\n /**\n * The absolute, 1-based rank of the result across pagination -\n * `data-ordinal`. `undefined` when missing, empty or not a positive\n * integer; the collectors validate before anything is emitted.\n */\n ordinal?: number;\n /** The backend field the object id maps to - `data-object-id-field`. */\n objectIdField?: string;\n /**\n * Overrides the emitted `action_name` (e.g. `\"add_to_cart\"`) -\n * `data-action-name`. Clicks default to `\"click\"`.\n */\n actionName?: string;\n /**\n * The server-issued `query_id` that produced the result - `data-query-id`\n * on the nearest ancestor carrying it (the element itself counts).\n */\n queryId?: string;\n /** The query text as the user entered it - `data-query` on that ancestor. */\n query?: string;\n /**\n * Any additional keys - `data-event-*` attributes read by the default\n * resolver, or extra keys returned by a custom one - are persisted as-is\n * under the event's `event_attributes`.\n */\n [key: string]: unknown;\n};\n\n// One-time flag for the malformed-JSON warning below; module-level because\n// readResultData is a free function shared by both collectors.\nlet hasWarnedMalformedJson = false;\n\n/**\n * A `data-event-*` value that looks like a JSON object or array is parsed as\n * one, so markup can carry structured attributes\n * (`data-event-filters='{\"brand\":\"acme\"}'`). Anything else - scalars\n * included - stays a string; typed scalars use the direct API. A malformed\n * candidate falls back to the raw string with a one-time warning.\n */\nconst parseAttributeValue = (\n raw: string,\n datasetKey: string,\n logger?: Logger,\n): unknown => {\n const trimmed = raw.trim();\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) {\n return raw;\n }\n try {\n return JSON.parse(trimmed);\n } catch (_exception) {\n if (!hasWarnedMalformedJson) {\n hasWarnedMalformedJson = true;\n const attribute = `data-${datasetKey.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;\n logger?.warn(\n `readResultData: the value of ${attribute} looks like JSON but does not parse; ` +\n \"it is kept as a string. Further malformed values are kept silently.\",\n );\n }\n return raw;\n }\n};\n\n/**\n * Reads the documented data-attribute convention off a result element:\n *\n * - `data-object-id`, `data-ordinal`, `data-object-id-field` and (optionally)\n * `data-action-name` on the result element itself;\n * - `data-event-*` on the result element for custom event attributes:\n * `data-event-badge=\"sale\"` becomes `badge: \"sale\"` (hyphens camelCase,\n * underscores survive - `data-event-sale_price` -> `sale_price`); a value\n * that looks like a JSON object or array is parsed as one\n * (`data-event-filters='{\"brand\":\"acme\"}'`), any other value - scalars\n * included - stays a string;\n * - `data-query-id` and `data-query` on the nearest ancestor carrying\n * `data-query-id` (typically the results container, rendered server-side).\n *\n * A missing, empty or non-positive-integer `data-ordinal` yields\n * `undefined`, never 0 - a\n * fabricated rank is indistinguishable from data downstream. The collectors\n * validate what they need and warn (once) when a required field is absent.\n *\n * @param logger receives the one-time malformed-JSON warning; the collectors\n * pass the tracker's logger.\n */\nexport const readResultData = (\n element: HTMLElement,\n logger?: Logger,\n): ResolvedResultData => {\n const container = element.closest<HTMLElement>(\"[data-query-id]\");\n // Number(\"\") is 0: an empty data-ordinal must yield undefined, never a\n // fabricated rank of 0.\n const trimmed = element.dataset.ordinal?.trim();\n const parsed = trimmed ? Number(trimmed) : Number.NaN;\n\n // data-event-* attributes become custom event attributes: the dataset key\n // minus the prefix, first character lowercased. Only the prefixed family\n // is read, so unrelated data-* (test ids, framework state) never leaks\n // into analytics.\n const extras: Record<string, unknown> = {};\n for (const key of Object.keys(element.dataset)) {\n if (/^event[A-Z]/.test(key)) {\n extras[key.charAt(5).toLowerCase() + key.slice(6)] = parseAttributeValue(\n element.dataset[key] as string,\n key,\n logger,\n );\n }\n }\n\n return {\n ...extras,\n // Cast: the collectors validate presence before anything is emitted.\n objectId: element.dataset.objectId as string,\n ordinal: isValidOrdinal(parsed) ? parsed : undefined,\n objectIdField: element.dataset.objectIdField,\n actionName: element.dataset.actionName,\n queryId: container?.dataset.queryId,\n query: container?.dataset.query,\n };\n};\n","import type { Dispatcher } from \"../dispatcher\";\nimport { isValidOrdinal } from \"../utils\";\nimport type { Collector, Detach } from \"./Collector\";\nimport { type ResolvedResultData, readResultData } from \"./resolveResult\";\n\n/**\n * The result data resolved for a clicked element. `actionName` defaults to\n * `\"click\"`; a click resolved to any other action (e.g. `\"add_to_cart\"`) is\n * routed through `trackResultEvent`, where a missing `queryId`/`ordinal` is\n * resolved from the attribution recorded when the result was clicked.\n */\nexport type ResolvedResultClick = ResolvedResultData;\n\n/**\n * Resolves the result data for a clicked element. Return `undefined` to skip\n * the click entirely. The default implementation reads the data-attribute\n * convention described on {@link readResultData}.\n */\nexport type ResultClickResolve = (\n element: HTMLElement,\n event: MouseEvent,\n) => ResolvedResultClick | undefined;\n\n/** Options for `Tracker.trackResultClicks`. */\nexport type TrackResultClicksOptions = {\n /**\n * CSS selector of result elements. Clicks are matched via `closest()`, so a\n * click on any descendant of a matching element counts.\n */\n selector: string;\n /** The root the single delegated listener attaches to. Defaults to `document`. */\n root?: Document | Element;\n /**\n * Selector for elements whose clicks must NOT be reported as result clicks,\n * checked inside the matched result (e.g. `'[data-add-to-cart]'` for a\n * button nested in the card). Without it a click on the nested button would\n * emit both its own event and a result click, inflating CTR.\n */\n ignore?: string;\n /** Custom resolver; defaults to the data-attribute convention. */\n resolve?: ResultClickResolve;\n};\n\n/**\n * Declarative result-click collection for server-rendered markup.\n *\n * A single delegated, capture-phase listener on the root resolves the clicked\n * result and emits it in the canonical joinable shape via the tracker's\n * high-level API - the integrator never hand-builds a UBI event. Delegation\n * means results added or re-rendered after `start()` are covered with no\n * rebinding and no leaked listeners; capture phase means a descendant calling\n * `stopPropagation()` during bubbling cannot lose the click.\n */\nexport class ResultClickCollector implements Collector {\n private readonly options: TrackResultClicksOptions;\n private readonly emit: (resolved: ResolvedResultClick) => void;\n private hasWarnedUnresolvable = false;\n private hasWarnedBodyScope = false;\n\n constructor(\n options: TrackResultClicksOptions,\n emit: (resolved: ResolvedResultClick) => void,\n ) {\n this.options = options;\n this.emit = emit;\n }\n\n attach(dispatcher: Dispatcher): Detach {\n const { selector, ignore } = this.options;\n const root = this.options.root ?? document;\n const resolve =\n this.options.resolve ??\n ((el: HTMLElement) => readResultData(el, dispatcher.logger));\n\n const handler = (event: Event) => {\n try {\n // composedPath()[0] sees through shadow-DOM retargeting (a click\n // inside an open shadow root would otherwise resolve to the host\n // element); fall back to target where it is unavailable.\n const path = event.composedPath?.()[0];\n const target =\n path instanceof Element ? path : (event.target as Element | null);\n const matched = target?.closest<HTMLElement>(selector);\n if (!matched) return;\n\n if (\n !this.options.resolve &&\n (matched === document.body || matched === document.documentElement)\n ) {\n // A selector matching <body>/<html> would report every click on\n // the page as a result click under the data-attribute convention -\n // almost certainly a selector mistake. Warn once and skip.\n if (!this.hasWarnedBodyScope) {\n this.hasWarnedBodyScope = true;\n dispatcher.logger?.warn(\n `trackResultClicks: the selector \"${selector}\" matched the page body itself; these ` +\n \"clicks are skipped. Narrow the selector to your result elements.\",\n );\n }\n return;\n }\n\n if (ignore) {\n const ignored = target?.closest<HTMLElement>(ignore);\n // Only a PROPER descendant suppresses: an ignore selector that\n // accidentally matches the result element itself must not silence\n // every result click.\n if (ignored && ignored !== matched && matched.contains(ignored)) {\n return;\n }\n }\n\n const resolved = resolve(matched, event as MouseEvent);\n if (!resolved) return;\n\n const actionName = resolved.actionName ?? \"click\";\n const unresolvableClick =\n actionName === \"click\" &&\n (!isValidOrdinal(resolved.ordinal) || !resolved.queryId);\n if (!resolved.objectId || unresolvableClick) {\n // Skip rather than emit an unjoinable row, and say so once: the\n // server accepts anything with a 202, so a silent skip here is the\n // only place the mistake can surface.\n if (!this.hasWarnedUnresolvable) {\n this.hasWarnedUnresolvable = true;\n dispatcher.logger?.warn(\n `trackResultClicks: skipped a click matching \"${selector}\" because objectId, ordinal or ` +\n \"queryId could not be resolved. Add data-object-id and data-ordinal to the result \" +\n \"element and data-query-id to an ancestor, or supply a resolve callback. \" +\n \"Further unresolvable clicks are skipped silently.\",\n );\n }\n return;\n }\n\n this.emit({ ...resolved, actionName });\n } catch (e) {\n dispatcher.logger?.error(\"Error during handler execution: \", e);\n }\n };\n\n // Capture phase so a matched element is still resolved even if a\n // descendant calls stopPropagation() during bubbling.\n root.addEventListener(\"click\", handler, { capture: true });\n\n return () => {\n root.removeEventListener(\"click\", handler, { capture: true });\n };\n }\n}\n","import type { Dispatcher } from \"../dispatcher\";\nimport type { ResultRef } from \"../tracker\";\nimport { createMutationObserver, debounce, isValidOrdinal } from \"../utils\";\nimport type { Collector, Detach } from \"./Collector\";\nimport { type ResolvedResultData, readResultData } from \"./resolveResult\";\n\n/**\n * The result data resolved for an element that became visible: the shared\n * {@link ResolvedResultData} shape. `actionName` is ignored here - an\n * impression is always an `impression`.\n */\nexport type ResolvedResultImpression = ResolvedResultData;\n\n/**\n * Resolves the result data for a visible element. Return `undefined` to skip\n * it. The default implementation reads the data-attribute convention described\n * on {@link readResultData}.\n */\nexport type ResultImpressionResolve = (\n element: HTMLElement,\n) => ResolvedResultImpression | undefined;\n\n/** Options for `Tracker.trackResultImpressions`. */\nexport type TrackResultImpressionsOptions = {\n /** CSS selector of result elements to observe for viewport visibility. */\n selector: string;\n /** The root observed for matching elements. Defaults to `document`. */\n root?: Document | Element;\n /** Custom resolver; defaults to the data-attribute convention. */\n resolve?: ResultImpressionResolve;\n};\n\n/** Receives resolved impressions, grouped per query. Internal wiring. */\nexport type EmitResultImpressions = (\n items: ResultRef[],\n queryId: string,\n query?: string,\n) => void;\n\nconst FLUSH_DEBOUNCE_MS = 250;\n\n/**\n * Declarative result-impression collection for server-rendered markup.\n *\n * An `IntersectionObserver` (threshold 0) reports the first time each matching\n * element enters the viewport - once per element, so a card scrolled out and\n * back does not re-fire - and a `MutationObserver` on the root discovers\n * elements added or re-rendered after `start()`, so a results grid re-rendered\n * for a new query is picked up automatically. Resolved impressions are\n * coalesced in memory for a beat and emitted per query in the canonical\n * joinable shape via the tracker's high-level API. Nothing is persisted:\n * impressions pending at detach are dropped deliberately, because after the\n * page moves on their query context can no longer be trusted.\n */\nexport class ResultImpressionCollector implements Collector {\n private readonly options: TrackResultImpressionsOptions;\n private readonly emit: EmitResultImpressions;\n private hasWarnedUnresolvable = false;\n private hasWarnedBodyScope = false;\n\n constructor(\n options: TrackResultImpressionsOptions,\n emit: EmitResultImpressions,\n ) {\n this.options = options;\n this.emit = emit;\n }\n\n attach(dispatcher: Dispatcher): Detach {\n const { selector } = this.options;\n const root = this.options.root ?? document;\n const resolve =\n this.options.resolve ??\n ((el: HTMLElement) => readResultData(el, dispatcher.logger));\n\n const pending: {\n item: ResultRef;\n queryId: string;\n query?: string;\n }[] = [];\n\n const flush = debounce(() => {\n const groups = new Map<\n string,\n { queryId: string; query?: string; items: ResultRef[] }\n >();\n for (const entry of pending) {\n // JSON key: plain concatenation would let distinct\n // (queryId, query) pairs collide.\n const key = JSON.stringify([entry.queryId, entry.query ?? null]);\n let group = groups.get(key);\n if (!group) {\n group = { queryId: entry.queryId, query: entry.query, items: [] };\n groups.set(key, group);\n }\n group.items.push(entry.item);\n }\n pending.length = 0;\n for (const group of groups.values()) {\n try {\n this.emit(group.items, group.queryId, group.query);\n } catch (e) {\n dispatcher.logger?.error(\"Error emitting impressions: \", e);\n }\n }\n }, FLUSH_DEBOUNCE_MS);\n\n const observedElements = new Set<HTMLElement>();\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n\n const element = entry.target as HTMLElement;\n // One impression per element: unobserve on first visibility.\n observer.unobserve(element);\n observedElements.delete(element);\n\n try {\n const resolved = resolve(element);\n if (!resolved) continue;\n\n if (\n !resolved.objectId ||\n !isValidOrdinal(resolved.ordinal) ||\n !resolved.queryId\n ) {\n if (!this.hasWarnedUnresolvable) {\n this.hasWarnedUnresolvable = true;\n dispatcher.logger?.warn(\n `trackResultImpressions: skipped an impression matching \"${selector}\" because objectId, ` +\n \"ordinal or queryId could not be resolved. Add data-object-id and data-ordinal to the \" +\n \"result element and data-query-id to an ancestor, or supply a resolve callback. \" +\n \"Further unresolvable impressions are skipped silently.\",\n );\n }\n continue;\n }\n\n const {\n actionName: _actionName,\n queryId: _queryId,\n query: _query,\n ...itemData\n } = resolved;\n const item: ResultRef = {\n // Spread first so data-event-* extras ride along; canonical\n // keys after.\n ...itemData,\n objectId: resolved.objectId,\n ordinal: resolved.ordinal,\n };\n if (resolved.objectIdField) {\n item.objectIdField = resolved.objectIdField;\n }\n pending.push({\n item,\n queryId: resolved.queryId,\n query: resolved.query,\n });\n flush();\n } catch (e) {\n dispatcher.logger?.error(\"Error resolving impression: \", e);\n }\n }\n },\n { threshold: 0 },\n );\n\n const addHandler = (element: HTMLElement) => {\n if (\n !this.options.resolve &&\n (element === document.body || element === document.documentElement)\n ) {\n // Observing <body> would fire an \"impression\" for the whole page on\n // load - almost certainly a selector mistake. Warn once and skip.\n if (!this.hasWarnedBodyScope) {\n this.hasWarnedBodyScope = true;\n dispatcher.logger?.warn(\n `trackResultImpressions: the selector \"${selector}\" matched the page body itself; ` +\n \"it is not observed. Narrow the selector to your result elements.\",\n );\n }\n return;\n }\n if (!observedElements.has(element)) {\n observedElements.add(element);\n observer.observe(element);\n }\n };\n\n const removeHandler = (element: HTMLElement) => {\n if (observedElements.has(element)) {\n observer.unobserve(element);\n observedElements.delete(element);\n }\n };\n\n root.querySelectorAll<HTMLElement>(selector).forEach(addHandler);\n const mutationObserver = createMutationObserver<HTMLElement>(\n selector,\n addHandler,\n removeHandler,\n );\n mutationObserver.observe(root, { childList: true, subtree: true });\n\n return () => {\n mutationObserver.disconnect();\n // Anything still pending is dropped, not persisted: see class docs.\n flush.clear();\n observer.disconnect();\n observedElements.clear();\n };\n }\n}\n","import type { Logger } from \"./Logger\";\n\n/** Options for {@link ConsoleLogger}. */\nexport type ConsoleLoggerOptions = {\n /**\n * When true, `debug()` and `info()` are emitted as well. `warn()` and\n * `error()` are always emitted regardless. Defaults to false, so production\n * pages stay quiet except for warnings and errors that indicate lost data.\n */\n readonly verbose?: boolean;\n};\n\n/**\n * The default {@link Logger}. Routes tracker diagnostics to the browser console\n * so that an integrator can see, without any extra wiring, why events are not\n * arriving (bad site_id, non-retryable server response, storage failures, ...).\n * `debug`/`info` are suppressed unless `verbose` is set.\n */\nexport class ConsoleLogger implements Logger {\n private readonly verbose: boolean;\n\n constructor(options: ConsoleLoggerOptions = {}) {\n this.verbose = options.verbose ?? false;\n }\n\n debug(msg: string, ...data: any[]): void {\n if (this.verbose) {\n console.debug(msg, ...data);\n }\n }\n\n info(msg: string, ...data: any[]): void {\n if (this.verbose) {\n console.info(msg, ...data);\n }\n }\n\n warn(msg: string, ...data: any[]): void {\n console.warn(msg, ...data);\n }\n\n error(msg: string, ...data: any[]): void {\n console.error(msg, ...data);\n }\n}\n","import type { Logger } from \"./Logger\";\n\nconst report = (e: unknown): void => {\n try {\n console.error(\"The configured tracker logger threw: \", e);\n } catch {\n // Nothing left to report to.\n }\n};\n\n/**\n * Wraps a logger so a throwing consumer logger can never escape the tracker's\n * own catch blocks (which themselves log) or `dispatch()`. The original\n * failure is surfaced via `console.error` rather than swallowed. Internal.\n */\nexport const safeLogger = (logger: Logger): Logger => ({\n debug(msg: string, ...data: any[]): void {\n try {\n logger.debug(msg, ...data);\n } catch (e) {\n report(e);\n }\n },\n info(msg: string, ...data: any[]): void {\n try {\n logger.info(msg, ...data);\n } catch (e) {\n report(e);\n }\n },\n warn(msg: string, ...data: any[]): void {\n try {\n logger.warn(msg, ...data);\n } catch (e) {\n report(e);\n }\n },\n error(msg: string, ...data: any[]): void {\n try {\n logger.error(msg, ...data);\n } catch (e) {\n report(e);\n }\n },\n});\n","import type { Logger } from \"../logging\";\nimport type { Event as UbiEvent } from \"../types\";\nimport type { Sink } from \"./Sink\";\n\n/**\n * Emits events to multiple sinks\n */\nexport class AggregateSink implements Sink {\n private sinks: Set<Sink>;\n private readonly logger?: Logger;\n\n constructor(sinks: Set<Sink>, logger?: Logger) {\n this.sinks = sinks;\n this.logger = logger;\n }\n\n public add(sink: Sink) {\n this.sinks.add(sink);\n }\n\n public delete(sink: Sink) {\n this.sinks.delete(sink);\n }\n\n emit(data: UbiEvent): void {\n this.sinks.forEach((sink) => {\n try {\n sink.emit(data);\n } catch (error) {\n this.logger?.error(\"Error emitting event to sink\", {\n error,\n sink,\n data,\n });\n }\n });\n }\n}\n","import type { Logger } from \"../logging\";\nimport type { Event as UbiEvent } from \"../types\";\nimport type { Sink } from \"./Sink\";\n\n/**\n * Configuration for the BatchSink, which queues events and sends them in bulk for better\n * network efficiency. This is the default sink when `endpointHost` is set in TrackerOptions.\n * Supports automatic retries with exponential backoff, persistent retry queues across page\n * reloads, and reliable delivery via the Beacon API on page unload.\n */\nexport type BatchSinkOptions = {\n /**\n * Base URL of the API endpoint (e.g., \"https://api.example.com\"). A path\n * prefix for a reverse proxy is allowed; the fixed Releval track-event\n * path is appended.\n */\n readonly endpointHost: string;\n /** Maximum events per batch before auto-flush. Defaults to 30. */\n readonly flushSize?: number;\n /** Milliseconds between automatic flushes. Defaults to 1000. */\n readonly flushIntervalMs?: number;\n /** Maximum retry attempts for failed batches. Defaults to 5. */\n readonly maxRetries?: number;\n /** Base delay in ms for exponential backoff. Defaults to 1000. */\n readonly retryBaseDelayMs?: number;\n /** Maximum delay in ms for exponential backoff. Defaults to 30000. */\n readonly retryMaxDelayMs?: number;\n /** Storage for persisting the retry queue across page reloads. Defaults to localStorage. */\n readonly storage?: Storage;\n /** Logger for internal diagnostics. */\n readonly logger?: Logger;\n};\n\ninterface RetryBatch {\n events: UbiEvent[];\n attempt: number;\n /** Persist time (epoch ms). */\n ts: number;\n}\n\n// Prefix of the persisted retry-queue key; the sanitized endpoint is\n// appended per instance, so two trackers (or a consumer-built BatchSink\n// pointed at another host) can never drain each other's batches.\nconst STORAGE_KEY = \"_ubi_batch_retry_\";\n\n// The Releval track-event path is fixed. A reverse-proxy prefix belongs on\n// endpointHost itself (e.g. \"https://shop.example.com/releval\").\nconst TRACK_EVENT_PATH = \"/api/v1/ubi/track-event\";\n\n// Bound the persisted retry queue so a long outage cannot grow an unbounded\n// on-device log of full event payloads: the newest batches win, and anything\n// older than a day is dropped on read.\nconst MAX_PERSISTED_BATCHES = 10;\nconst MAX_PERSISTED_AGE_MS = 24 * 60 * 60 * 1000;\n\n/**\n * A sink that batches events, retries failures with exponential backoff,\n * and uses the Beacon API for reliable delivery on page unload.\n */\nexport class BatchSink implements Sink {\n private readonly url: string;\n private readonly storageKey: string;\n private readonly flushSize: number;\n private readonly flushIntervalMs: number;\n private readonly maxRetries: number;\n private readonly retryBaseDelayMs: number;\n private readonly retryMaxDelayMs: number;\n private readonly storage?: Storage;\n private readonly logger?: Logger;\n\n private queue: UbiEvent[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n // Pending retry batches keyed by their timer, so they can be persisted (not\n // lost) if the page unloads mid-backoff. `ts` is stamped at persist time.\n private readonly pendingRetries = new Map<\n ReturnType<typeof setTimeout>,\n Omit<RetryBatch, \"ts\">\n >();\n private disposed = false;\n\n private readonly boundOnVisibilityChange: () => void;\n private readonly boundOnPageHide: (event: PageTransitionEvent) => void;\n\n constructor(options: BatchSinkOptions) {\n const {\n endpointHost,\n flushSize = 30,\n flushIntervalMs = 1000,\n maxRetries = 5,\n retryBaseDelayMs = 1000,\n retryMaxDelayMs = 30000,\n storage,\n logger,\n } = options;\n\n this.url = `${endpointHost.replace(/\\/+$/, \"\")}${TRACK_EVENT_PATH}`;\n this.storageKey = `${STORAGE_KEY}${endpointHost.replace(/[^a-zA-Z0-9]+/g, \"-\")}_`;\n this.flushSize = flushSize;\n this.flushIntervalMs = flushIntervalMs;\n this.maxRetries = maxRetries;\n this.retryBaseDelayMs = retryBaseDelayMs;\n this.retryMaxDelayMs = retryMaxDelayMs;\n this.storage = storage;\n this.logger = logger;\n\n this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);\n this.boundOnPageHide = this.onPageHide.bind(this);\n\n this.start();\n this.drainStoredRetries();\n }\n\n /**\n * Add an event to the batch queue. Triggers a flush if the queue reaches flushSize.\n */\n emit(event: UbiEvent): void {\n if (this.disposed) return;\n\n this.queue.push(event);\n\n if (this.queue.length >= this.flushSize) {\n this.flush();\n }\n }\n\n /**\n * Flush the current queue immediately via fetch.\n * Returns a promise that resolves when the batch is sent (or scheduled for retry).\n */\n flush(): Promise<void> {\n if (this.queue.length === 0) return Promise.resolve();\n\n const batch = this.queue;\n this.queue = [];\n\n return this.sendBatch(batch, 0);\n }\n\n /**\n * Start the flush timer and unload listeners.\n */\n start(): void {\n if (this.flushTimer) return;\n\n this.flushTimer = setInterval(() => {\n this.flush();\n }, this.flushIntervalMs);\n\n // visibilitychange covers most navigations on Chrome/Firefox/Android;\n // pagehide is the event Safari/iOS fire on same-tab navigation, so both\n // are needed to reliably capture the terminal click. Neither forfeits the\n // back/forward cache the way unload/beforeunload would.\n if (typeof document !== \"undefined\") {\n document.addEventListener(\n \"visibilitychange\",\n this.boundOnVisibilityChange,\n );\n }\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"pagehide\", this.boundOnPageHide);\n }\n }\n\n /**\n * Stop the flush timer, flush remaining events, and clean up.\n */\n dispose(): void {\n this.disposed = true;\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n // Persist any in-flight retry batches so they are not lost on teardown.\n this.persistPendingRetries();\n\n if (typeof document !== \"undefined\") {\n document.removeEventListener(\n \"visibilitychange\",\n this.boundOnVisibilityChange,\n );\n }\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"pagehide\", this.boundOnPageHide);\n }\n\n // Best-effort flush of remaining events\n if (this.queue.length > 0) {\n this.sendViaBeacon(this.queue);\n this.queue = [];\n }\n }\n\n /** Visible for testing - returns the current queue length */\n get pendingCount(): number {\n return this.queue.length;\n }\n\n private onVisibilityChange(): void {\n if (typeof document === \"undefined\") return;\n if (document.visibilityState === \"hidden\") {\n // hidden is the last callback guaranteed to run before a backgrounded tab\n // may be frozen and discarded (mobile Chrome/Android can discard without\n // ever firing pagehide), so flush the queue and persist in-flight retries\n // here too, not only on pagehide.\n this.flushViaBeacon();\n this.persistPendingRetries();\n } else if (document.visibilityState === \"visible\") {\n // The page came back rather than being discarded; resume the persisted\n // retries in memory. drainStoredRetries clears storage first, so a later\n // load does not also replay them (no duplicate).\n this.drainStoredRetries();\n }\n }\n\n private onPageHide(event: PageTransitionEvent): void {\n this.flushViaBeacon();\n // On a real unload (not bfcache), in-memory retry timers will die with the\n // page, so persist any pending retry batches for the next page load. On a\n // bfcache entry (event.persisted) the page may resume with its timers\n // intact, so do not persist (that would duplicate on resume).\n if (!event.persisted) {\n this.persistPendingRetries();\n }\n }\n\n /**\n * Clear and persist any in-flight retry batches so a page discard or unload\n * does not lose them. Empties the map, so it is safe to call more than once.\n */\n private persistPendingRetries(): void {\n for (const [timer, batch] of this.pendingRetries) {\n clearTimeout(timer);\n this.persistForRetry(batch.events, batch.attempt);\n }\n this.pendingRetries.clear();\n }\n\n /**\n * Flush all queued events using the Beacon API (reliable on page unload).\n */\n private flushViaBeacon(): void {\n if (this.queue.length === 0) return;\n\n const batch = this.queue;\n this.queue = [];\n this.sendViaBeacon(batch);\n }\n\n private sendViaBeacon(events: UbiEvent[]): void {\n if (\n typeof navigator !== \"undefined\" &&\n typeof navigator.sendBeacon === \"function\"\n ) {\n try {\n const blob = new Blob([JSON.stringify({ events })], {\n type: \"application/json\",\n });\n // sendBeacon returns false when the UA refuses to queue the beacon\n // (over the ~64KiB cap, too many pending beacons). Fall through to the\n // keepalive fetch in that case rather than silently dropping the batch.\n if (navigator.sendBeacon(this.url, blob)) {\n this.logger?.info(\n `ubi: queued ${events.length} event(s) via beacon to ${this.url}`,\n );\n return;\n }\n } catch (e) {\n this.logger?.error(\"Beacon API failed: \", e);\n }\n }\n\n this.sendViaKeepaliveFetch(events);\n }\n\n /**\n * Fallback delivery on unload when the beacon is unavailable or refused.\n * Persists the batch for the next page load if the fetch fails.\n */\n private sendViaKeepaliveFetch(events: UbiEvent[]): void {\n try {\n fetch(this.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ events }),\n keepalive: true,\n })\n .then((response) => {\n if (!response.ok) {\n this.persistForRetry(events, 0);\n }\n })\n .catch(() => {\n this.persistForRetry(events, 0);\n });\n } catch (e) {\n this.logger?.error(\"Keepalive fetch failed: \", e);\n this.persistForRetry(events, 0);\n }\n }\n\n private async sendBatch(events: UbiEvent[], attempt: number): Promise<void> {\n try {\n const response = await fetch(this.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ events }),\n // Routine flushes use a normal fetch. keepalive is reserved for the\n // unload path (sendViaKeepaliveFetch): keepalive request bodies share a\n // small (~64KiB) browser-wide budget that sustained batching plus\n // retries would exceed, making fetch reject and silently churn batches.\n // The terminal flush during navigation is covered by the unload beacon.\n });\n\n if (!response.ok) {\n if (this.isRetryable(response.status)) {\n this.scheduleRetry(events, attempt);\n } else {\n this.logger?.error(\n `Batch send failed with status ${response.status}, not retrying`,\n );\n }\n } else {\n this.logger?.info(\n `ubi: sent ${events.length} event(s) to ${this.url} (${response.status})`,\n );\n }\n } catch (_e) {\n // Network error - retryable\n this.scheduleRetry(events, attempt);\n }\n }\n\n private isRetryable(status: number): boolean {\n return status >= 500 || status === 429;\n }\n\n private scheduleRetry(events: UbiEvent[], attempt: number): void {\n if (this.disposed) {\n this.persistForRetry(events, attempt);\n return;\n }\n\n if (attempt >= this.maxRetries) {\n this.logger?.error(\n `Batch send failed after ${this.maxRetries} attempts, discarding ${events.length} events`,\n );\n return;\n }\n\n const delay = this.calculateBackoff(attempt);\n this.logger?.warn(\n `Retrying batch (attempt ${attempt + 1}/${this.maxRetries}) in ${delay}ms`,\n );\n\n const timer = setTimeout(() => {\n this.pendingRetries.delete(timer);\n this.sendBatch(events, attempt + 1);\n }, delay);\n this.pendingRetries.set(timer, { events, attempt });\n }\n\n private calculateBackoff(attempt: number): number {\n const exponentialDelay = this.retryBaseDelayMs * 2 ** attempt;\n const cappedDelay = Math.min(exponentialDelay, this.retryMaxDelayMs);\n // Full jitter: a value in [0, cappedDelay]. This keeps clients from\n // retrying in lockstep (a biased-high \"cap + jitter\" collapses to exactly\n // the cap for every client once the cap is reached - a thundering herd).\n return Math.random() * cappedDelay;\n }\n\n private persistForRetry(events: UbiEvent[], attempt: number): void {\n if (!this.storage) return;\n\n try {\n const stored = this.loadStoredRetries();\n stored.push({ events, attempt, ts: Date.now() });\n const bounded = stored.slice(-MAX_PERSISTED_BATCHES);\n if (bounded.length < stored.length) {\n this.logger?.warn(\n `ubi: dropped ${stored.length - bounded.length} oldest persisted retry batch(es) (cap ${MAX_PERSISTED_BATCHES})`,\n );\n }\n this.storage.setItem(this.storageKey, JSON.stringify(bounded));\n } catch (e) {\n this.logger?.error(\"Failed to persist retry queue: \", e);\n }\n }\n\n private loadStoredRetries(): RetryBatch[] {\n if (!this.storage) return [];\n\n try {\n const data = this.storage.getItem(this.storageKey);\n const parsed: unknown = data ? JSON.parse(data) : [];\n if (!Array.isArray(parsed)) return [];\n return (parsed as RetryBatch[]).filter(\n (batch) =>\n Array.isArray(batch?.events) &&\n typeof batch?.ts === \"number\" &&\n Date.now() - batch.ts <= MAX_PERSISTED_AGE_MS,\n );\n } catch {\n return [];\n }\n }\n\n private drainStoredRetries(): void {\n if (!this.storage) return;\n\n const batches = this.loadStoredRetries();\n if (batches.length === 0) return;\n\n // Clear storage immediately to prevent duplicate processing across tabs\n this.storage.removeItem(this.storageKey);\n\n this.logger?.info(\n `ubi: resuming ${batches.length} persisted retry batch(es)`,\n );\n\n for (const batch of batches) {\n this.sendBatch(batch.events, batch.attempt);\n }\n }\n}\n","import type { Event as UbiEvent } from \"../types\";\nimport type { Sink } from \"./Sink\";\n\n/**\n * Logs every event to the browser console. The development default: used\n * automatically when no `endpointHost` is configured and no sink has been\n * added, so events are visible without anything leaving the page. With an\n * endpoint configured, delivery goes through `BatchSink` instead.\n */\nexport class ConsoleSink implements Sink {\n emit(event: UbiEvent): void {\n console.log(event);\n }\n}\n","import type { Logger } from \"../logging\";\n\n/**\n * The search context recorded for a result when it was clicked, used to\n * attribute later conversion events (add_to_cart, purchase) for the same\n * object back to the originating query.\n */\nexport type AttributionRecord = {\n /** The server-issued query id of the search that produced the result. */\n queryId: string;\n /** The absolute 1-based rank the result was clicked at, if recorded. */\n ordinal?: number;\n /** The query text as the user entered it, if recorded. */\n query?: string;\n};\n\ntype StoredRecord = AttributionRecord & {\n /** The session the record was registered in; resolution is scoped to it. */\n sessionId: string;\n /** Registration time (epoch ms), for the age sweep. */\n ts: number;\n};\n\ntype StoredMap = Record<string, StoredRecord>;\n\nconst STORAGE_KEY = \"_ubi_attribution_\";\n\n// A record can never outlive the session that wrote it (the sessionId check\n// below enforces that), so this age cap only guards against clock skew and\n// entries orphaned by a session id that was cleared from storage. It matches\n// the session's maximum duration.\nconst MAX_AGE_MS = 24 * 60 * 60 * 1000;\n\n// Bounds the on-device footprint. Fifty distinct clicked results per session\n// is far beyond any real shopping journey; beyond it the oldest records go.\nconst MAX_ENTRIES = 50;\n\nexport type AttributionStoreOptions = {\n localStorage: Storage;\n sessionStorage: Storage;\n /** Resolves the current session id, so records can be scoped to it. */\n sessionId: () => string;\n /** Resolves the current logger for storage diagnostics. */\n logger?: () => Logger | undefined;\n};\n\n/**\n * Object-keyed attribution store: `objectId -> { queryId, ordinal, query }`.\n *\n * Written by `Tracker.trackResultClick` and read by `Tracker.trackResultEvent`\n * (and `getResultAttribution`), so a conversion on a later page - including\n * after a full document navigation - resolves the query that produced the\n * clicked result without the integrator threading `query_id` by hand.\n *\n * Records go to BOTH sessionStorage and localStorage: sessionStorage covers\n * same-tab navigation, and the localStorage copy covers a result opened in a\n * new tab (which does not inherit sessionStorage). Resolution is scoped to the\n * session that registered the record, so the shared localStorage copy can\n * never re-attribute a later visit in a different session.\n *\n * The store performs no storage IO at construction; it reads on `get` and\n * writes on `register` only.\n */\nexport class AttributionStore {\n private readonly options: AttributionStoreOptions;\n\n constructor(options: AttributionStoreOptions) {\n this.options = options;\n }\n\n /** Records the attribution for a clicked result. */\n register(objectId: string, record: AttributionRecord): void {\n const stored: StoredRecord = {\n queryId: record.queryId,\n ordinal: record.ordinal,\n query: record.query,\n sessionId: this.options.sessionId(),\n ts: Date.now(),\n };\n\n for (const storage of [\n this.options.sessionStorage,\n this.options.localStorage,\n ]) {\n const map = this.pruneStale(this.load(storage));\n if (!(objectId in map)) {\n // Only a NEW object needs room: re-clicking an already-recorded\n // result at the cap must not evict an innocent oldest entry.\n this.enforceCap(map);\n }\n map[objectId] = stored;\n this.save(storage, map);\n }\n }\n\n /**\n * Returns the attribution recorded for an object in the current session, or\n * undefined. Stale entries (other session, over age) are pruned on read.\n */\n get(objectId: string): AttributionRecord | undefined {\n for (const storage of [\n this.options.sessionStorage,\n this.options.localStorage,\n ]) {\n const map = this.load(storage);\n const stored = map[objectId];\n if (!stored) continue;\n\n if (this.isStale(stored)) {\n delete map[objectId];\n this.save(storage, map);\n continue;\n }\n\n const record: AttributionRecord = { queryId: stored.queryId };\n if (stored.ordinal !== undefined) record.ordinal = stored.ordinal;\n if (stored.query !== undefined) record.query = stored.query;\n return record;\n }\n return undefined;\n }\n\n private isStale(stored: StoredRecord): boolean {\n return (\n typeof stored.queryId !== \"string\" ||\n stored.sessionId !== this.options.sessionId() ||\n Date.now() - stored.ts > MAX_AGE_MS\n );\n }\n\n /** Drops stale entries (other session, over age, malformed). */\n private pruneStale(map: StoredMap): StoredMap {\n for (const key of Object.keys(map)) {\n if (typeof map[key]?.ts !== \"number\" || this.isStale(map[key])) {\n delete map[key];\n }\n }\n return map;\n }\n\n /** Makes room for one new entry, evicting the oldest beyond the cap. */\n private enforceCap(map: StoredMap): void {\n const keys = Object.keys(map);\n if (keys.length >= MAX_ENTRIES) {\n keys\n .sort((a, b) => map[a].ts - map[b].ts)\n .slice(0, keys.length - MAX_ENTRIES + 1)\n .forEach((key) => {\n delete map[key];\n });\n }\n }\n\n private load(storage: Storage): StoredMap {\n try {\n const raw = storage.getItem(STORAGE_KEY);\n if (!raw) return {};\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== \"object\" || parsed === null) {\n // A corrupt value would otherwise break attribution on every read;\n // drop it so the store self-heals.\n storage.removeItem(STORAGE_KEY);\n return {};\n }\n return parsed as StoredMap;\n } catch (e) {\n this.logError(\"Error reading the attribution store: \", e);\n try {\n storage.removeItem(STORAGE_KEY);\n } catch {\n // Removal is best-effort; a read-only storage keeps the corrupt value\n // and every load keeps returning the empty map.\n }\n return {};\n }\n }\n\n private save(storage: Storage, map: StoredMap): void {\n try {\n storage.setItem(STORAGE_KEY, JSON.stringify(map));\n } catch (e) {\n // Quota errors must never escape into the caller's click handler.\n this.logError(\"Error writing the attribution store: \", e);\n }\n }\n\n private logError(message: string, e: unknown): void {\n this.options.logger?.()?.error(message, e);\n }\n}\n","import { ulid } from \"ulidx\";\nimport type { Enricher } from \"./enrichers\";\nimport type { Logger } from \"./logging\";\nimport type { Sink } from \"./sinks\";\nimport type { Event as UbiEvent } from \"./types\";\n\n// Injected by the build (tsdown `define`); absent under ts-jest.\ndeclare const __TRACKER_VERSION__: string | undefined;\nconst TRACKER_VERSION =\n typeof __TRACKER_VERSION__ === \"string\" ? __TRACKER_VERSION__ : \"dev\";\n\n/**\n * Receives events from collectors, runs them through enrichers, and emits them to the sink.\n * Collectors receive a Dispatcher when attached and use it to send events. Library consumers\n * do not typically implement this interface directly - the Tracker creates the appropriate\n * dispatcher internally.\n */\nexport interface Dispatcher {\n readonly logger?: Logger;\n\n dispatch(event: UbiEvent): void;\n}\n\n/**\n * Enriches events and dispatches to the sink\n */\nexport class DefaultDispatcher implements Dispatcher {\n readonly logger?: Logger;\n private enrichers: Set<Enricher>;\n private sink: Sink;\n\n constructor(enrichers: Set<Enricher>, sink: Sink, logger?: Logger) {\n this.logger = logger;\n this.sink = sink;\n this.enrichers = enrichers;\n }\n\n public dispatch(event: UbiEvent) {\n if (!event.timestamp) {\n event.timestamp = new Date().toISOString();\n }\n\n if (!event.event_attributes) {\n event.event_attributes = {};\n }\n\n // Stamp a stable per-event id, set once at creation time, so delivery\n // retries (which can genuinely duplicate: the persisted retry queue plus\n // a beacon whose response never arrived) stay deduplicable in analysis.\n // It lives under event_attributes because the server's TrackEvent DTO\n // drops unknown top-level fields but preserves unknown attribute fields\n // into the ClickHouse JSON column.\n if (!event.event_attributes.event_id) {\n event.event_attributes.event_id = ulid();\n }\n\n // Which tracker build produced the row, for supportability once several\n // versions are deployed across customer sites.\n if (!event.event_attributes.tracker) {\n event.event_attributes.tracker = { version: TRACKER_VERSION };\n }\n\n this.enrichers.forEach((enricher) => {\n try {\n enricher.enrich(event);\n } catch (e) {\n this.logger?.error(\"Error enriching event: \", e);\n }\n });\n\n // With `debug: true` the default console logger prints this, which makes\n // it the one-line way to watch events leave the page against a server\n // that answers 202 to everything.\n this.logger?.debug(\"ubi: dispatching event\", event);\n\n try {\n this.sink.emit(event);\n } catch (e) {\n this.logger?.error(\"Error emitting event to sink: \", e);\n }\n }\n}\n","import type { Event as UbiEvent } from \"./../types\";\nimport type { Enricher } from \"./Enricher\";\n\n/**\n * Enriches the event with information about the browser:\n * - user_agent\n * - language\n * - screen resolution\n * - webdriver (only when set: the one cheap automation signal, so analysis\n * can filter bot and headless traffic that would distort CTR)\n */\nexport class BrowserEnricher implements Enricher {\n enrich(event: UbiEvent): void {\n if (!event.event_attributes) {\n event.event_attributes = {};\n }\n\n const browser: Record<string, unknown> = {\n ...event.event_attributes.browser,\n user_agent: navigator.userAgent,\n language: navigator.language,\n resolution: {\n width: screen.width,\n height: screen.height,\n },\n };\n if (navigator.webdriver) {\n browser.webdriver = true;\n }\n\n event.event_attributes.browser = browser;\n }\n}\n","import type { Event as UbiEvent } from \"../types\";\nimport type { Enricher } from \"./Enricher\";\n\n/**\n * Returns a stable anonymous identifier for the current client/device.\n * Called by the ClientIdEnricher on every event to populate the `client_id` field.\n * The Tracker supplies this automatically using a ULID persisted in localStorage.\n */\nexport type ClientIdProvider = () => string;\n\n/**\n * Enriches the event with\n * - client_id\n */\nexport class ClientIdEnricher implements Enricher {\n private readonly clientIdProvider: ClientIdProvider;\n\n constructor(clientIdProvider: ClientIdProvider) {\n this.clientIdProvider = clientIdProvider;\n }\n\n enrich(event: UbiEvent): void {\n if (!event.client_id) {\n event.client_id = this.clientIdProvider();\n }\n }\n}\n","import type { Event as UbiEvent } from \"./../types\";\nimport type { Enricher } from \"./Enricher\";\n\n/**\n * The values the OptionsEnricher stamps. `application` and `siteId` are fixed\n * at construction; `userId` is a getter so {@link Tracker.setUserId} takes\n * effect on events dispatched after the call (the same late-binding pattern\n * as the session and client id enrichers). Holding copied values rather than\n * the caller's options object also closes the accidental lever where\n * mutating the passed options after construction silently changed events.\n */\nexport type OptionsEnricherValues = {\n application: string;\n siteId?: string;\n userId: () => string | undefined;\n};\n\n/**\n * Enriches the event with configured values:\n * - application\n * - user_id (late-bound via the getter)\n * - siteId (stamped as `site_id` on the event)\n */\nexport class OptionsEnricher implements Enricher {\n private readonly values: OptionsEnricherValues;\n\n constructor(values: OptionsEnricherValues) {\n this.values = values;\n }\n\n enrich(data: UbiEvent): void {\n if (!data.application) {\n data.application = this.values.application;\n }\n\n const userId = this.values.userId();\n if (!data.user_id && userId) {\n data.user_id = userId;\n }\n\n // site_id is a Releval extension to the upstream UBI schema, declared in\n // scripts/schema-overrides.json so it is typed on Event like any other field.\n if (!data.site_id && this.values.siteId) {\n data.site_id = this.values.siteId;\n }\n }\n}\n","import type { Event as UbiEvent } from \"./../types\";\nimport type { Enricher } from \"./Enricher\";\n\n/**\n * Enriches the event with information about the page:\n * - url\n * - title\n * - referrer\n */\nexport class PageEnricher implements Enricher {\n enrich(event: UbiEvent): void {\n if (!event.event_attributes) {\n event.event_attributes = {};\n }\n\n event.event_attributes.page = {\n ...event.event_attributes.page,\n url: location.href,\n title: document.title,\n referrer: document.referrer || \"\",\n };\n }\n}\n","import type { Event as UbiEvent } from \"./../types\";\nimport type { Enricher } from \"./Enricher\";\n\n/**\n * Returns the current session ID, or undefined if no session is active.\n * Called by the SessionEnricher on every event to populate the `session_id` field.\n * The Tracker supplies this automatically via its SessionManager.\n */\nexport type SessionIdProvider = () => string | undefined;\n\n/**\n * Enriches the event with\n * - session_id\n *\n * Optionally calls onActivity to update the session's last activity timestamp.\n */\nexport class SessionEnricher implements Enricher {\n private readonly sessionIdProvider: SessionIdProvider;\n private readonly onActivity?: () => void;\n\n constructor(sessionIdProvider: SessionIdProvider, onActivity?: () => void) {\n this.sessionIdProvider = sessionIdProvider;\n this.onActivity = onActivity;\n }\n\n enrich(event: UbiEvent): void {\n if (!event.session_id) {\n event.session_id = this.sessionIdProvider();\n }\n this.onActivity?.();\n }\n}\n","/**\n * An in-memory {@link Storage} implementation used when no real Web Storage is\n * available - most importantly under server-side rendering (no `window`), where\n * touching `localStorage`/`document.cookie` would throw. It provides page-scoped\n * state with no cross-page persistence, so the tracker constructs and runs\n * inertly on the server and starts persisting once it hydrates in the browser.\n */\nexport class MemoryStorage implements Storage {\n private readonly store = new Map<string, string>();\n\n get length(): number {\n return this.store.size;\n }\n\n clear(): void {\n this.store.clear();\n }\n\n getItem(key: string): string | null {\n return this.store.get(key) ?? null;\n }\n\n key(index: number): string | null {\n return Array.from(this.store.keys())[index] ?? null;\n }\n\n removeItem(key: string): void {\n this.store.delete(key);\n }\n\n setItem(key: string, value: string): void {\n this.store.set(key, String(value));\n }\n}\n","import { ulid } from \"ulidx\";\nimport type { Logger } from \"../logging\";\n\nconst CLIENT_ID_KEY = \"_ubi_client_id_\";\n\n/**\n * Gets or creates a stable anonymous client ID persisted in localStorage.\n * This ID survives across sessions and is used to identify the device/browser.\n * A failing storage read or write is logged and a freshly generated id is\n * still returned, so tracking continues (per page) without persistence.\n */\nexport function getOrCreateClientId(storage: Storage, logger?: Logger): string {\n let clientId: string | null = null;\n try {\n clientId = storage.getItem(CLIENT_ID_KEY);\n } catch (e) {\n logger?.error(\"Error reading the client id: \", e);\n }\n if (!clientId) {\n clientId = ulid();\n try {\n storage.setItem(CLIENT_ID_KEY, clientId);\n } catch (e) {\n logger?.error(\"Error persisting the client id: \", e);\n }\n }\n return clientId;\n}\n","import { ulid } from \"ulidx\";\nimport type { Logger } from \"../logging\";\n\n/**\n * Configuration for the SessionManager, which handles automatic session ID rotation based\n * on inactivity timeout and maximum session duration. The Tracker creates a SessionManager\n * internally - these options are exposed via `TrackerOptions.sessionInactivityTimeoutMs`\n * and `TrackerOptions.maxSessionDurationMs`.\n */\nexport type SessionManagerOptions = {\n storage: Storage;\n /** Milliseconds of inactivity before rotating the session. Default: 30 minutes. */\n inactivityTimeoutMs?: number;\n /** Maximum session duration in milliseconds. Default: 24 hours. */\n maxSessionDurationMs?: number;\n /** Logger for storage diagnostics. */\n logger?: Logger;\n};\n\ninterface SessionData {\n id: string;\n createdAt: number;\n lastActivity: number;\n}\n\nconst SESSION_KEY = \"_ubi_session_\";\n\n// Tolerate small clock differences between tabs and devices; anything further\n// in the future is corrupt or from a skewed clock and cannot be trusted (a\n// far-future lastActivity would pin the session alive forever).\nconst MAX_CLOCK_SKEW_MS = 5 * 60 * 1000;\nconst THIRTY_MINUTES = 30 * 60 * 1000;\nconst TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;\n\n/**\n * Manages session IDs with inactivity timeout and maximum duration.\n */\nexport class SessionManager {\n private readonly storage: Storage;\n private readonly inactivityTimeoutMs: number;\n private readonly maxSessionDurationMs: number;\n private readonly logger?: Logger;\n private session: SessionData;\n\n constructor(options: SessionManagerOptions) {\n this.storage = options.storage;\n this.inactivityTimeoutMs = options.inactivityTimeoutMs ?? THIRTY_MINUTES;\n this.maxSessionDurationMs =\n options.maxSessionDurationMs ?? TWENTY_FOUR_HOURS;\n this.logger = options.logger;\n this.session = this.loadOrCreate();\n }\n\n get sessionId(): string {\n // Reconcile with storage first, so tabs converge on the session another tab\n // rotated to, and a session stays alive as long as any tab is active\n // (rather than each tab ping-ponging its own in-memory copy back to storage).\n this.syncFromStorage();\n if (this.isExpired()) {\n this.rotate();\n }\n return this.session.id;\n }\n\n private syncFromStorage(): void {\n try {\n const stored = this.storage.getItem(SESSION_KEY);\n if (!stored) return;\n const data: SessionData = JSON.parse(stored);\n if (!this.isUsable(data, Date.now())) return;\n // Adopt a different session (another tab rotated) or a newer heartbeat of\n // the same session (another tab kept it alive).\n if (\n data.id !== this.session.id ||\n data.lastActivity > this.session.lastActivity\n ) {\n this.session = data;\n }\n } catch {\n // Ignore corrupt storage; keep the in-memory session.\n }\n }\n\n /** Update the last activity timestamp. Call on every event. */\n touch(): void {\n this.session.lastActivity = Date.now();\n this.persist();\n }\n\n /**\n * Marks user activity that is not an event: reconciles with storage,\n * rotates if genuinely expired, then refreshes the activity timestamp.\n * Called from `Tracker.start()` so that on a classic multi-page site every\n * page load counts as a hit - the GA-style 30-minute convention - instead\n * of only dispatched events keeping the session alive. Without it, reading\n * a product page for 31 minutes rotates the session and drops attribution.\n */\n activity(): void {\n void this.sessionId;\n this.touch();\n }\n\n /** True when stored session data is well-formed and temporally sane. */\n private isUsable(data: SessionData, now: number): boolean {\n return (\n typeof data.id === \"string\" &&\n data.id.length > 0 &&\n Number.isFinite(data.createdAt) &&\n Number.isFinite(data.lastActivity) &&\n data.createdAt <= now + MAX_CLOCK_SKEW_MS &&\n data.lastActivity <= now + MAX_CLOCK_SKEW_MS\n );\n }\n\n private isExpired(): boolean {\n const now = Date.now();\n const inactivityExpired =\n now - this.session.lastActivity > this.inactivityTimeoutMs;\n const maxDurationExpired =\n now - this.session.createdAt > this.maxSessionDurationMs;\n return inactivityExpired || maxDurationExpired;\n }\n\n private rotate(): void {\n this.session = this.createSession();\n this.persist();\n }\n\n private loadOrCreate(): SessionData {\n try {\n const stored = this.storage.getItem(SESSION_KEY);\n if (stored) {\n const data: SessionData = JSON.parse(stored);\n if (this.isUsable(data, Date.now())) {\n return data;\n }\n }\n } catch {\n // Corrupted data, create fresh session\n }\n const session = this.createSession();\n this.persist(session);\n return session;\n }\n\n private createSession(): SessionData {\n const now = Date.now();\n return {\n id: ulid(),\n createdAt: now,\n lastActivity: now,\n };\n }\n\n private persist(session?: SessionData): void {\n try {\n this.storage.setItem(\n SESSION_KEY,\n JSON.stringify(session ?? this.session),\n );\n } catch (e) {\n // A blocked or full storage (the retry queue shares the same origin\n // quota) must not break tracking; the in-memory session keeps working\n // for the life of this page.\n this.logger?.error(\"Error persisting the session: \", e);\n }\n }\n}\n","import { MemoryStorage } from \"./MemoryStorage\";\n\nexport * from \"./ClientId\";\nexport * from \"./MemoryStorage\";\nexport * from \"./SessionManager\";\n\nfunction tryGetStorage(\n storageName: \"localStorage\" | \"sessionStorage\",\n): Storage | undefined {\n // Guard the bare `window` reference so construction does not throw under SSR.\n if (typeof window === \"undefined\") {\n return undefined;\n }\n // EVERYTHING below stays inside the try: the `window.localStorage` accessor\n // ITSELF throws SecurityError when access is denied (sandboxed iframes,\n // browsers with site data blocked) - not just the setItem call. An\n // embeddable tracker must never let that reach the customer's page script.\n try {\n if (!(storageName in window)) {\n return undefined;\n }\n const storage = window[storageName] as Storage;\n if (!storage) {\n return undefined;\n }\n const uid = `_ubi_probe_${Date.now()}`;\n storage.setItem(uid, uid);\n const result = storage.getItem(uid) === uid;\n storage.removeItem(uid);\n return result ? storage : undefined;\n } catch (_exception) {\n return undefined;\n }\n}\n\n// With no usable Web Storage (blocked by the user, or no DOM at all under\n// SSR), fall back to in-memory storage so nothing throws. Identity then lives\n// only as long as the page. The previous cookie fallback serialised every key\n// (session, retry queue, attribution) into a single cookie sent on every\n// request to the customer's origin and silently failed past ~4 KB - strictly\n// worse than not persisting.\nexport const initLocalStorage = (): Storage =>\n tryGetStorage(\"localStorage\") ?? new MemoryStorage();\n\nexport const initSessionStorage = (): Storage =>\n tryGetStorage(\"sessionStorage\") ?? new MemoryStorage();\n","import {\n type AttributionRecord,\n AttributionStore,\n} from \"./attribution/AttributionStore\";\nimport {\n type Collector,\n type Detach,\n type ResolvedResultClick,\n ResultClickCollector,\n ResultImpressionCollector,\n type TrackResultClicksOptions,\n type TrackResultImpressionsOptions,\n} from \"./collectors\";\nimport { DefaultDispatcher, type Dispatcher } from \"./dispatcher\";\nimport {\n BrowserEnricher,\n ClientIdEnricher,\n type Enricher,\n OptionsEnricher,\n PageEnricher,\n SessionEnricher,\n} from \"./enrichers\";\nimport { ConsoleLogger, type Logger, safeLogger } from \"./logging\";\nimport { AggregateSink, BatchSink, ConsoleSink, type Sink } from \"./sinks\";\nimport {\n getOrCreateClientId,\n initLocalStorage,\n initSessionStorage,\n SessionManager,\n} from \"./storage\";\nimport type { Event as UbiEvent } from \"./types\";\nimport { isValidOrdinal } from \"./utils\";\n\n// Bounds the pre-start buffer (dispatches before start() are replayed at\n// start()); beyond it the oldest buffered events are dropped with a warning.\nconst PRE_START_BUFFER_LIMIT = 100;\n\n// Bounds the per-instance impression dedup set (one entry per reported\n// queryId+objectId pair). Generous: eviction only risks a re-fire for a pair\n// last seen a thousand pairs ago.\nconst IMPRESSION_DEDUP_LIMIT = 1000;\n\n/**\n * Options shared by every {@link TrackerOptions} variant. Not used directly -\n * see {@link TrackerOptions} for the endpoint pairing rules.\n */\nexport type TrackerBaseOptions = {\n /** The name of the application */\n application: string;\n /**\n * The initial user id attached to events (`user_id`). Must be an opaque,\n * pseudonymous identifier - never an email address or name. Change it\n * mid-session (login/logout) with {@link Tracker.setUserId}.\n */\n userId?: string;\n /** Session inactivity timeout in milliseconds. Defaults to 30 minutes. */\n sessionInactivityTimeoutMs?: number;\n /** Maximum session duration in milliseconds. Defaults to 24 hours. */\n maxSessionDurationMs?: number;\n /**\n * When true, the default console logger also prints debug/info\n * diagnostics: every dispatched event and every delivery outcome. This is\n * the one-line install check against a server that answers 202 to\n * everything. Warnings and errors are printed regardless. Applies to the\n * default `ConsoleLogger` only - a supplied `logger` receives all levels\n * and filters for itself. Defaults to false.\n */\n debug?: boolean;\n /**\n * A logger to receive internal diagnostics. Replaces the default\n * `ConsoleLogger`, so pass one here to route (or silence) tracker logging\n * instead of writing to the console. To fan out to several destinations,\n * pass a logger that wraps them.\n */\n logger?: Logger;\n};\n\n/**\n * Configuration passed to `new Tracker()`. Delivering to Releval requires the\n * pair: `endpointHost` (the base URL browsers use to reach the Releval\n * deployment - not your site's own origin) and `siteId` (the public Site\n * identifier issued by Releval). The server silently drops events whose\n * `site_id` does not match a registered Site, so the type requires the two\n * together. Omit `endpointHost` for development mode: events go to the\n * console - or only to sinks added with `addSink`, once any are added - and\n * nothing leaves the page.\n */\nexport type TrackerOptions = TrackerBaseOptions &\n (\n | {\n /**\n * Base URL browsers use to reach the Releval deployment, e.g.\n * `\"https://releval.example.com\"` - not your site's own origin. A\n * path prefix for a reverse proxy is allowed; events POST to\n * `<endpointHost>/api/v1/ubi/track-event`.\n */\n endpointHost: string;\n /**\n * The public Site identifier issued by Releval when registering a\n * Site. Stamped on every event in the request body; identity travels\n * in the payload, not a header, because `navigator.sendBeacon`\n * cannot set custom headers on the unload path.\n */\n siteId: string;\n }\n | {\n /** Omit to log events to the console instead of delivering (development mode). */\n endpointHost?: undefined;\n /** Without an endpoint the site id is optional; it is stamped when present. */\n siteId?: string;\n }\n );\n\n/**\n * A reference to a search result that was interacted with. Used by the\n * high-level `trackResult*` methods to build the UBI `object` + `position`\n * attributes so callers do not hand-roll the event shape.\n */\nexport type ResultRef = {\n /** The identifier of the object (e.g. product/SKU) that was interacted with. */\n objectId: string;\n /**\n * The absolute, 1-based rank of the result across pagination -\n * `(page - 1) * pageSize + positionOnPage` with `page` 1-based - not the\n * per-page position. It must equal index + 1 of this object in the\n * `query_response_hit_ids` your backend sent to track-query for this\n * `query_id`: either one query_id spans all pages with absolute ordinals,\n * or each page fetch is its own track-query call with its own query_id.\n */\n ordinal: number;\n /** The backend field the object id maps to (UBI `object_id_field`). Optional. */\n objectIdField?: string;\n /**\n * Any additional keys are persisted as-is under the event's\n * `event_attributes` (e.g. `badge: \"sale\"`). Values must be\n * JSON-serialisable. Keys the tracker itself owns - `object`, `position`,\n * `event_id`, `tracker`, `page`, `browser` - are overwritten by it; use\n * {@link Tracker.dispatch} for full control of the event shape.\n */\n [key: string]: unknown;\n};\n\n/** Options for {@link Tracker.trackResultEvent}. */\nexport type TrackResultEventOptions = {\n /** The UBI `action_name` (e.g. `\"add_to_cart\"`, `\"purchase\"`, `\"view\"`). */\n actionName: string;\n /** The identifier of the object (e.g. product/SKU) the event is about. */\n objectId: string;\n /** The backend field the object id maps to (UBI `object_id_field`). Optional. */\n objectIdField?: string;\n /**\n * The absolute, 1-based rank the result was returned at. Optional: when\n * omitted it is resolved from the attribution recorded by\n * {@link Tracker.trackResultClick} for the same `objectId`. When neither is\n * available the event carries no position.\n */\n ordinal?: number;\n /**\n * The server-issued `query_id` that produced this result. Optional: when\n * omitted it is resolved from the attribution recorded by\n * {@link Tracker.trackResultClick} for the same `objectId`, so a conversion\n * on a later page joins to the originating query without the integrator\n * threading it by hand. When neither is available the event is sent\n * unattributed and a warning is logged once.\n */\n queryId?: string;\n /** The query text as the user entered it, if known (populates `user_query`). */\n query?: string;\n /**\n * Any additional keys are persisted as-is under the event's\n * `event_attributes` (e.g. `badge: \"sale\"`). Values must be\n * JSON-serialisable. Keys the tracker itself owns - `object`, `position`,\n * `event_id`, `tracker`, `page`, `browser` - are overwritten by it; use\n * {@link Tracker.dispatch} for full control of the event shape.\n */\n [key: string]: unknown;\n};\n\n/** Options for {@link Tracker.trackResultClick}. */\nexport type TrackResultClickOptions = ResultRef & {\n /** The server-issued `query_id` that produced this result. Required. */\n queryId: string;\n /** The query text as the user entered it, if known (populates `user_query`). */\n query?: string;\n /** Override the `action_name`. Defaults to `\"click\"`. */\n actionName?: string;\n};\n\n/** Options for {@link Tracker.trackResultImpression}. */\nexport type TrackResultImpressionOptions = {\n /** The results that became visible, each with its object id and absolute rank. */\n items: ResultRef[];\n /** The server-issued `query_id` that produced these results. Required. */\n queryId: string;\n /** The query text as the user entered it, if known (populates `user_query`). */\n query?: string;\n};\n\n/** Options for {@link Tracker.trackSearch}. */\nexport type TrackSearchOptions = {\n /** The query text as the user entered it. */\n query: string;\n /** The server-issued `query_id` for this search. Required. */\n queryId: string;\n /**\n * Any additional keys are persisted as-is under the event's\n * `event_attributes` (e.g. `badge: \"sale\"`). Values must be\n * JSON-serialisable. Keys the tracker itself owns - `object`, `position`,\n * `event_id`, `tracker`, `page`, `browser` - are overwritten by it; use\n * {@link Tracker.dispatch} for full control of the event shape.\n */\n [key: string]: unknown;\n};\n\n/**\n * The entry point of `@releval/tracker`: collects User Behavior Insights\n * events - searches, result impressions, result clicks and the conversions\n * that follow - in the canonical joinable shape\n * (`query_id` + `object_id` + `ordinal`) and delivers them to a Releval\n * deployment's track-event API.\n *\n * Events flow through a small pipeline: the high-level `track*` methods (or\n * the declarative collectors) build canonical events, enrichers stamp\n * context (application, session, client id, page, browser), and sinks\n * deliver - batched with retry to the endpoint, or to the console in\n * development. Nothing is delivered before {@link Tracker.start}; earlier\n * dispatches are buffered and replayed.\n *\n * @example\n * ```ts\n * const tracker = new Tracker({\n * application: \"primary-search\",\n * siteId: \"YOUR_SITE_ID\",\n * endpointHost: \"https://releval.example.com\",\n * });\n * tracker.start();\n *\n * tracker.trackSearch({ query, queryId }); // ids come from your backend\n * tracker.trackResultClick({ objectId, ordinal, queryId });\n * tracker.trackResultEvent({ actionName: \"add_to_cart\", objectId });\n * ```\n *\n * The full integration flow is documented at\n * https://releval.co/docs/user-behavior-insights/browser-tracker\n */\nexport class Tracker {\n private readonly enrichers: Set<Enricher>;\n private readonly options: TrackerOptions;\n private readonly _sessionManager: SessionManager;\n private started: boolean;\n // Distinguishes \"not started yet\" (buffer and replay) from \"stopped after\n // running\" (drop): see dispatch().\n private hasEverStarted = false;\n private collectors: Set<Collector>;\n private readonly detaches = new Map<Collector, Detach>();\n private sink?: AggregateSink;\n private batchSink?: BatchSink;\n private readonly logger: Logger;\n private dispatcher?: Dispatcher;\n private _clientId?: string;\n private _userId?: string;\n private _attribution?: AttributionStore;\n private hasWarnedMissingQueryId = false;\n private hasWarnedUnattributedResult = false;\n private hasWarnedInvalidOrdinal = false;\n // Events dispatched before start() are buffered (bounded) and replayed at\n // start(), because framework lifecycles routinely dispatch first: React\n // runs child effects before the parent provider's start() effect.\n private readonly preStartBuffer: UbiEvent[] = [];\n private hasWarnedPreStartOverflow = false;\n // Impressions already reported by this instance, keyed queryId+objectId, so\n // re-renders and remounted components cannot inflate the CTR denominator.\n private readonly seenImpressions = new Set<string>();\n // User-supplied sinks (via addSink). Kept separately from the auto-created\n // endpoint/console sink so that a stop()/start() cycle rebuilds the internal\n // pipeline without discarding them.\n private readonly extraSinks = new Set<Sink>();\n\n constructor(options: TrackerOptions) {\n this.options = options;\n this.collectors = new Set<Collector>();\n this.started = false;\n\n // Default to a console logger so failures (dropped events, bad site_id,\n // non-retryable responses) are visible without any extra wiring. A caller\n // can replace it via the `logger` option (e.g. to silence the console).\n // Wrapped so a throwing consumer logger can never escape the tracker's\n // own catch blocks.\n this.logger = safeLogger(\n options.logger ?? new ConsoleLogger({ verbose: options.debug }),\n );\n\n this._sessionManager = new SessionManager({\n storage: this.localStorage,\n inactivityTimeoutMs: options.sessionInactivityTimeoutMs,\n maxSessionDurationMs: options.maxSessionDurationMs,\n logger: this.logger,\n });\n\n this._userId = options.userId;\n\n this.enrichers = new Set<Enricher>([\n new OptionsEnricher({\n application: options.application,\n siteId: options.siteId,\n userId: () => this._userId,\n }),\n new BrowserEnricher(),\n new PageEnricher(),\n new SessionEnricher(\n () => this.sessionId,\n () => this._sessionManager.touch(),\n ),\n new ClientIdEnricher(() => this.clientId),\n ]);\n }\n\n private _localStorage?: Storage;\n\n /**\n * The tracker's local storage (with fallbacks when Web Storage is\n * unavailable). Internal: collectors and stores receive it via options.\n */\n private get localStorage(): Storage {\n if (!this._localStorage) {\n this._localStorage = initLocalStorage();\n }\n return this._localStorage;\n }\n\n private _sessionStorage?: Storage;\n\n /**\n * The tracker's session storage (with fallbacks when Web Storage is\n * unavailable). Internal: collectors and stores receive it via options.\n */\n private get sessionStorage(): Storage {\n if (!this._sessionStorage) {\n this._sessionStorage = initSessionStorage();\n }\n return this._sessionStorage;\n }\n\n /**\n * Gets the session identifier. Automatically rotates on inactivity or max duration.\n */\n get sessionId(): string {\n return this._sessionManager.sessionId;\n }\n\n /**\n * Gets the stable anonymous client/device ID. Persisted in localStorage across sessions.\n */\n get clientId(): string {\n if (!this._clientId) {\n this._clientId = getOrCreateClientId(this.localStorage, this.logger);\n }\n return this._clientId;\n }\n\n /**\n * Sets (or clears, with `undefined`) the user id stamped on events - for a\n * login or logout that happens without a page reload. Applies to events\n * dispatched after the call; events already queued or persisted for retry\n * keep the identity they were stamped with. An explicit `user_id` on a\n * dispatched event still wins. Callable before `start()`. Does not rotate\n * the session or touch the client id. The value must be an opaque,\n * pseudonymous identifier - never an email address or name.\n */\n public setUserId(userId: string | undefined): void {\n this._userId = userId;\n }\n\n /**\n * The object-keyed attribution store, created on first use so construction\n * performs no storage IO for it.\n */\n private get attribution(): AttributionStore {\n if (!this._attribution) {\n this._attribution = new AttributionStore({\n localStorage: this.localStorage,\n sessionStorage: this.sessionStorage,\n sessionId: () => this.sessionId,\n logger: () => this.logger,\n });\n }\n return this._attribution;\n }\n\n /**\n * Returns the attribution recorded by {@link Tracker.trackResultClick} for\n * an object in the current session, or undefined. Use it to build custom\n * conversion payloads (e.g. a checkout page resolving several line items).\n */\n public getResultAttribution(objectId: string): AttributionRecord | undefined {\n try {\n return this.attribution.get(objectId);\n } catch (e) {\n this.logger.error(\"Error reading result attribution: \", e);\n return undefined;\n }\n }\n\n /**\n * Reports that a search ran, with the server-issued `query_id`. Call this\n * after your search backend returns results.\n *\n * Dispatches a `search` event (`message_type: \"QUERY\"`) carrying `query_id`\n * and `user_query`. Without it a query that gets no impression and no click\n * leaves no event at all, so abandonment and reformulation cannot be\n * analysed downstream.\n */\n public trackSearch(options: TrackSearchOptions): void {\n try {\n const { query, queryId, ...extras } = options;\n if (!queryId && !this.hasWarnedMissingQueryId) {\n // Warn once, endpoint or not: development mode must surface the same\n // mistakes production would. A search event without a query_id still\n // reaches the server, but nothing can be joined back to it, so it is\n // worthless for relevance analysis. Loud, non-breaking diagnostic for\n // JS callers (the TypeScript type already requires queryId).\n this.hasWarnedMissingQueryId = true;\n this.logger.warn(\n \"Tracker.trackSearch: called without a queryId. The search event is still emitted \" +\n \"but result clicks and impressions cannot be attributed back to this query. Pass \" +\n \"the query_id issued by your search backend (Releval track-query): \" +\n \"tracker.trackSearch({ query, queryId }).\",\n );\n }\n\n const event: UbiEvent = {\n action_name: \"search\",\n message_type: \"QUERY\",\n user_query: query,\n };\n if (queryId) {\n event.query_id = queryId;\n }\n if (Object.keys(extras).length > 0) {\n event.event_attributes = { ...extras };\n }\n this.dispatch(event);\n } catch (e) {\n this.logger.error(\"Error tracking search: \", e);\n }\n }\n\n /**\n * Dispatches a UBI event for a search result in the canonical shape\n * (`query_id`, `event_attributes.object.object_id`, `event_attributes.position.ordinal`),\n * so callers never hand-build it. Prefer {@link Tracker.trackResultClick} /\n * {@link Tracker.trackResultImpression}; use this directly for conversions\n * (e.g. `actionName: \"add_to_cart\"` or `\"purchase\"`), where `queryId` and\n * `ordinal` may be omitted and are resolved from the attribution recorded\n * when the result was clicked - including on a later page. When `queryId`\n * IS supplied and matches the recorded click, a missing `ordinal`/`query`\n * is still borrowed from it, so the row shape does not depend on which\n * page supplied the id.\n */\n public trackResultEvent(options: TrackResultEventOptions): void {\n try {\n const {\n actionName,\n objectId,\n objectIdField,\n ordinal: suppliedOrdinal,\n queryId: suppliedQueryId,\n query: suppliedQuery,\n ...extras\n } = options;\n let ordinal = suppliedOrdinal;\n let queryId = suppliedQueryId;\n let query = suppliedQuery;\n\n if (\n queryId === undefined ||\n ordinal === undefined ||\n query === undefined\n ) {\n const attribution = this.attribution.get(objectId);\n if (attribution) {\n if (queryId === undefined) {\n queryId = attribution.queryId;\n ordinal = ordinal ?? attribution.ordinal;\n query = query ?? attribution.query;\n } else if (attribution.queryId === queryId) {\n // The caller supplied the query (e.g. read from the DOM) but not\n // the rank: borrow it from the recorded click for the SAME query,\n // so the row shape does not depend on which page supplied the id.\n // A mismatched query borrows nothing - never mix two searches.\n ordinal = ordinal ?? attribution.ordinal;\n query = query ?? attribution.query;\n }\n }\n }\n\n if (ordinal !== undefined && !isValidOrdinal(ordinal)) {\n // Never send a fabricated rank: drop the field, keep the event.\n if (!this.hasWarnedInvalidOrdinal) {\n this.hasWarnedInvalidOrdinal = true;\n this.logger.warn(\n `Tracker: ignoring invalid ordinal ${String(ordinal)} for \"${objectId}\" - ` +\n \"an ordinal is a positive integer: (page - 1) * pageSize + positionOnPage.\",\n );\n }\n ordinal = undefined;\n }\n\n if (!queryId && !this.hasWarnedUnattributedResult) {\n // Warn once. The event is still delivered, but without a query_id it\n // cannot be joined back to a search for relevance analysis.\n this.hasWarnedUnattributedResult = true;\n this.logger.warn(\n `Tracker.trackResultEvent: no queryId was supplied and no attribution is recorded for \"${objectId}\" ` +\n \"in this session, so the event is sent unattributed. Either pass queryId explicitly or report \" +\n \"the originating click with tracker.trackResultClick so conversions resolve automatically.\",\n );\n }\n\n const object: { object_id: string; object_id_field?: string } = {\n object_id: objectId,\n };\n if (objectIdField) {\n object.object_id_field = objectIdField;\n }\n\n const event: UbiEvent = {\n action_name: actionName,\n // Caller extras first, canonical keys after: object and position\n // always win a key collision.\n event_attributes: { ...extras, object },\n };\n if (typeof ordinal === \"number\") {\n event.event_attributes!.position = { ordinal };\n }\n if (queryId) {\n event.query_id = queryId;\n }\n if (query) {\n event.user_query = query;\n }\n\n this.dispatch(event);\n } catch (e) {\n this.logger.error(\"Error tracking result event: \", e);\n }\n }\n\n /**\n * Dispatches a `click` event for a clicked search result, attributed to\n * `queryId` (required at the call site by design), and records the\n * attribution for the object so later conversion events for it - on this\n * page or a later one - resolve the originating query automatically.\n */\n public trackResultClick(options: TrackResultClickOptions): void {\n try {\n const { objectId, ordinal, queryId, query } = options;\n this.trackResultEvent({\n ...options,\n actionName: options.actionName ?? \"click\",\n });\n this.attribution.register(objectId, { queryId, ordinal, query });\n } catch (e) {\n this.logger.error(\"Error tracking result click: \", e);\n }\n }\n\n /**\n * Dispatches an `impression` event for each result that became visible,\n * attributed to `queryId`. Emits one canonical event per item so impressions\n * join to clicks on `object_id`/`ordinal`.\n *\n * Each `(queryId, objectId)` pair is reported ONCE per Tracker instance:\n * re-renders, virtualized-list remounts and re-discovered elements do not\n * inflate the impression count (the CTR denominator), while a new `queryId`\n * re-fires for results returned by consecutive searches. A full page load\n * builds a fresh Tracker and so starts fresh; when revisits matter,\n * deduplicate downstream on distinct `(query_id, object_id)`.\n */\n public trackResultImpression(options: TrackResultImpressionOptions): void {\n try {\n for (const item of options.items) {\n const key = `${options.queryId}\\u0000${item.objectId}`;\n if (this.seenImpressions.has(key)) {\n continue;\n }\n if (this.seenImpressions.size >= IMPRESSION_DEDUP_LIMIT) {\n // Bounded: evict the oldest pair (Set preserves insertion order)\n // rather than growing without limit in a long-lived SPA session.\n const oldest = this.seenImpressions.values().next().value;\n if (oldest !== undefined) {\n this.seenImpressions.delete(oldest);\n }\n }\n this.seenImpressions.add(key);\n\n this.trackResultEvent({\n ...item,\n actionName: \"impression\",\n queryId: options.queryId,\n query: options.query,\n });\n }\n } catch (e) {\n this.logger.error(\"Error tracking result impression: \", e);\n }\n }\n\n /**\n * Adds an enricher that runs on every event before emission (e.g. stamping\n * an A/B variant, store or locale - the split key for any comparison).\n * Returns a disposer that removes it again.\n */\n public addEnricher(enricher: Enricher): () => void {\n try {\n this.enrichers.add(enricher);\n return () => {\n this.enrichers.delete(enricher);\n };\n } catch (e) {\n this.logger.error(\"Error adding enricher: \", e);\n return () => {};\n }\n }\n\n /**\n * Registers a collector so it attaches at start() (or immediately when the\n * tracker is already started), and returns a function that detaches and\n * unregisters it again.\n */\n private registerCollector(collector: Collector): () => void {\n try {\n if (!this.collectors.has(collector)) {\n if (this.started) {\n try {\n this.detaches.set(collector, collector.attach(this.dispatcher!));\n } catch (e) {\n this.logger.error(\"Error attaching collector: \", e);\n }\n }\n this.collectors.add(collector);\n }\n } catch (e) {\n this.logger.error(\"Error adding collector: \", e);\n }\n\n return () => {\n try {\n const detach = this.detaches.get(collector);\n this.detaches.delete(collector);\n this.collectors.delete(collector);\n detach?.();\n } catch (e) {\n this.logger.error(\"Error detaching collector: \", e);\n }\n };\n }\n\n /**\n * Binds declarative result-click collection to the DOM: one delegated\n * listener reports clicks on elements matching `selector` in the canonical\n * joinable shape, reading `data-object-id` / `data-ordinal` from the result\n * element and `data-query-id` from its nearest ancestor (or a custom\n * `resolve`). A resolved `data-action-name` other than `click` (e.g.\n * `add_to_cart`) is routed through {@link Tracker.trackResultEvent}, so its\n * attribution can resolve from the recorded click.\n *\n * Use `ignore` for interactive descendants of a result (an add-to-cart\n * button inside the card) so their clicks are not double-reported as result\n * clicks.\n *\n * @returns a function that stops this collection again.\n */\n public trackResultClicks(options: TrackResultClicksOptions): () => void {\n try {\n const collector = new ResultClickCollector(options, (resolved) =>\n this.emitResolvedResultClick(resolved),\n );\n return this.registerCollector(collector);\n } catch (e) {\n this.logger.error(\"Error creating result click collector: \", e);\n return () => {};\n }\n }\n\n /** Routes a resolved declarative click through the high-level API. */\n private emitResolvedResultClick(resolved: ResolvedResultClick): void {\n // Spread, not field-by-field: custom event attributes resolved from\n // data-event-* (or returned by a custom resolver) ride along untouched.\n const { actionName: resolvedAction, ...data } = resolved;\n const actionName = resolvedAction ?? \"click\";\n if (\n actionName === \"click\" &&\n resolved.queryId &&\n typeof resolved.ordinal === \"number\"\n ) {\n this.trackResultClick({\n ...data,\n objectId: resolved.objectId,\n ordinal: resolved.ordinal,\n queryId: resolved.queryId,\n });\n return;\n }\n\n this.trackResultEvent({ ...data, actionName });\n }\n\n /**\n * Binds declarative result-impression collection to the DOM: each element\n * matching `selector` emits one canonical `impression` event the first time\n * it enters the viewport, attributed via the same data-attribute convention\n * as {@link Tracker.trackResultClicks} (or a custom `resolve`). Elements\n * added or re-rendered after `start()` are discovered automatically.\n *\n * @returns a function that stops this collection again.\n */\n public trackResultImpressions(\n options: TrackResultImpressionsOptions,\n ): () => void {\n try {\n const collector = new ResultImpressionCollector(\n options,\n (items, queryId, query) =>\n this.trackResultImpression({ items, queryId, query }),\n );\n return this.registerCollector(collector);\n } catch (e) {\n this.logger.error(\"Error creating result impression collector: \", e);\n return () => {};\n }\n }\n\n /**\n * Adds a sink that receives every emitted event (e.g. mirroring into your\n * own analytics, or a test spy). Returns a disposer that removes it again.\n */\n public addSink(sink: Sink): () => void {\n try {\n this.extraSinks.add(sink);\n // If already started, add to the live aggregate so the change reaches the\n // dispatcher (which holds a stable reference to it). If not started, the\n // sink is picked up when start() builds the pipeline.\n if (this.started && this.sink) {\n this.sink.add(sink);\n }\n return () => {\n this.extraSinks.delete(sink);\n this.sink?.delete(sink);\n };\n } catch (e) {\n this.logger.error(\"Error adding sink: \", e);\n return () => {};\n }\n }\n\n /**\n * starts the tracker and attaches all collectors.\n */\n public start() {\n try {\n if (this.started) {\n return;\n }\n\n const { endpointHost, siteId } = this.options;\n if (endpointHost && !siteId) {\n this.logger.error(\n \"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.\",\n );\n }\n\n // A page load is user activity: on a classic multi-page site nothing\n // may be dispatched for a while after start(), and without this a\n // 31-minute read of a product page would rotate the session and drop\n // attribution even though the user never left.\n this._sessionManager.activity();\n\n // Build a fresh internal pipeline every start(), so a stop()/start()\n // cycle never reuses a disposed BatchSink. The effective sink is always\n // an AggregateSink whose reference is stable for the dispatcher, so\n // addSink() after start() reaches it via .add().\n this.sink = this.buildSink();\n this.dispatcher = new DefaultDispatcher(\n this.enrichers,\n this.sink,\n this.logger,\n );\n\n this.collectors.forEach((collector) => {\n try {\n this.detaches.set(collector, collector.attach(this.dispatcher!));\n } catch (e) {\n this.logger.error(\"Error attaching collector: \", e);\n }\n });\n\n this.started = true;\n this.hasEverStarted = true;\n\n if (this.preStartBuffer.length > 0) {\n const buffered = this.preStartBuffer.splice(0);\n this.logger.debug(\n `ubi: replaying ${buffered.length} event(s) dispatched before start()`,\n );\n for (const event of buffered) {\n this.dispatcher!.dispatch(event);\n }\n }\n } catch (e) {\n this.logger.error(\"Error starting tracker: \", e);\n }\n }\n\n /**\n * Builds the effective sink from the configured endpoint plus any user sinks.\n * ConsoleSink is used only when there is neither an endpoint nor a user sink\n * (the development default). The result is always an AggregateSink so its\n * reference stays stable across addSink() calls.\n */\n private buildSink(): AggregateSink {\n const { endpointHost } = this.options;\n const sinks = new Set<Sink>();\n\n if (endpointHost) {\n this.batchSink = new BatchSink({\n endpointHost,\n storage: this.localStorage,\n logger: this.logger,\n });\n sinks.add(this.batchSink);\n }\n\n for (const sink of this.extraSinks) {\n sinks.add(sink);\n }\n\n if (sinks.size === 0) {\n sinks.add(new ConsoleSink());\n }\n\n return new AggregateSink(sinks, this.logger);\n }\n\n /**\n * Dispatches an event through the tracker pipeline (enrichers → sinks).\n * Use this to send custom events that aren't captured by a collector.\n * Safe to call before start(): the event is buffered (bounded), stamped\n * with the timestamp of the moment it happened, and replayed once start()\n * runs.\n * @param event The event to dispatch.\n */\n public dispatch(event: UbiEvent) {\n if (!this.started || !this.dispatcher) {\n if (this.hasEverStarted) {\n // After stop() the tracker is off: drop, do not queue for a start()\n // that may never come (a later delivery would misdate the interaction\n // anyway). Pre-START dispatches below are different: framework\n // lifecycles routinely dispatch before start().\n this.logger.debug(\"ubi: tracker is stopped; event dropped\", event);\n return;\n }\n if (!event.timestamp) {\n event.timestamp = new Date().toISOString();\n }\n // Stamp the identity current at DISPATCH time: a logout between this\n // dispatch and start() must not rewrite who did it.\n if (!event.user_id && this._userId) {\n event.user_id = this._userId;\n }\n if (this.preStartBuffer.length >= PRE_START_BUFFER_LIMIT) {\n if (!this.hasWarnedPreStartOverflow) {\n this.hasWarnedPreStartOverflow = true;\n this.logger.warn(\n `More than ${PRE_START_BUFFER_LIMIT} events were dispatched before start(); dropping the oldest. Call start() earlier.`,\n );\n }\n this.preStartBuffer.shift();\n }\n this.preStartBuffer.push(event);\n return;\n }\n this.dispatcher.dispatch(event);\n }\n\n /**\n * Forces immediate delivery of any queued events, rather than waiting for the\n * next batch interval. Useful before a critical action or a hard navigation.\n * Resolves once the current batch has been sent (or scheduled for retry).\n * A no-op that resolves immediately when there is no batching sink (events go\n * to the console) or the tracker is stopped.\n */\n public flush(): Promise<void> {\n try {\n return this.batchSink?.flush() ?? Promise.resolve();\n } catch (e) {\n this.logger.error(\"Error flushing events: \", e);\n return Promise.resolve();\n }\n }\n\n /**\n * stops the tracker and detaches all collectors.\n */\n public stop() {\n try {\n if (!this.started) {\n return;\n }\n\n this.detaches.forEach((detach) => {\n try {\n detach();\n } catch (e) {\n this.logger.error(\"Error detaching collector: \", e);\n }\n });\n this.detaches.clear();\n\n if (this.batchSink) {\n this.batchSink.dispose();\n }\n\n // Discard the internal pipeline so a subsequent start() rebuilds a fresh\n // sink and dispatcher rather than reusing the disposed BatchSink (which\n // would silently drop every event). User sinks live in extraSinks and are\n // re-combined on the next start().\n this.sink = undefined;\n this.batchSink = undefined;\n this.dispatcher = undefined;\n\n this.started = false;\n } catch (e) {\n this.logger.error(\"Error stopping tracker: \", e);\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@releval/tracker",
|
|
3
|
+
"version": "1.0.0-bootstrap.0",
|
|
4
|
+
"description": "User Behavior Insights tracker for capturing events to help improve the website search experience.",
|
|
5
|
+
"main": "dist/releval-tracker.cjs",
|
|
6
|
+
"module": "dist/releval-tracker.mjs",
|
|
7
|
+
"types": "dist/releval-tracker.d.cts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"author": "Releval <hello@releval.co>",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ubi",
|
|
13
|
+
"user-behavior-insights",
|
|
14
|
+
"analytics",
|
|
15
|
+
"tracking",
|
|
16
|
+
"click-tracking",
|
|
17
|
+
"impression-tracking",
|
|
18
|
+
"search-attribution",
|
|
19
|
+
"event-tracking"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/releval/tracker.git",
|
|
24
|
+
"directory": "src/tracker"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://releval.co",
|
|
27
|
+
"bugs": "https://github.com/releval/tracker/issues",
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"./dist/releval-tracker.global.js"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/releval-tracker.d.mts",
|
|
43
|
+
"default": "./dist/releval-tracker.mjs"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/releval-tracker.d.cts",
|
|
47
|
+
"default": "./dist/releval-tracker.cjs"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"./react": {
|
|
51
|
+
"import": {
|
|
52
|
+
"types": "./dist/react.d.mts",
|
|
53
|
+
"default": "./dist/react.mjs"
|
|
54
|
+
},
|
|
55
|
+
"require": {
|
|
56
|
+
"types": "./dist/react.d.cts",
|
|
57
|
+
"default": "./dist/react.cjs"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"./script": "./dist/releval-tracker.global.js"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"prepack": "node scripts/prepack.mjs",
|
|
64
|
+
"prepublishOnly": "npm run build",
|
|
65
|
+
"watch": "tsc -w",
|
|
66
|
+
"test": "jest",
|
|
67
|
+
"test:watch": "jest --watch",
|
|
68
|
+
"generate:types": "node scripts/generate-types.mjs",
|
|
69
|
+
"build": "tsdown",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"lint": "biome check",
|
|
72
|
+
"lint:fix": "biome check --write",
|
|
73
|
+
"docs:check": "typedoc"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"ulidx": "^2.3.0"
|
|
77
|
+
},
|
|
78
|
+
"peerDependencies": {
|
|
79
|
+
"react": ">=17"
|
|
80
|
+
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"react": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@biomejs/biome": "^2.5.12",
|
|
88
|
+
"@testing-library/react": "^16.3.3",
|
|
89
|
+
"@types/jest": "^30.0.0",
|
|
90
|
+
"@types/react": "^19.2.18",
|
|
91
|
+
"@types/react-dom": "^19.2.7",
|
|
92
|
+
"jest": "^30.5.1",
|
|
93
|
+
"jest-environment-jsdom": "^30.5.1",
|
|
94
|
+
"react": "^19.2.8",
|
|
95
|
+
"react-dom": "^19.2.8",
|
|
96
|
+
"ts-jest": "^29.4.12",
|
|
97
|
+
"tsdown": "^0.22.14",
|
|
98
|
+
"typedoc": "^0.28.20",
|
|
99
|
+
"typescript": "^5.9.3"
|
|
100
|
+
},
|
|
101
|
+
"jest": {
|
|
102
|
+
"preset": "ts-jest",
|
|
103
|
+
"testEnvironment": "jsdom",
|
|
104
|
+
"moduleFileExtensions": [
|
|
105
|
+
"ts",
|
|
106
|
+
"js"
|
|
107
|
+
],
|
|
108
|
+
"transform": {
|
|
109
|
+
"^.+\\.ts$": [
|
|
110
|
+
"ts-jest",
|
|
111
|
+
{
|
|
112
|
+
"tsconfig": {
|
|
113
|
+
"module": "CommonJS",
|
|
114
|
+
"moduleResolution": "Node"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
|
120
|
+
"testPathIgnorePatterns": [
|
|
121
|
+
"/node_modules/",
|
|
122
|
+
"/dist/"
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
"publishConfig": {
|
|
126
|
+
"access": "public",
|
|
127
|
+
"provenance": true
|
|
128
|
+
},
|
|
129
|
+
"jsdelivr": "dist/releval-tracker.global.js",
|
|
130
|
+
"unpkg": "dist/releval-tracker.global.js"
|
|
131
|
+
}
|