@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
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
4
|
+
const require_tracker = require("./tracker.cjs");
|
|
5
|
+
let react = require("react");
|
|
6
|
+
//#region src/react/SearchResults.ts
|
|
7
|
+
const SearchResultsContext = (0, react.createContext)(null);
|
|
8
|
+
/**
|
|
9
|
+
* Declares which search produced the results rendered beneath it - the SPA
|
|
10
|
+
* twin of `data-query-id` on a server-rendered results container. Result
|
|
11
|
+
* components read it with {@link useSearchResults} (and
|
|
12
|
+
* {@link useResultImpression} reads it automatically) instead of threading
|
|
13
|
+
* `queryId` down as a prop.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <SearchResults queryId={response.query_id} query={q}>
|
|
17
|
+
* {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}
|
|
18
|
+
* </SearchResults>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function SearchResults({ queryId, query, children }) {
|
|
22
|
+
const value = (0, react.useMemo)(() => ({
|
|
23
|
+
queryId,
|
|
24
|
+
query
|
|
25
|
+
}), [queryId, query]);
|
|
26
|
+
return (0, react.createElement)(SearchResultsContext.Provider, { value }, children);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The current search context, or null outside a {@link SearchResults} (e.g. a
|
|
30
|
+
* card rendered on a home page that did not come from a search).
|
|
31
|
+
*/
|
|
32
|
+
function useSearchResults() {
|
|
33
|
+
return (0, react.useContext)(SearchResultsContext);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/react/context.ts
|
|
37
|
+
/**
|
|
38
|
+
* Holds the configured {@link Tracker} for a React tree. Provided by
|
|
39
|
+
* {@link TrackerProvider} and read with {@link useTracker}.
|
|
40
|
+
*/
|
|
41
|
+
const TrackerContext = (0, react.createContext)(null);
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/react/TrackerProvider.ts
|
|
44
|
+
/**
|
|
45
|
+
* Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.
|
|
46
|
+
* Starts the tracker on mount and stops it on unmount, so components never
|
|
47
|
+
* import a module singleton and the tracker is torn down cleanly.
|
|
48
|
+
*
|
|
49
|
+
* `options` (and `tracker`) are read ONCE on mount; later prop changes are
|
|
50
|
+
* ignored. Update identity with `useTracker().setUserId(...)` rather than by
|
|
51
|
+
* changing `options.userId`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <TrackerProvider
|
|
56
|
+
* options={{ application: "my-app", siteId: "...", endpointHost: "..." }}
|
|
57
|
+
* onInit={(t) => t.addEnricher(abTestEnricher)}
|
|
58
|
+
* >
|
|
59
|
+
* <App />
|
|
60
|
+
* </TrackerProvider>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function TrackerProvider(props) {
|
|
64
|
+
const { children, onInit, autoStart = true } = props;
|
|
65
|
+
const [tracker] = (0, react.useState)(() => "tracker" in props && props.tracker ? props.tracker : new require_tracker.Tracker(props.options));
|
|
66
|
+
const onInitRef = (0, react.useRef)(onInit);
|
|
67
|
+
const initialized = (0, react.useRef)(false);
|
|
68
|
+
(0, react.useEffect)(() => {
|
|
69
|
+
if (!autoStart) return;
|
|
70
|
+
if (!initialized.current) {
|
|
71
|
+
var _onInitRef$current;
|
|
72
|
+
initialized.current = true;
|
|
73
|
+
(_onInitRef$current = onInitRef.current) === null || _onInitRef$current === void 0 || _onInitRef$current.call(onInitRef, tracker);
|
|
74
|
+
}
|
|
75
|
+
tracker.start();
|
|
76
|
+
return () => {
|
|
77
|
+
tracker.stop();
|
|
78
|
+
};
|
|
79
|
+
}, [tracker, autoStart]);
|
|
80
|
+
return (0, react.createElement)(TrackerContext.Provider, { value: tracker }, children);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.
|
|
84
|
+
* Throws when called outside a provider.
|
|
85
|
+
*/
|
|
86
|
+
function useTracker() {
|
|
87
|
+
const tracker = (0, react.useContext)(TrackerContext);
|
|
88
|
+
if (!tracker) throw new Error("useTracker must be used within a <TrackerProvider>. Wrap your app in <TrackerProvider options={{ application: '...' }}> ... </TrackerProvider>.");
|
|
89
|
+
return tracker;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/react/useResultImpression.ts
|
|
93
|
+
/**
|
|
94
|
+
* Returns a callback ref that reports ONE canonical impression for the result
|
|
95
|
+
* the first time its element becomes visible, attributed to the surrounding
|
|
96
|
+
* {@link SearchResults} context.
|
|
97
|
+
*
|
|
98
|
+
* Fires once per query, not once per component instance: a new `queryId`
|
|
99
|
+
* re-arms the hook, so a card that stays mounted across consecutive searches
|
|
100
|
+
* is counted for each query. (A per-instance one-shot undercounts exactly the
|
|
101
|
+
* products that are returned most often, inflating their CTR.) The Tracker
|
|
102
|
+
* additionally dedupes impressions per `(queryId, objectId)` for its
|
|
103
|
+
* lifetime, so remounted instances - virtualized lists, route re-entry -
|
|
104
|
+
* cannot re-fire a pair this page already reported.
|
|
105
|
+
*
|
|
106
|
+
* Extra keys on `result` are persisted under the event's `event_attributes`
|
|
107
|
+
* (see {@link ResultRef}). Changing only extras does not re-arm the
|
|
108
|
+
* observer; the values current when the impression fires are sent.
|
|
109
|
+
*
|
|
110
|
+
* No-ops when there is no search context (nothing joinable to report), when
|
|
111
|
+
* `disabled`, or where `IntersectionObserver` is unavailable.
|
|
112
|
+
*
|
|
113
|
+
* StrictMode-safe by construction: cleanup is driven by the callback ref
|
|
114
|
+
* itself - React calls it with `null` on detach and re-invokes it when its
|
|
115
|
+
* identity changes - not by an effect whose simulated unmount would
|
|
116
|
+
* disconnect the observer without re-arming it.
|
|
117
|
+
*
|
|
118
|
+
* ```tsx
|
|
119
|
+
* const ref = useResultImpression({ objectId: r.sku, ordinal });
|
|
120
|
+
* return <article ref={ref}>...</article>;
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
function useResultImpression(result, options) {
|
|
124
|
+
var _options$disabled;
|
|
125
|
+
const tracker = useTracker();
|
|
126
|
+
const search = useSearchResults();
|
|
127
|
+
const firedForQuery = (0, react.useRef)(null);
|
|
128
|
+
const observerRef = (0, react.useRef)(null);
|
|
129
|
+
const latestResult = (0, react.useRef)(result);
|
|
130
|
+
latestResult.current = result;
|
|
131
|
+
const disabled = (_options$disabled = options === null || options === void 0 ? void 0 : options.disabled) !== null && _options$disabled !== void 0 ? _options$disabled : false;
|
|
132
|
+
const { objectId, ordinal } = result;
|
|
133
|
+
const queryId = search === null || search === void 0 ? void 0 : search.queryId;
|
|
134
|
+
const query = search === null || search === void 0 ? void 0 : search.query;
|
|
135
|
+
return (0, react.useCallback)((node) => {
|
|
136
|
+
var _observerRef$current;
|
|
137
|
+
(_observerRef$current = observerRef.current) === null || _observerRef$current === void 0 || _observerRef$current.disconnect();
|
|
138
|
+
observerRef.current = null;
|
|
139
|
+
if (!node || disabled || !queryId) return;
|
|
140
|
+
if (firedForQuery.current === queryId) return;
|
|
141
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
142
|
+
const observer = new IntersectionObserver((entries) => {
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (!entry.isIntersecting) continue;
|
|
145
|
+
if (firedForQuery.current === queryId) continue;
|
|
146
|
+
firedForQuery.current = queryId;
|
|
147
|
+
const item = {
|
|
148
|
+
...latestResult.current,
|
|
149
|
+
objectId,
|
|
150
|
+
ordinal
|
|
151
|
+
};
|
|
152
|
+
tracker.trackResultImpression({
|
|
153
|
+
items: [item],
|
|
154
|
+
queryId,
|
|
155
|
+
query
|
|
156
|
+
});
|
|
157
|
+
observer.disconnect();
|
|
158
|
+
observerRef.current = null;
|
|
159
|
+
}
|
|
160
|
+
}, { threshold: 0 });
|
|
161
|
+
observer.observe(node);
|
|
162
|
+
observerRef.current = observer;
|
|
163
|
+
}, [
|
|
164
|
+
tracker,
|
|
165
|
+
objectId,
|
|
166
|
+
ordinal,
|
|
167
|
+
queryId,
|
|
168
|
+
query,
|
|
169
|
+
disabled
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
exports.SearchResults = SearchResults;
|
|
174
|
+
exports.Tracker = require_tracker.Tracker;
|
|
175
|
+
exports.TrackerProvider = TrackerProvider;
|
|
176
|
+
exports.useResultImpression = useResultImpression;
|
|
177
|
+
exports.useSearchResults = useSearchResults;
|
|
178
|
+
exports.useTracker = useTracker;
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=react.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.cjs","names":["createContext","useMemo","createElement","useContext","createContext","useState","Tracker","useRef","createElement","useContext","useRef","useCallback"],"sources":["../src/react/SearchResults.ts","../src/react/context.ts","../src/react/TrackerProvider.ts","../src/react/useResultImpression.ts"],"sourcesContent":["import {\n createContext,\n createElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useMemo,\n} from \"react\";\n\n/** The search that produced the results rendered beneath a {@link SearchResults}. */\nexport type SearchResultsValue = {\n /** The server-issued `query_id` for the search. */\n queryId: string;\n /** The query text as the user entered it, if known. */\n query?: string;\n};\n\nconst SearchResultsContext = createContext<SearchResultsValue | null>(null);\n\n/** Props for {@link SearchResults}. */\nexport type SearchResultsProps = SearchResultsValue & {\n /** The results subtree this search context applies to. */\n children?: ReactNode;\n};\n\n/**\n * Declares which search produced the results rendered beneath it - the SPA\n * twin of `data-query-id` on a server-rendered results container. Result\n * components read it with {@link useSearchResults} (and\n * {@link useResultImpression} reads it automatically) instead of threading\n * `queryId` down as a prop.\n *\n * ```tsx\n * <SearchResults queryId={response.query_id} query={q}>\n * {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}\n * </SearchResults>\n * ```\n */\nexport function SearchResults({\n queryId,\n query,\n children,\n}: SearchResultsProps): ReactElement {\n const value = useMemo(() => ({ queryId, query }), [queryId, query]);\n return createElement(SearchResultsContext.Provider, { value }, children);\n}\n\n/**\n * The current search context, or null outside a {@link SearchResults} (e.g. a\n * card rendered on a home page that did not come from a search).\n */\nexport function useSearchResults(): SearchResultsValue | null {\n return useContext(SearchResultsContext);\n}\n","import { createContext } from \"react\";\nimport type { Tracker } from \"../tracker\";\n\n/**\n * Holds the configured {@link Tracker} for a React tree. Provided by\n * {@link TrackerProvider} and read with {@link useTracker}.\n */\nexport const TrackerContext = createContext<Tracker | null>(null);\n","import {\n createElement,\n type ReactElement,\n type ReactNode,\n useContext,\n useEffect,\n useRef,\n useState,\n} from \"react\";\nimport { Tracker, type TrackerOptions } from \"../tracker\";\nimport { TrackerContext } from \"./context\";\n\n/**\n * Props shared by both {@link TrackerProviderProps} variants (provide\n * `options` OR a pre-built `tracker`).\n */\nexport interface TrackerProviderBaseProps {\n /**\n * Runs exactly once, before the first `start()`. Do one-time wiring here:\n * `addEnricher()`, `addSink()`. It is guarded so React StrictMode's\n * double-mount cannot run the wiring twice.\n */\n onInit?: (tracker: Tracker) => void;\n /**\n * Call `start()` on mount and `stop()` on unmount. Defaults to `true`. Set\n * `false` when an external owner controls the tracker lifecycle and the\n * provider is only used to expose the instance via context.\n */\n autoStart?: boolean;\n /** The tree that can read the tracker via {@link useTracker}. */\n children?: ReactNode;\n}\n\n/**\n * Provide the tracker either by handing over an already-constructed instance\n * (`tracker`) or by passing `options` for the provider to construct. Exactly\n * one of the two is required.\n */\nexport type TrackerProviderProps =\n | (TrackerProviderBaseProps & {\n /** Options the provider constructs its own {@link Tracker} from. */\n options: TrackerOptions;\n /** Not allowed together with `options`. */\n tracker?: never;\n })\n | (TrackerProviderBaseProps & {\n /** A pre-built instance owned outside React, exposed via context. */\n tracker: Tracker;\n /** Not allowed together with `tracker`. */\n options?: never;\n });\n\n/**\n * Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.\n * Starts the tracker on mount and stops it on unmount, so components never\n * import a module singleton and the tracker is torn down cleanly.\n *\n * `options` (and `tracker`) are read ONCE on mount; later prop changes are\n * ignored. Update identity with `useTracker().setUserId(...)` rather than by\n * changing `options.userId`.\n *\n * @example\n * ```tsx\n * <TrackerProvider\n * options={{ application: \"my-app\", siteId: \"...\", endpointHost: \"...\" }}\n * onInit={(t) => t.addEnricher(abTestEnricher)}\n * >\n * <App />\n * </TrackerProvider>\n * ```\n */\nexport function TrackerProvider(props: TrackerProviderProps): ReactElement {\n const { children, onInit, autoStart = true } = props;\n\n // Own the instance once. The lazy initializer keeps its identity stable across\n // renders, so the context value never changes and consumers do not re-render.\n const [tracker] = useState<Tracker>(() =>\n \"tracker\" in props && props.tracker\n ? props.tracker\n : new Tracker((props as { options: TrackerOptions }).options),\n );\n\n // Capture the mount-time onInit; it runs once, so later identity changes are\n // irrelevant and it never needs to be an effect dependency.\n const onInitRef = useRef(onInit);\n const initialized = useRef(false);\n\n useEffect(() => {\n if (!autoStart) return;\n // StrictMode runs mount -> cleanup -> mount. onInit is ref-guarded to run\n // once (one-time wiring such as addSink/addEnricher would otherwise run\n // twice), while start()/stop() are restart-safe and idempotent - so the\n // tracker ends started with the wiring applied exactly once.\n if (!initialized.current) {\n initialized.current = true;\n onInitRef.current?.(tracker);\n }\n tracker.start();\n return () => {\n tracker.stop();\n };\n }, [tracker, autoStart]);\n\n return createElement(TrackerContext.Provider, { value: tracker }, children);\n}\n\n/**\n * Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.\n * Throws when called outside a provider.\n */\nexport function useTracker(): Tracker {\n const tracker = useContext(TrackerContext);\n if (!tracker) {\n throw new Error(\n \"useTracker must be used within a <TrackerProvider>. Wrap your app in \" +\n \"<TrackerProvider options={{ application: '...' }}> ... </TrackerProvider>.\",\n );\n }\n return tracker;\n}\n","import { useCallback, useRef } from \"react\";\nimport type { ResultRef } from \"../tracker\";\nimport { useSearchResults } from \"./SearchResults\";\nimport { useTracker } from \"./TrackerProvider\";\n\n/** Options for {@link useResultImpression}. */\nexport type UseResultImpressionOptions = {\n /** When true, no impression is observed or reported. */\n disabled?: boolean;\n};\n\n/**\n * Returns a callback ref that reports ONE canonical impression for the result\n * the first time its element becomes visible, attributed to the surrounding\n * {@link SearchResults} context.\n *\n * Fires once per query, not once per component instance: a new `queryId`\n * re-arms the hook, so a card that stays mounted across consecutive searches\n * is counted for each query. (A per-instance one-shot undercounts exactly the\n * products that are returned most often, inflating their CTR.) The Tracker\n * additionally dedupes impressions per `(queryId, objectId)` for its\n * lifetime, so remounted instances - virtualized lists, route re-entry -\n * cannot re-fire a pair this page already reported.\n *\n * Extra keys on `result` are persisted under the event's `event_attributes`\n * (see {@link ResultRef}). Changing only extras does not re-arm the\n * observer; the values current when the impression fires are sent.\n *\n * No-ops when there is no search context (nothing joinable to report), when\n * `disabled`, or where `IntersectionObserver` is unavailable.\n *\n * StrictMode-safe by construction: cleanup is driven by the callback ref\n * itself - React calls it with `null` on detach and re-invokes it when its\n * identity changes - not by an effect whose simulated unmount would\n * disconnect the observer without re-arming it.\n *\n * ```tsx\n * const ref = useResultImpression({ objectId: r.sku, ordinal });\n * return <article ref={ref}>...</article>;\n * ```\n */\nexport function useResultImpression(\n result: ResultRef,\n options?: UseResultImpressionOptions,\n): (node: Element | null) => void {\n const tracker = useTracker();\n const search = useSearchResults();\n const firedForQuery = useRef<string | null>(null);\n const observerRef = useRef<IntersectionObserver | null>(null);\n // Latest-value ref: extra keys on `result` ride into the fired impression\n // without being callback deps (an inline extras literal would otherwise\n // re-arm the observer every render). Identity-forming fields stay as deps.\n const latestResult = useRef(result);\n latestResult.current = result;\n\n const disabled = options?.disabled ?? false;\n const { objectId, ordinal } = result;\n const queryId = search?.queryId;\n const query = search?.query;\n\n return useCallback(\n (node: Element | null) => {\n // Called with the node on attach and null on detach; also re-invoked\n // when the callback identity changes (e.g. a new queryId arrives).\n observerRef.current?.disconnect();\n observerRef.current = null;\n\n if (!node || disabled || !queryId) return;\n if (firedForQuery.current === queryId) return;\n if (typeof IntersectionObserver === \"undefined\") return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n if (firedForQuery.current === queryId) continue;\n firedForQuery.current = queryId;\n\n const item: ResultRef = {\n ...latestResult.current,\n objectId,\n ordinal,\n };\n tracker.trackResultImpression({ items: [item], queryId, query });\n\n observer.disconnect();\n observerRef.current = null;\n }\n },\n { threshold: 0 },\n );\n observer.observe(node);\n observerRef.current = observer;\n },\n [tracker, objectId, ordinal, queryId, query, disabled],\n );\n}\n"],"mappings":"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"}
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
|
|
2
|
+
import { S as EventPosition, _ as Sink, a as TrackResultClickOptions, b as EventAttributes, c as TrackSearchOptions, d as TrackerOptions, i as ResultRef, l as Tracker, o as TrackResultEventOptions, s as TrackResultImpressionOptions, u as TrackerBaseOptions, v as Enricher, w as Logger, x as EventObject, y as Event } from "./ResultImpressionCollector.cjs";
|
|
3
|
+
import { ReactElement, ReactNode } from "react";
|
|
4
|
+
//#region src/react/SearchResults.d.ts
|
|
5
|
+
/** The search that produced the results rendered beneath a {@link SearchResults}. */
|
|
6
|
+
type SearchResultsValue = {
|
|
7
|
+
/** The server-issued `query_id` for the search. */
|
|
8
|
+
queryId: string;
|
|
9
|
+
/** The query text as the user entered it, if known. */
|
|
10
|
+
query?: string;
|
|
11
|
+
};
|
|
12
|
+
/** Props for {@link SearchResults}. */
|
|
13
|
+
type SearchResultsProps = SearchResultsValue & {
|
|
14
|
+
/** The results subtree this search context applies to. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Declares which search produced the results rendered beneath it - the SPA
|
|
19
|
+
* twin of `data-query-id` on a server-rendered results container. Result
|
|
20
|
+
* components read it with {@link useSearchResults} (and
|
|
21
|
+
* {@link useResultImpression} reads it automatically) instead of threading
|
|
22
|
+
* `queryId` down as a prop.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <SearchResults queryId={response.query_id} query={q}>
|
|
26
|
+
* {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}
|
|
27
|
+
* </SearchResults>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function SearchResults({ queryId, query, children }: SearchResultsProps): ReactElement;
|
|
31
|
+
/**
|
|
32
|
+
* The current search context, or null outside a {@link SearchResults} (e.g. a
|
|
33
|
+
* card rendered on a home page that did not come from a search).
|
|
34
|
+
*/
|
|
35
|
+
declare function useSearchResults(): SearchResultsValue | null;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/react/TrackerProvider.d.ts
|
|
38
|
+
/**
|
|
39
|
+
* Props shared by both {@link TrackerProviderProps} variants (provide
|
|
40
|
+
* `options` OR a pre-built `tracker`).
|
|
41
|
+
*/
|
|
42
|
+
interface TrackerProviderBaseProps {
|
|
43
|
+
/**
|
|
44
|
+
* Runs exactly once, before the first `start()`. Do one-time wiring here:
|
|
45
|
+
* `addEnricher()`, `addSink()`. It is guarded so React StrictMode's
|
|
46
|
+
* double-mount cannot run the wiring twice.
|
|
47
|
+
*/
|
|
48
|
+
onInit?: (tracker: Tracker) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Call `start()` on mount and `stop()` on unmount. Defaults to `true`. Set
|
|
51
|
+
* `false` when an external owner controls the tracker lifecycle and the
|
|
52
|
+
* provider is only used to expose the instance via context.
|
|
53
|
+
*/
|
|
54
|
+
autoStart?: boolean;
|
|
55
|
+
/** The tree that can read the tracker via {@link useTracker}. */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Provide the tracker either by handing over an already-constructed instance
|
|
60
|
+
* (`tracker`) or by passing `options` for the provider to construct. Exactly
|
|
61
|
+
* one of the two is required.
|
|
62
|
+
*/
|
|
63
|
+
type TrackerProviderProps = (TrackerProviderBaseProps & {
|
|
64
|
+
/** Options the provider constructs its own {@link Tracker} from. */
|
|
65
|
+
options: TrackerOptions;
|
|
66
|
+
/** Not allowed together with `options`. */
|
|
67
|
+
tracker?: never;
|
|
68
|
+
}) | (TrackerProviderBaseProps & {
|
|
69
|
+
/** A pre-built instance owned outside React, exposed via context. */
|
|
70
|
+
tracker: Tracker;
|
|
71
|
+
/** Not allowed together with `tracker`. */
|
|
72
|
+
options?: never;
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.
|
|
76
|
+
* Starts the tracker on mount and stops it on unmount, so components never
|
|
77
|
+
* import a module singleton and the tracker is torn down cleanly.
|
|
78
|
+
*
|
|
79
|
+
* `options` (and `tracker`) are read ONCE on mount; later prop changes are
|
|
80
|
+
* ignored. Update identity with `useTracker().setUserId(...)` rather than by
|
|
81
|
+
* changing `options.userId`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <TrackerProvider
|
|
86
|
+
* options={{ application: "my-app", siteId: "...", endpointHost: "..." }}
|
|
87
|
+
* onInit={(t) => t.addEnricher(abTestEnricher)}
|
|
88
|
+
* >
|
|
89
|
+
* <App />
|
|
90
|
+
* </TrackerProvider>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare function TrackerProvider(props: TrackerProviderProps): ReactElement;
|
|
94
|
+
/**
|
|
95
|
+
* Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.
|
|
96
|
+
* Throws when called outside a provider.
|
|
97
|
+
*/
|
|
98
|
+
declare function useTracker(): Tracker;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/react/useResultImpression.d.ts
|
|
101
|
+
/** Options for {@link useResultImpression}. */
|
|
102
|
+
type UseResultImpressionOptions = {
|
|
103
|
+
/** When true, no impression is observed or reported. */
|
|
104
|
+
disabled?: boolean;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Returns a callback ref that reports ONE canonical impression for the result
|
|
108
|
+
* the first time its element becomes visible, attributed to the surrounding
|
|
109
|
+
* {@link SearchResults} context.
|
|
110
|
+
*
|
|
111
|
+
* Fires once per query, not once per component instance: a new `queryId`
|
|
112
|
+
* re-arms the hook, so a card that stays mounted across consecutive searches
|
|
113
|
+
* is counted for each query. (A per-instance one-shot undercounts exactly the
|
|
114
|
+
* products that are returned most often, inflating their CTR.) The Tracker
|
|
115
|
+
* additionally dedupes impressions per `(queryId, objectId)` for its
|
|
116
|
+
* lifetime, so remounted instances - virtualized lists, route re-entry -
|
|
117
|
+
* cannot re-fire a pair this page already reported.
|
|
118
|
+
*
|
|
119
|
+
* Extra keys on `result` are persisted under the event's `event_attributes`
|
|
120
|
+
* (see {@link ResultRef}). Changing only extras does not re-arm the
|
|
121
|
+
* observer; the values current when the impression fires are sent.
|
|
122
|
+
*
|
|
123
|
+
* No-ops when there is no search context (nothing joinable to report), when
|
|
124
|
+
* `disabled`, or where `IntersectionObserver` is unavailable.
|
|
125
|
+
*
|
|
126
|
+
* StrictMode-safe by construction: cleanup is driven by the callback ref
|
|
127
|
+
* itself - React calls it with `null` on detach and re-invokes it when its
|
|
128
|
+
* identity changes - not by an effect whose simulated unmount would
|
|
129
|
+
* disconnect the observer without re-arming it.
|
|
130
|
+
*
|
|
131
|
+
* ```tsx
|
|
132
|
+
* const ref = useResultImpression({ objectId: r.sku, ordinal });
|
|
133
|
+
* return <article ref={ref}>...</article>;
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
declare function useResultImpression(result: ResultRef, options?: UseResultImpressionOptions): (node: Element | null) => void;
|
|
137
|
+
//#endregion
|
|
138
|
+
export { type Enricher, type Event, type EventAttributes, type EventObject, type EventPosition, type Logger, type ResultRef, SearchResults, type SearchResultsProps, type SearchResultsValue, type Sink, type TrackResultClickOptions, type TrackResultEventOptions, type TrackResultImpressionOptions, type TrackSearchOptions, Tracker, type TrackerBaseOptions, type TrackerOptions, TrackerProvider, type TrackerProviderBaseProps, type TrackerProviderProps, type UseResultImpressionOptions, useResultImpression, useSearchResults, useTracker };
|
|
139
|
+
//# sourceMappingURL=react.d.cts.map
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
|
|
2
|
+
import { S as EventPosition, _ as Sink, a as TrackResultClickOptions, b as EventAttributes, c as TrackSearchOptions, d as TrackerOptions, i as ResultRef, l as Tracker, o as TrackResultEventOptions, s as TrackResultImpressionOptions, u as TrackerBaseOptions, v as Enricher, w as Logger, x as EventObject, y as Event } from "./ResultImpressionCollector.mjs";
|
|
3
|
+
import { ReactElement, ReactNode } from "react";
|
|
4
|
+
//#region src/react/SearchResults.d.ts
|
|
5
|
+
/** The search that produced the results rendered beneath a {@link SearchResults}. */
|
|
6
|
+
type SearchResultsValue = {
|
|
7
|
+
/** The server-issued `query_id` for the search. */
|
|
8
|
+
queryId: string;
|
|
9
|
+
/** The query text as the user entered it, if known. */
|
|
10
|
+
query?: string;
|
|
11
|
+
};
|
|
12
|
+
/** Props for {@link SearchResults}. */
|
|
13
|
+
type SearchResultsProps = SearchResultsValue & {
|
|
14
|
+
/** The results subtree this search context applies to. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Declares which search produced the results rendered beneath it - the SPA
|
|
19
|
+
* twin of `data-query-id` on a server-rendered results container. Result
|
|
20
|
+
* components read it with {@link useSearchResults} (and
|
|
21
|
+
* {@link useResultImpression} reads it automatically) instead of threading
|
|
22
|
+
* `queryId` down as a prop.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <SearchResults queryId={response.query_id} query={q}>
|
|
26
|
+
* {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}
|
|
27
|
+
* </SearchResults>
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare function SearchResults({ queryId, query, children }: SearchResultsProps): ReactElement;
|
|
31
|
+
/**
|
|
32
|
+
* The current search context, or null outside a {@link SearchResults} (e.g. a
|
|
33
|
+
* card rendered on a home page that did not come from a search).
|
|
34
|
+
*/
|
|
35
|
+
declare function useSearchResults(): SearchResultsValue | null;
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/react/TrackerProvider.d.ts
|
|
38
|
+
/**
|
|
39
|
+
* Props shared by both {@link TrackerProviderProps} variants (provide
|
|
40
|
+
* `options` OR a pre-built `tracker`).
|
|
41
|
+
*/
|
|
42
|
+
interface TrackerProviderBaseProps {
|
|
43
|
+
/**
|
|
44
|
+
* Runs exactly once, before the first `start()`. Do one-time wiring here:
|
|
45
|
+
* `addEnricher()`, `addSink()`. It is guarded so React StrictMode's
|
|
46
|
+
* double-mount cannot run the wiring twice.
|
|
47
|
+
*/
|
|
48
|
+
onInit?: (tracker: Tracker) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Call `start()` on mount and `stop()` on unmount. Defaults to `true`. Set
|
|
51
|
+
* `false` when an external owner controls the tracker lifecycle and the
|
|
52
|
+
* provider is only used to expose the instance via context.
|
|
53
|
+
*/
|
|
54
|
+
autoStart?: boolean;
|
|
55
|
+
/** The tree that can read the tracker via {@link useTracker}. */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Provide the tracker either by handing over an already-constructed instance
|
|
60
|
+
* (`tracker`) or by passing `options` for the provider to construct. Exactly
|
|
61
|
+
* one of the two is required.
|
|
62
|
+
*/
|
|
63
|
+
type TrackerProviderProps = (TrackerProviderBaseProps & {
|
|
64
|
+
/** Options the provider constructs its own {@link Tracker} from. */
|
|
65
|
+
options: TrackerOptions;
|
|
66
|
+
/** Not allowed together with `options`. */
|
|
67
|
+
tracker?: never;
|
|
68
|
+
}) | (TrackerProviderBaseProps & {
|
|
69
|
+
/** A pre-built instance owned outside React, exposed via context. */
|
|
70
|
+
tracker: Tracker;
|
|
71
|
+
/** Not allowed together with `tracker`. */
|
|
72
|
+
options?: never;
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.
|
|
76
|
+
* Starts the tracker on mount and stops it on unmount, so components never
|
|
77
|
+
* import a module singleton and the tracker is torn down cleanly.
|
|
78
|
+
*
|
|
79
|
+
* `options` (and `tracker`) are read ONCE on mount; later prop changes are
|
|
80
|
+
* ignored. Update identity with `useTracker().setUserId(...)` rather than by
|
|
81
|
+
* changing `options.userId`.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```tsx
|
|
85
|
+
* <TrackerProvider
|
|
86
|
+
* options={{ application: "my-app", siteId: "...", endpointHost: "..." }}
|
|
87
|
+
* onInit={(t) => t.addEnricher(abTestEnricher)}
|
|
88
|
+
* >
|
|
89
|
+
* <App />
|
|
90
|
+
* </TrackerProvider>
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
declare function TrackerProvider(props: TrackerProviderProps): ReactElement;
|
|
94
|
+
/**
|
|
95
|
+
* Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.
|
|
96
|
+
* Throws when called outside a provider.
|
|
97
|
+
*/
|
|
98
|
+
declare function useTracker(): Tracker;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/react/useResultImpression.d.ts
|
|
101
|
+
/** Options for {@link useResultImpression}. */
|
|
102
|
+
type UseResultImpressionOptions = {
|
|
103
|
+
/** When true, no impression is observed or reported. */
|
|
104
|
+
disabled?: boolean;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Returns a callback ref that reports ONE canonical impression for the result
|
|
108
|
+
* the first time its element becomes visible, attributed to the surrounding
|
|
109
|
+
* {@link SearchResults} context.
|
|
110
|
+
*
|
|
111
|
+
* Fires once per query, not once per component instance: a new `queryId`
|
|
112
|
+
* re-arms the hook, so a card that stays mounted across consecutive searches
|
|
113
|
+
* is counted for each query. (A per-instance one-shot undercounts exactly the
|
|
114
|
+
* products that are returned most often, inflating their CTR.) The Tracker
|
|
115
|
+
* additionally dedupes impressions per `(queryId, objectId)` for its
|
|
116
|
+
* lifetime, so remounted instances - virtualized lists, route re-entry -
|
|
117
|
+
* cannot re-fire a pair this page already reported.
|
|
118
|
+
*
|
|
119
|
+
* Extra keys on `result` are persisted under the event's `event_attributes`
|
|
120
|
+
* (see {@link ResultRef}). Changing only extras does not re-arm the
|
|
121
|
+
* observer; the values current when the impression fires are sent.
|
|
122
|
+
*
|
|
123
|
+
* No-ops when there is no search context (nothing joinable to report), when
|
|
124
|
+
* `disabled`, or where `IntersectionObserver` is unavailable.
|
|
125
|
+
*
|
|
126
|
+
* StrictMode-safe by construction: cleanup is driven by the callback ref
|
|
127
|
+
* itself - React calls it with `null` on detach and re-invokes it when its
|
|
128
|
+
* identity changes - not by an effect whose simulated unmount would
|
|
129
|
+
* disconnect the observer without re-arming it.
|
|
130
|
+
*
|
|
131
|
+
* ```tsx
|
|
132
|
+
* const ref = useResultImpression({ objectId: r.sku, ordinal });
|
|
133
|
+
* return <article ref={ref}>...</article>;
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
declare function useResultImpression(result: ResultRef, options?: UseResultImpressionOptions): (node: Element | null) => void;
|
|
137
|
+
//#endregion
|
|
138
|
+
export { type Enricher, type Event, type EventAttributes, type EventObject, type EventPosition, type Logger, type ResultRef, SearchResults, type SearchResultsProps, type SearchResultsValue, type Sink, type TrackResultClickOptions, type TrackResultEventOptions, type TrackResultImpressionOptions, type TrackSearchOptions, Tracker, type TrackerBaseOptions, type TrackerOptions, TrackerProvider, type TrackerProviderBaseProps, type TrackerProviderProps, type UseResultImpressionOptions, useResultImpression, useSearchResults, useTracker };
|
|
139
|
+
//# sourceMappingURL=react.d.mts.map
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/*! @releval/tracker | Apache-2.0 | https://github.com/releval/tracker */
|
|
3
|
+
import { t as Tracker } from "./tracker.mjs";
|
|
4
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
//#region src/react/SearchResults.ts
|
|
6
|
+
const SearchResultsContext = createContext(null);
|
|
7
|
+
/**
|
|
8
|
+
* Declares which search produced the results rendered beneath it - the SPA
|
|
9
|
+
* twin of `data-query-id` on a server-rendered results container. Result
|
|
10
|
+
* components read it with {@link useSearchResults} (and
|
|
11
|
+
* {@link useResultImpression} reads it automatically) instead of threading
|
|
12
|
+
* `queryId` down as a prop.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SearchResults queryId={response.query_id} query={q}>
|
|
16
|
+
* {results.map((r, i) => <ResultCard key={r.sku} result={r} ordinal={i + 1} />)}
|
|
17
|
+
* </SearchResults>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function SearchResults({ queryId, query, children }) {
|
|
21
|
+
const value = useMemo(() => ({
|
|
22
|
+
queryId,
|
|
23
|
+
query
|
|
24
|
+
}), [queryId, query]);
|
|
25
|
+
return createElement(SearchResultsContext.Provider, { value }, children);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The current search context, or null outside a {@link SearchResults} (e.g. a
|
|
29
|
+
* card rendered on a home page that did not come from a search).
|
|
30
|
+
*/
|
|
31
|
+
function useSearchResults() {
|
|
32
|
+
return useContext(SearchResultsContext);
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/react/context.ts
|
|
36
|
+
/**
|
|
37
|
+
* Holds the configured {@link Tracker} for a React tree. Provided by
|
|
38
|
+
* {@link TrackerProvider} and read with {@link useTracker}.
|
|
39
|
+
*/
|
|
40
|
+
const TrackerContext = createContext(null);
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/react/TrackerProvider.ts
|
|
43
|
+
/**
|
|
44
|
+
* Owns a {@link Tracker}'s lifecycle and exposes it to the tree via context.
|
|
45
|
+
* Starts the tracker on mount and stops it on unmount, so components never
|
|
46
|
+
* import a module singleton and the tracker is torn down cleanly.
|
|
47
|
+
*
|
|
48
|
+
* `options` (and `tracker`) are read ONCE on mount; later prop changes are
|
|
49
|
+
* ignored. Update identity with `useTracker().setUserId(...)` rather than by
|
|
50
|
+
* changing `options.userId`.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* <TrackerProvider
|
|
55
|
+
* options={{ application: "my-app", siteId: "...", endpointHost: "..." }}
|
|
56
|
+
* onInit={(t) => t.addEnricher(abTestEnricher)}
|
|
57
|
+
* >
|
|
58
|
+
* <App />
|
|
59
|
+
* </TrackerProvider>
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function TrackerProvider(props) {
|
|
63
|
+
const { children, onInit, autoStart = true } = props;
|
|
64
|
+
const [tracker] = useState(() => "tracker" in props && props.tracker ? props.tracker : new Tracker(props.options));
|
|
65
|
+
const onInitRef = useRef(onInit);
|
|
66
|
+
const initialized = useRef(false);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!autoStart) return;
|
|
69
|
+
if (!initialized.current) {
|
|
70
|
+
var _onInitRef$current;
|
|
71
|
+
initialized.current = true;
|
|
72
|
+
(_onInitRef$current = onInitRef.current) === null || _onInitRef$current === void 0 || _onInitRef$current.call(onInitRef, tracker);
|
|
73
|
+
}
|
|
74
|
+
tracker.start();
|
|
75
|
+
return () => {
|
|
76
|
+
tracker.stop();
|
|
77
|
+
};
|
|
78
|
+
}, [tracker, autoStart]);
|
|
79
|
+
return createElement(TrackerContext.Provider, { value: tracker }, children);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the {@link Tracker} provided by the nearest {@link TrackerProvider}.
|
|
83
|
+
* Throws when called outside a provider.
|
|
84
|
+
*/
|
|
85
|
+
function useTracker() {
|
|
86
|
+
const tracker = useContext(TrackerContext);
|
|
87
|
+
if (!tracker) throw new Error("useTracker must be used within a <TrackerProvider>. Wrap your app in <TrackerProvider options={{ application: '...' }}> ... </TrackerProvider>.");
|
|
88
|
+
return tracker;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/react/useResultImpression.ts
|
|
92
|
+
/**
|
|
93
|
+
* Returns a callback ref that reports ONE canonical impression for the result
|
|
94
|
+
* the first time its element becomes visible, attributed to the surrounding
|
|
95
|
+
* {@link SearchResults} context.
|
|
96
|
+
*
|
|
97
|
+
* Fires once per query, not once per component instance: a new `queryId`
|
|
98
|
+
* re-arms the hook, so a card that stays mounted across consecutive searches
|
|
99
|
+
* is counted for each query. (A per-instance one-shot undercounts exactly the
|
|
100
|
+
* products that are returned most often, inflating their CTR.) The Tracker
|
|
101
|
+
* additionally dedupes impressions per `(queryId, objectId)` for its
|
|
102
|
+
* lifetime, so remounted instances - virtualized lists, route re-entry -
|
|
103
|
+
* cannot re-fire a pair this page already reported.
|
|
104
|
+
*
|
|
105
|
+
* Extra keys on `result` are persisted under the event's `event_attributes`
|
|
106
|
+
* (see {@link ResultRef}). Changing only extras does not re-arm the
|
|
107
|
+
* observer; the values current when the impression fires are sent.
|
|
108
|
+
*
|
|
109
|
+
* No-ops when there is no search context (nothing joinable to report), when
|
|
110
|
+
* `disabled`, or where `IntersectionObserver` is unavailable.
|
|
111
|
+
*
|
|
112
|
+
* StrictMode-safe by construction: cleanup is driven by the callback ref
|
|
113
|
+
* itself - React calls it with `null` on detach and re-invokes it when its
|
|
114
|
+
* identity changes - not by an effect whose simulated unmount would
|
|
115
|
+
* disconnect the observer without re-arming it.
|
|
116
|
+
*
|
|
117
|
+
* ```tsx
|
|
118
|
+
* const ref = useResultImpression({ objectId: r.sku, ordinal });
|
|
119
|
+
* return <article ref={ref}>...</article>;
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
function useResultImpression(result, options) {
|
|
123
|
+
var _options$disabled;
|
|
124
|
+
const tracker = useTracker();
|
|
125
|
+
const search = useSearchResults();
|
|
126
|
+
const firedForQuery = useRef(null);
|
|
127
|
+
const observerRef = useRef(null);
|
|
128
|
+
const latestResult = useRef(result);
|
|
129
|
+
latestResult.current = result;
|
|
130
|
+
const disabled = (_options$disabled = options === null || options === void 0 ? void 0 : options.disabled) !== null && _options$disabled !== void 0 ? _options$disabled : false;
|
|
131
|
+
const { objectId, ordinal } = result;
|
|
132
|
+
const queryId = search === null || search === void 0 ? void 0 : search.queryId;
|
|
133
|
+
const query = search === null || search === void 0 ? void 0 : search.query;
|
|
134
|
+
return useCallback((node) => {
|
|
135
|
+
var _observerRef$current;
|
|
136
|
+
(_observerRef$current = observerRef.current) === null || _observerRef$current === void 0 || _observerRef$current.disconnect();
|
|
137
|
+
observerRef.current = null;
|
|
138
|
+
if (!node || disabled || !queryId) return;
|
|
139
|
+
if (firedForQuery.current === queryId) return;
|
|
140
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
141
|
+
const observer = new IntersectionObserver((entries) => {
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
if (!entry.isIntersecting) continue;
|
|
144
|
+
if (firedForQuery.current === queryId) continue;
|
|
145
|
+
firedForQuery.current = queryId;
|
|
146
|
+
const item = {
|
|
147
|
+
...latestResult.current,
|
|
148
|
+
objectId,
|
|
149
|
+
ordinal
|
|
150
|
+
};
|
|
151
|
+
tracker.trackResultImpression({
|
|
152
|
+
items: [item],
|
|
153
|
+
queryId,
|
|
154
|
+
query
|
|
155
|
+
});
|
|
156
|
+
observer.disconnect();
|
|
157
|
+
observerRef.current = null;
|
|
158
|
+
}
|
|
159
|
+
}, { threshold: 0 });
|
|
160
|
+
observer.observe(node);
|
|
161
|
+
observerRef.current = observer;
|
|
162
|
+
}, [
|
|
163
|
+
tracker,
|
|
164
|
+
objectId,
|
|
165
|
+
ordinal,
|
|
166
|
+
queryId,
|
|
167
|
+
query,
|
|
168
|
+
disabled
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
export { SearchResults, Tracker, TrackerProvider, useResultImpression, useSearchResults, useTracker };
|
|
173
|
+
|
|
174
|
+
//# sourceMappingURL=react.mjs.map
|