@askdialog/dialog-sdk 2.4.0 → 2.6.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.
Files changed (53) hide show
  1. package/README.md +56 -22
  2. package/dist/Dialog.d.ts +14 -1
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +30 -1
  5. package/dist/EventsHandler.d.ts.map +1 -1
  6. package/dist/EventsHandler.js +2 -0
  7. package/dist/__tests__/dialogLoadAssistant.spec.d.ts +2 -0
  8. package/dist/__tests__/dialogLoadAssistant.spec.d.ts.map +1 -0
  9. package/dist/__tests__/dialogLoadAssistant.spec.js +50 -0
  10. package/dist/__tests__/dialogSearchAnalytics.spec.d.ts +2 -0
  11. package/dist/__tests__/dialogSearchAnalytics.spec.d.ts.map +1 -0
  12. package/dist/__tests__/dialogSearchAnalytics.spec.js +90 -0
  13. package/dist/__tests__/publicTypes.spec.js +3 -0
  14. package/dist/__tests__/searchController.spec.d.ts +2 -0
  15. package/dist/__tests__/searchController.spec.d.ts.map +1 -0
  16. package/dist/__tests__/searchController.spec.js +254 -0
  17. package/dist/__tests__/searchControllerAttribution.spec.d.ts +2 -0
  18. package/dist/__tests__/searchControllerAttribution.spec.d.ts.map +1 -0
  19. package/dist/__tests__/searchControllerAttribution.spec.js +176 -0
  20. package/dist/__tests__/searchImpressions.spec.d.ts +2 -0
  21. package/dist/__tests__/searchImpressions.spec.d.ts.map +1 -0
  22. package/dist/__tests__/searchImpressions.spec.js +218 -0
  23. package/dist/__tests__/searchService.spec.js +3 -1
  24. package/dist/config/config.development.js +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/searchController.d.ts +3 -0
  29. package/dist/searchController.d.ts.map +1 -0
  30. package/dist/searchController.js +164 -0
  31. package/dist/types/constructor.d.ts +8 -0
  32. package/dist/types/constructor.d.ts.map +1 -1
  33. package/dist/types/events.d.ts +5 -2
  34. package/dist/types/events.d.ts.map +1 -1
  35. package/dist/types/events.js +2 -0
  36. package/dist/types/index.d.ts +2 -0
  37. package/dist/types/index.d.ts.map +1 -1
  38. package/dist/types/index.js +2 -0
  39. package/dist/types/search.d.ts +3 -1
  40. package/dist/types/search.d.ts.map +1 -1
  41. package/dist/types/searchAnalytics.d.ts +30 -0
  42. package/dist/types/searchAnalytics.d.ts.map +1 -0
  43. package/dist/types/searchAnalytics.js +10 -0
  44. package/dist/types/searchController.d.ts +80 -0
  45. package/dist/types/searchController.d.ts.map +1 -0
  46. package/dist/types/searchController.js +8 -0
  47. package/dist/utils/searchControllerAnalytics.d.ts +14 -0
  48. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -0
  49. package/dist/utils/searchControllerAnalytics.js +54 -0
  50. package/dist/utils/searchImpressions.d.ts +21 -0
  51. package/dist/utils/searchImpressions.d.ts.map +1 -0
  52. package/dist/utils/searchImpressions.js +126 -0
  53. package/package.json +1 -1
@@ -0,0 +1,176 @@
1
+ /* eslint max-lines: ["error", 300] */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createSearchController } from "../searchController";
4
+ const tracker = vi.hoisted(() => ({
5
+ setContext: vi.fn(),
6
+ observe: vi.fn(),
7
+ forceImpression: vi.fn(),
8
+ flush: vi.fn(),
9
+ disconnect: vi.fn(),
10
+ }));
11
+ vi.mock("../utils/searchImpressions", () => ({
12
+ createSearchImpressionTracker: () => tracker,
13
+ }));
14
+ const response = (overrides = {}) => ({
15
+ queryId: "qid-1",
16
+ hits: [
17
+ {
18
+ id: "h1",
19
+ score: 1,
20
+ product: { id: "p1", url: "https://shop.example/p1" },
21
+ },
22
+ { id: "h2", score: 0.9, product: { id: "p2" } },
23
+ ],
24
+ nbHits: 30,
25
+ page: 0,
26
+ nbPages: 3,
27
+ hitsPerPage: 12,
28
+ processingTimeMs: 5,
29
+ query: "shoes",
30
+ ...overrides,
31
+ });
32
+ const search = vi.fn();
33
+ const trackViewSearchResults = vi.fn();
34
+ const trackSelectSearchResult = vi.fn();
35
+ const navigate = vi.fn();
36
+ const createController = () => createSearchController({
37
+ search,
38
+ analytics: {
39
+ surface: "search_page",
40
+ trackViewSearchResults,
41
+ trackSelectSearchResult,
42
+ },
43
+ navigate,
44
+ });
45
+ beforeEach(() => {
46
+ vi.useFakeTimers();
47
+ });
48
+ afterEach(() => {
49
+ vi.useRealTimers();
50
+ vi.clearAllMocks();
51
+ });
52
+ const settle = async () => {
53
+ await Promise.resolve();
54
+ await Promise.resolve();
55
+ };
56
+ describe("search controller attribution", () => {
57
+ it("omits queryId for a new query and resends it while the query is unchanged", async () => {
58
+ const controller = createController();
59
+ search.mockResolvedValue(response());
60
+ controller.submit("shoes");
61
+ await settle();
62
+ controller.setPage(1);
63
+ await settle();
64
+ expect(search.mock.calls[0][0].queryId).toBeUndefined();
65
+ expect(search.mock.calls[1][0]).toMatchObject({
66
+ query: "shoes",
67
+ page: 1,
68
+ queryId: "qid-1",
69
+ });
70
+ });
71
+ it("declares the rendered response's envelope to the impression tracker", async () => {
72
+ const controller = createController();
73
+ search.mockResolvedValue(response({ page: 1, query: "shoes" }));
74
+ controller.submit("shoes");
75
+ await settle();
76
+ expect(tracker.setContext).toHaveBeenCalledWith({
77
+ query_id: "qid-1",
78
+ surface: "search_page",
79
+ page: 2,
80
+ total_hits: 30,
81
+ query_length: 5,
82
+ });
83
+ });
84
+ it("keeps query_id stable across pagination and rotates it on a new query", async () => {
85
+ const controller = createController();
86
+ search
87
+ .mockResolvedValueOnce(response())
88
+ .mockResolvedValueOnce(response({ page: 1, queryId: "qid-1" }))
89
+ .mockResolvedValueOnce(response({ query: "boots", queryId: "qid-2" }));
90
+ controller.submit("shoes");
91
+ await settle();
92
+ controller.setPage(1);
93
+ await settle();
94
+ controller.submit("boots");
95
+ await settle();
96
+ const queryIds = tracker.setContext.mock.calls.map(([envelope]) => envelope.query_id);
97
+ expect(queryIds).toEqual(["qid-1", "qid-1", "qid-2"]);
98
+ });
99
+ it("emits the zero-items view event for a rendered no-results state", async () => {
100
+ const controller = createController();
101
+ search.mockResolvedValue(response({ hits: [], nbHits: 0, nbPages: 0, query: "nothing" }));
102
+ controller.submit("nothing");
103
+ await settle();
104
+ expect(trackViewSearchResults).toHaveBeenCalledWith({
105
+ query_id: "qid-1",
106
+ surface: "search_page",
107
+ page: 1,
108
+ total_hits: 0,
109
+ query_length: 7,
110
+ items: [],
111
+ });
112
+ });
113
+ it("observes result elements with absolute 1-based positions", async () => {
114
+ const controller = createController();
115
+ search.mockResolvedValue(response({ page: 2 }));
116
+ const element = {};
117
+ controller.submit("shoes");
118
+ await settle();
119
+ controller.observeResult(element, 1);
120
+ expect(tracker.observe).toHaveBeenCalledWith(element, {
121
+ product_id: "p2",
122
+ position: 2 * 12 + 2,
123
+ });
124
+ });
125
+ it("ignores observe and select before any response", () => {
126
+ const controller = createController();
127
+ controller.observeResult({}, 0);
128
+ controller.selectResult(0);
129
+ expect(tracker.observe).not.toHaveBeenCalled();
130
+ expect(trackSelectSearchResult).not.toHaveBeenCalled();
131
+ expect(navigate).not.toHaveBeenCalled();
132
+ });
133
+ it("records selection attribution before handing navigation to the adapter", async () => {
134
+ const controller = createController();
135
+ const landed = response();
136
+ search.mockResolvedValue(landed);
137
+ const order = [];
138
+ tracker.forceImpression.mockImplementation(() => order.push("impression"));
139
+ trackSelectSearchResult.mockImplementation(() => order.push("select"));
140
+ navigate.mockImplementation(() => order.push("navigate"));
141
+ controller.submit("shoes");
142
+ await settle();
143
+ controller.selectResult(0);
144
+ expect(tracker.forceImpression).toHaveBeenCalledWith({
145
+ product_id: "p1",
146
+ position: 1,
147
+ });
148
+ expect(trackSelectSearchResult).toHaveBeenCalledWith({
149
+ query_id: "qid-1",
150
+ surface: "search_page",
151
+ page: 1,
152
+ total_hits: 30,
153
+ query_length: 5,
154
+ items: [{ product_id: "p1", position: 1 }],
155
+ });
156
+ expect(navigate).toHaveBeenCalledWith("https://shop.example/p1", landed.hits[0]);
157
+ expect(order).toEqual(["impression", "select", "navigate"]);
158
+ });
159
+ it("still records attribution when the hit has no URL to navigate to", async () => {
160
+ const controller = createController();
161
+ search.mockResolvedValue(response());
162
+ controller.submit("shoes");
163
+ await settle();
164
+ controller.selectResult(1);
165
+ expect(trackSelectSearchResult).toHaveBeenCalledWith(expect.objectContaining({ items: [{ product_id: "p2", position: 2 }] }));
166
+ expect(navigate).not.toHaveBeenCalled();
167
+ });
168
+ it("disconnects the impression tracker on dispose", async () => {
169
+ const controller = createController();
170
+ search.mockResolvedValue(response());
171
+ controller.submit("shoes");
172
+ await settle();
173
+ controller.dispose();
174
+ expect(tracker.disconnect).toHaveBeenCalledTimes(1);
175
+ });
176
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=searchImpressions.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchImpressions.spec.d.ts","sourceRoot":"","sources":["../../src/__tests__/searchImpressions.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,218 @@
1
+ /* eslint max-lines: ["error", 300] */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createSearchImpressionTracker } from "../utils/searchImpressions";
4
+ const envelope = {
5
+ query_id: "query-1",
6
+ surface: "search_page",
7
+ page: 1,
8
+ total_hits: 42,
9
+ query_length: 6,
10
+ };
11
+ let observerCallback;
12
+ const observe = vi.fn();
13
+ const unobserve = vi.fn();
14
+ const disconnect = vi.fn();
15
+ const windowListeners = new Map();
16
+ const element = () => ({});
17
+ const intersect = (target, ratio) => {
18
+ observerCallback([{ target, intersectionRatio: ratio }]);
19
+ };
20
+ beforeEach(() => {
21
+ vi.useFakeTimers();
22
+ vi.stubGlobal("IntersectionObserver", class {
23
+ constructor(callback) {
24
+ observerCallback = callback;
25
+ }
26
+ observe = observe;
27
+ unobserve = unobserve;
28
+ disconnect = disconnect;
29
+ });
30
+ vi.stubGlobal("window", {
31
+ addEventListener: (type, listener) => {
32
+ windowListeners.set(type, listener);
33
+ },
34
+ removeEventListener: (type) => {
35
+ windowListeners.delete(type);
36
+ },
37
+ });
38
+ });
39
+ afterEach(() => {
40
+ vi.useRealTimers();
41
+ vi.unstubAllGlobals();
42
+ vi.clearAllMocks();
43
+ windowListeners.clear();
44
+ });
45
+ describe("createSearchImpressionTracker", () => {
46
+ it("emits a batched impression after dwell then scroll inactivity", () => {
47
+ const emit = vi.fn();
48
+ const tracker = createSearchImpressionTracker({ emit });
49
+ tracker.setContext(envelope);
50
+ const card = element();
51
+ tracker.observe(card, { product_id: "product-1", position: 1 });
52
+ intersect(card, 0.6);
53
+ vi.advanceTimersByTime(500);
54
+ expect(emit).not.toHaveBeenCalled();
55
+ vi.advanceTimersByTime(1500);
56
+ expect(emit).toHaveBeenCalledWith({
57
+ ...envelope,
58
+ items: [{ product_id: "product-1", position: 1 }],
59
+ });
60
+ });
61
+ it("does not count an item that leaves the viewport before the dwell time", () => {
62
+ const emit = vi.fn();
63
+ const tracker = createSearchImpressionTracker({ emit });
64
+ tracker.setContext(envelope);
65
+ const card = element();
66
+ tracker.observe(card, { product_id: "product-1", position: 1 });
67
+ intersect(card, 0.6);
68
+ vi.advanceTimersByTime(300);
69
+ intersect(card, 0.2);
70
+ vi.advanceTimersByTime(5000);
71
+ expect(emit).not.toHaveBeenCalled();
72
+ });
73
+ it("never re-counts a (query_id, product_id) pair", () => {
74
+ const emit = vi.fn();
75
+ const tracker = createSearchImpressionTracker({ emit });
76
+ tracker.setContext(envelope);
77
+ const card = element();
78
+ tracker.observe(card, { product_id: "product-1", position: 1 });
79
+ intersect(card, 0.6);
80
+ vi.advanceTimersByTime(2000);
81
+ intersect(card, 0.1);
82
+ intersect(card, 0.6);
83
+ vi.advanceTimersByTime(5000);
84
+ expect(emit).toHaveBeenCalledTimes(1);
85
+ });
86
+ it("flushes immediately when the batch reaches the item cap", () => {
87
+ const emit = vi.fn();
88
+ const tracker = createSearchImpressionTracker({
89
+ emit,
90
+ maxItemsPerEvent: 2,
91
+ });
92
+ tracker.setContext(envelope);
93
+ const first = element();
94
+ const second = element();
95
+ tracker.observe(first, { product_id: "product-1", position: 1 });
96
+ tracker.observe(second, { product_id: "product-2", position: 2 });
97
+ intersect(first, 0.6);
98
+ intersect(second, 0.6);
99
+ vi.advanceTimersByTime(500);
100
+ expect(emit).toHaveBeenCalledTimes(1);
101
+ expect(emit.mock.calls[0][0].items).toHaveLength(2);
102
+ });
103
+ it("flushes pending impressions under their own envelope when the context changes", () => {
104
+ const emit = vi.fn();
105
+ const tracker = createSearchImpressionTracker({ emit });
106
+ tracker.setContext(envelope);
107
+ const card = element();
108
+ tracker.observe(card, { product_id: "product-1", position: 1 });
109
+ intersect(card, 0.6);
110
+ vi.advanceTimersByTime(500);
111
+ tracker.setContext({ ...envelope, query_id: "query-2", page: 1 });
112
+ expect(emit).toHaveBeenCalledWith(expect.objectContaining({ query_id: "query-1" }));
113
+ });
114
+ it("forces the clicked item's impression and flushes immediately", () => {
115
+ const emit = vi.fn();
116
+ const tracker = createSearchImpressionTracker({ emit });
117
+ tracker.setContext(envelope);
118
+ tracker.forceImpression({ product_id: "product-1", position: 1 });
119
+ expect(emit).toHaveBeenCalledWith({
120
+ ...envelope,
121
+ items: [{ product_id: "product-1", position: 1 }],
122
+ });
123
+ });
124
+ it("does not duplicate a forced impression already counted", () => {
125
+ const emit = vi.fn();
126
+ const tracker = createSearchImpressionTracker({ emit });
127
+ tracker.setContext(envelope);
128
+ const card = element();
129
+ tracker.observe(card, { product_id: "product-1", position: 1 });
130
+ intersect(card, 0.6);
131
+ vi.advanceTimersByTime(2000);
132
+ tracker.forceImpression({ product_id: "product-1", position: 1 });
133
+ expect(emit).toHaveBeenCalledTimes(1);
134
+ });
135
+ it("keeps deduplication across pagination of the same query", () => {
136
+ const emit = vi.fn();
137
+ const tracker = createSearchImpressionTracker({ emit });
138
+ tracker.setContext(envelope);
139
+ const card = element();
140
+ tracker.observe(card, { product_id: "product-1", position: 1 });
141
+ intersect(card, 0.6);
142
+ vi.advanceTimersByTime(2000);
143
+ tracker.setContext({ ...envelope, page: 2 });
144
+ const cardOnPage2 = element();
145
+ tracker.observe(cardOnPage2, { product_id: "product-1", position: 1 });
146
+ intersect(cardOnPage2, 0.6);
147
+ vi.advanceTimersByTime(2000);
148
+ expect(emit).toHaveBeenCalledTimes(1);
149
+ });
150
+ it("drops a previous query's dedup keys when the query changes", () => {
151
+ const emit = vi.fn();
152
+ const tracker = createSearchImpressionTracker({ emit });
153
+ tracker.setContext(envelope);
154
+ const card = element();
155
+ tracker.observe(card, { product_id: "product-1", position: 1 });
156
+ intersect(card, 0.6);
157
+ vi.advanceTimersByTime(2000);
158
+ tracker.setContext({ ...envelope, query_id: "query-2" });
159
+ // Same query_id served again (e.g. a cached response): a fresh render is
160
+ // a new exposure, like the bfcache restore.
161
+ tracker.setContext(envelope);
162
+ const cardAgain = element();
163
+ tracker.observe(cardAgain, { product_id: "product-1", position: 1 });
164
+ intersect(cardAgain, 0.6);
165
+ vi.advanceTimersByTime(2000);
166
+ expect(emit).toHaveBeenCalledTimes(2);
167
+ });
168
+ it("flushes qualified impressions on pagehide", () => {
169
+ const emit = vi.fn();
170
+ const tracker = createSearchImpressionTracker({ emit });
171
+ tracker.setContext(envelope);
172
+ const card = element();
173
+ tracker.observe(card, { product_id: "product-1", position: 1 });
174
+ intersect(card, 0.6);
175
+ vi.advanceTimersByTime(500);
176
+ windowListeners.get("pagehide")?.({});
177
+ expect(emit).toHaveBeenCalledTimes(1);
178
+ });
179
+ it("re-qualifies items after a back/forward-cache restore", () => {
180
+ const emit = vi.fn();
181
+ const tracker = createSearchImpressionTracker({ emit });
182
+ tracker.setContext(envelope);
183
+ const card = element();
184
+ tracker.observe(card, { product_id: "product-1", position: 1 });
185
+ intersect(card, 0.6);
186
+ vi.advanceTimersByTime(2000);
187
+ expect(emit).toHaveBeenCalledTimes(1);
188
+ windowListeners.get("pageshow")?.({ persisted: true });
189
+ intersect(card, 0.6);
190
+ vi.advanceTimersByTime(2000);
191
+ expect(emit).toHaveBeenCalledTimes(2);
192
+ });
193
+ it("requires a full fresh dwell after a back/forward-cache restore", () => {
194
+ const emit = vi.fn();
195
+ const tracker = createSearchImpressionTracker({ emit });
196
+ tracker.setContext(envelope);
197
+ const card = element();
198
+ tracker.observe(card, { product_id: "product-1", position: 1 });
199
+ intersect(card, 0.6);
200
+ vi.advanceTimersByTime(300);
201
+ windowListeners.get("pageshow")?.({ persisted: true });
202
+ intersect(card, 0.6);
203
+ // A stale pre-restore timer (200ms left) would record at 200ms and flush
204
+ // at 1700ms; a fresh dwell records at 500ms and flushes at 2000ms.
205
+ vi.advanceTimersByTime(1700);
206
+ expect(emit).not.toHaveBeenCalled();
207
+ vi.advanceTimersByTime(300);
208
+ expect(emit).toHaveBeenCalledTimes(1);
209
+ });
210
+ it("emits nothing when no impression qualified", () => {
211
+ const emit = vi.fn();
212
+ const tracker = createSearchImpressionTracker({ emit });
213
+ tracker.setContext(envelope);
214
+ tracker.flush();
215
+ windowListeners.get("pagehide")?.({});
216
+ expect(emit).not.toHaveBeenCalled();
217
+ });
218
+ });
@@ -31,6 +31,7 @@ describe("searchProducts", () => {
31
31
  query: "running shoes",
32
32
  page: 2,
33
33
  hitsPerPage: 50,
34
+ queryId: "0198c3f2-0000-7000-8000-000000000000",
34
35
  });
35
36
  expect(fetchMock).toHaveBeenCalledTimes(1);
36
37
  const [url, init] = fetchMock.mock.calls[0];
@@ -44,9 +45,10 @@ describe("searchProducts", () => {
44
45
  query: "running shoes",
45
46
  page: 2,
46
47
  hitsPerPage: 50,
48
+ queryId: "0198c3f2-0000-7000-8000-000000000000",
47
49
  });
48
50
  });
49
- it("omits pagination fields the caller did not provide", async () => {
51
+ it("omits pagination fields and queryId the caller did not provide", async () => {
50
52
  fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
51
53
  await searchProducts(API_KEY, { query: "running shoes" });
52
54
  const [, init] = fetchMock.mock.calls[0];
@@ -1,5 +1,5 @@
1
1
  export const config = {
2
- baseApiUrl: "https://abkjr5ukvi.execute-api.eu-west-1.amazonaws.com",
2
+ baseApiUrl: "https://hr5buzenb1.execute-api.eu-west-1.amazonaws.com",
3
3
  // Nest monolith (staging) — serves POST /public/search.
4
4
  monolithApiUrl: "https://fvcphlqyle.execute-api.eu-west-1.amazonaws.com",
5
5
  assistantUrl: "https://d2bycosa71tnxv.cloudfront.net/assets/index.js",
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./Dialog";
2
2
  export * from "./DialogSearchError";
3
3
  export * from "./EventsHandler";
4
+ export * from "./searchController";
4
5
  export * from "./types";
6
+ export * from "./utils/searchImpressions";
5
7
  export * from "./windowAudit";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./Dialog";
2
2
  export * from "./DialogSearchError";
3
3
  export * from "./EventsHandler";
4
+ export * from "./searchController";
4
5
  export * from "./types";
6
+ export * from "./utils/searchImpressions";
5
7
  export * from "./windowAudit";
@@ -0,0 +1,3 @@
1
+ import { SearchController, SearchControllerOptions } from "./types/searchController";
2
+ export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, }: SearchControllerOptions): SearchController;
3
+ //# sourceMappingURL=searchController.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGxB,MAAM,0BAA0B,CAAC;AAgBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,UAAgC,EAChC,WAAmC,GACpC,EAAE,uBAAuB,GAAG,gBAAgB,CA2K5C"}
@@ -0,0 +1,164 @@
1
+ import { SearchStatus, } from "./types/searchController";
2
+ import { createControllerAnalytics } from "./utils/searchControllerAnalytics";
3
+ const DEFAULT_DEBOUNCE_MS = 250;
4
+ const DEFAULT_HITS_PER_PAGE = 12;
5
+ // `SearchRequest` rejects queries under two visible characters (code points).
6
+ const MIN_QUERY_CODE_POINTS = 2;
7
+ const INITIAL_STATE = {
8
+ status: SearchStatus.IDLE,
9
+ query: "",
10
+ page: 0,
11
+ response: undefined,
12
+ error: undefined,
13
+ };
14
+ export function createSearchController({ search, analytics, navigate, debounceMs = DEFAULT_DEBOUNCE_MS, hitsPerPage = DEFAULT_HITS_PER_PAGE, }) {
15
+ let state = INITIAL_STATE;
16
+ const listeners = new Set();
17
+ const controllerAnalytics = createControllerAnalytics(analytics);
18
+ let debounceTimer;
19
+ // Query typed but not yet run: `state.query` only holds committed queries, so
20
+ // without this, `setPage()` would cancel the debounce and paginate stale results.
21
+ let pendingQuery;
22
+ let abortController;
23
+ // Monotonic token: a response only lands if it belongs to the latest request.
24
+ let requestId = 0;
25
+ let disposed = false;
26
+ const setState = (patch) => {
27
+ state = { ...state, ...patch };
28
+ for (const listener of listeners) {
29
+ listener(state);
30
+ }
31
+ };
32
+ const cancelInFlight = () => {
33
+ clearTimeout(debounceTimer);
34
+ abortController?.abort();
35
+ abortController = undefined;
36
+ };
37
+ const buildRequest = (query, page) => {
38
+ const request = { query, page, hitsPerPage };
39
+ // Query unchanged since the last response: resend its queryId; a new
40
+ // query gets a fresh engine-generated one.
41
+ const previous = state.response;
42
+ if (previous?.query === query) {
43
+ request.queryId = previous.queryId;
44
+ }
45
+ return request;
46
+ };
47
+ const run = async (query, page) => {
48
+ cancelInFlight();
49
+ abortController = new AbortController();
50
+ const id = ++requestId;
51
+ const request = buildRequest(query, page);
52
+ setState({ status: SearchStatus.LOADING, query, page });
53
+ try {
54
+ const response = await search(request, {
55
+ signal: abortController.signal,
56
+ });
57
+ if (id !== requestId) {
58
+ return; // A newer request landed first: this response is stale.
59
+ }
60
+ controllerAnalytics.onResponse(response);
61
+ setState({
62
+ status: response.nbHits === 0 ? SearchStatus.EMPTY : SearchStatus.SUCCESS,
63
+ response,
64
+ error: undefined,
65
+ });
66
+ }
67
+ catch (error) {
68
+ if (error instanceof DOMException && error.name === "AbortError") {
69
+ return; // Superseded by a newer request — never surfaced.
70
+ }
71
+ if (id !== requestId) {
72
+ return;
73
+ }
74
+ setState({ status: SearchStatus.ERROR, error, response: undefined });
75
+ }
76
+ };
77
+ // Shared by `setQuery`/`submit`: undefined = reset to idle (query too short).
78
+ const prepare = (rawQuery) => {
79
+ const query = rawQuery.trim();
80
+ cancelInFlight();
81
+ pendingQuery = undefined;
82
+ if ([...query].length < MIN_QUERY_CODE_POINTS) {
83
+ requestId += 1; // Invalidate anything in flight.
84
+ setState(INITIAL_STATE);
85
+ return undefined;
86
+ }
87
+ return query;
88
+ };
89
+ return {
90
+ setQuery(rawQuery) {
91
+ const query = disposed ? undefined : prepare(rawQuery);
92
+ if (query === undefined) {
93
+ return;
94
+ }
95
+ pendingQuery = query;
96
+ debounceTimer = setTimeout(() => {
97
+ pendingQuery = undefined;
98
+ void run(query, 0);
99
+ }, debounceMs);
100
+ },
101
+ submit(rawQuery) {
102
+ const query = disposed ? undefined : prepare(rawQuery);
103
+ if (query === undefined) {
104
+ return;
105
+ }
106
+ void run(query, 0);
107
+ },
108
+ setPage(page) {
109
+ if (disposed) {
110
+ return;
111
+ }
112
+ if (pendingQuery !== undefined) {
113
+ // A query change is pending: the click targeted stale results, so the
114
+ // new query supersedes pagination — flush it now instead of debouncing.
115
+ const query = pendingQuery;
116
+ pendingQuery = undefined;
117
+ void run(query, 0);
118
+ return;
119
+ }
120
+ if (state.query === "") {
121
+ return; // Pagination requires a committed non-empty query.
122
+ }
123
+ void run(state.query, page);
124
+ },
125
+ retry() {
126
+ if (disposed || state.status !== SearchStatus.ERROR) {
127
+ return;
128
+ }
129
+ void run(state.query, state.page);
130
+ },
131
+ observeResult(element, index) {
132
+ if (state.response === undefined) {
133
+ return;
134
+ }
135
+ controllerAnalytics.observeResult(element, state.response, index);
136
+ },
137
+ selectResult(index) {
138
+ const response = state.response;
139
+ if (response === undefined) {
140
+ return;
141
+ }
142
+ controllerAnalytics.select(response, index);
143
+ const url = response.hits[index].product.url;
144
+ if (navigate !== undefined && url !== undefined) {
145
+ navigate(url, response.hits[index]);
146
+ }
147
+ },
148
+ subscribe(listener) {
149
+ listeners.add(listener);
150
+ return () => {
151
+ listeners.delete(listener);
152
+ };
153
+ },
154
+ getState: () => state,
155
+ dispose() {
156
+ disposed = true;
157
+ cancelInFlight();
158
+ requestId += 1;
159
+ pendingQuery = undefined;
160
+ listeners.clear();
161
+ controllerAnalytics.dispose();
162
+ },
163
+ };
164
+ }
@@ -22,5 +22,13 @@ export interface DialogConstructor {
22
22
  callbacks?: DialogCallbacks;
23
23
  theme?: Partial<Theme>;
24
24
  userId?: string;
25
+ /**
26
+ * Adds `data-ot-ignore` on the injected assistant script so OneTrust
27
+ * auto-blocking does not neutralize it for visitors who declined cookies.
28
+ * The assistant gates analytics on consent internally, so loading it is
29
+ * safe — but exempting a script from the CMP is the merchant's compliance
30
+ * call, hence opt-in. No effect outside OneTrust auto-blocking setups.
31
+ */
32
+ ignoreOneTrustAutoBlock?: boolean;
25
33
  }
26
34
  //# sourceMappingURL=constructor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"constructor.d.ts","sourceRoot":"","sources":["../../src/types/constructor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,CACV,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"constructor.d.ts","sourceRoot":"","sources":["../../src/types/constructor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,UAAU,EAAE,CACV,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC"}
@@ -1,3 +1,4 @@
1
+ import { SelectSearchResultEventPayload, ViewSearchResultsEventPayload } from "./searchAnalytics";
1
2
  export declare const DIALOG_CUSTOM_EVENT = "enableDialogAssistantEvent";
2
3
  export declare enum DialogEvents {
3
4
  OPEN_ASSISTANT = "open_assistant",
@@ -5,7 +6,9 @@ export declare enum DialogEvents {
5
6
  SEND_MESSAGE = "PRODUCT_QUESTION",
6
7
  SEND_GENERIC_QUESTION = "GENERIC_QUESTION",
7
8
  TRACK_ADD_TO_CART = "TRACK_ADD_TO_CART",
8
- TRACK_SUBMIT_CHECKOUT = "TRACK_SUBMIT_CHECKOUT"
9
+ TRACK_SUBMIT_CHECKOUT = "TRACK_SUBMIT_CHECKOUT",
10
+ TRACK_VIEW_SEARCH_RESULTS = "TRACK_VIEW_SEARCH_RESULTS",
11
+ TRACK_SELECT_SEARCH_RESULT = "TRACK_SELECT_SEARCH_RESULT"
9
12
  }
10
13
  export interface GenericQuestionPayload {
11
14
  question: string;
@@ -87,7 +90,7 @@ export interface LegacyCheckoutParams {
87
90
  currency?: string;
88
91
  variantId?: string;
89
92
  }
90
- export type DialogEventPayload = ProductQuestionPayload | GenericQuestionPayload | DiagnosticPayload | OpenAssistantPayload | TrackEventPayload | SubmitCheckoutEventPayload;
93
+ export type DialogEventPayload = ProductQuestionPayload | GenericQuestionPayload | DiagnosticPayload | OpenAssistantPayload | TrackEventPayload | SubmitCheckoutEventPayload | ViewSearchResultsEventPayload | SelectSearchResultEventPayload;
91
94
  export interface DialogEvent<T = DialogEventPayload> {
92
95
  type: DialogEvents;
93
96
  payload: T;