@askdialog/dialog-sdk 2.9.0 → 2.10.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/README.md +43 -10
- package/dist/Dialog.d.ts +21 -6
- package/dist/Dialog.d.ts.map +1 -1
- package/dist/Dialog.js +72 -12
- package/dist/__tests__/dialogCurrentProduct.spec.d.ts +2 -0
- package/dist/__tests__/dialogCurrentProduct.spec.d.ts.map +1 -0
- package/dist/__tests__/dialogCurrentProduct.spec.js +134 -0
- package/dist/__tests__/dialogSearchAnalytics.spec.js +1 -0
- package/dist/__tests__/publicTypes.spec.js +2 -1
- package/dist/__tests__/searchController.spec.js +46 -34
- package/dist/__tests__/searchControllerAttribution.spec.js +30 -26
- package/dist/__tests__/searchImpressions.spec.js +3 -20
- package/dist/__tests__/searchService.spec.js +66 -47
- package/dist/config/config.development.js +1 -1
- package/dist/config/config.production.js +1 -1
- package/dist/config/index.js +1 -1
- package/dist/searchController.d.ts +1 -1
- package/dist/searchController.d.ts.map +1 -1
- package/dist/searchController.js +18 -21
- package/dist/services/search.d.ts +3 -2
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +4 -2
- package/dist/types/constructor.d.ts +15 -0
- package/dist/types/constructor.d.ts.map +1 -1
- package/dist/types/search.d.ts +24 -22
- package/dist/types/search.d.ts.map +1 -1
- package/dist/types/search.js +7 -4
- package/dist/types/searchAnalytics.d.ts +2 -0
- package/dist/types/searchAnalytics.d.ts.map +1 -1
- package/dist/types/searchController.d.ts +7 -5
- package/dist/types/searchController.d.ts.map +1 -1
- package/dist/utils/localization.d.ts +1 -0
- package/dist/utils/localization.d.ts.map +1 -1
- package/dist/utils/localization.js +11 -0
- package/dist/utils/searchControllerAnalytics.d.ts +4 -4
- package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
- package/dist/utils/searchControllerAnalytics.js +14 -16
- package/dist/utils/searchImpressions.d.ts.map +1 -1
- package/dist/utils/searchImpressions.js +1 -6
- package/package.json +1 -1
|
@@ -12,22 +12,23 @@ vi.mock("../utils/searchImpressions", () => ({
|
|
|
12
12
|
createSearchImpressionTracker: () => tracker,
|
|
13
13
|
}));
|
|
14
14
|
const response = (overrides = {}) => ({
|
|
15
|
-
|
|
16
|
-
hits: [
|
|
15
|
+
results: [
|
|
17
16
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
index: "products_fr",
|
|
18
|
+
hits: [
|
|
19
|
+
{ objectID: "p1", url: "https://shop.example/p1" },
|
|
20
|
+
{ objectID: "p2" },
|
|
21
|
+
],
|
|
22
|
+
nbHits: 30,
|
|
23
|
+
page: 0,
|
|
24
|
+
nbPages: 3,
|
|
25
|
+
hitsPerPage: 12,
|
|
26
|
+
processingTimeMS: 5,
|
|
27
|
+
query: "shoes",
|
|
28
|
+
queryID: "qid-1",
|
|
29
|
+
...overrides,
|
|
21
30
|
},
|
|
22
|
-
{ id: "h2", score: 0.9, product: { id: "p2" } },
|
|
23
31
|
],
|
|
24
|
-
nbHits: 30,
|
|
25
|
-
page: 0,
|
|
26
|
-
nbPages: 3,
|
|
27
|
-
hitsPerPage: 12,
|
|
28
|
-
processingTimeMs: 5,
|
|
29
|
-
query: "shoes",
|
|
30
|
-
...overrides,
|
|
31
32
|
});
|
|
32
33
|
const search = vi.fn();
|
|
33
34
|
const trackViewSearchResults = vi.fn();
|
|
@@ -41,6 +42,7 @@ const createController = () => createSearchController({
|
|
|
41
42
|
trackSelectSearchResult,
|
|
42
43
|
},
|
|
43
44
|
navigate,
|
|
45
|
+
locale: "fr",
|
|
44
46
|
});
|
|
45
47
|
beforeEach(() => {
|
|
46
48
|
vi.useFakeTimers();
|
|
@@ -54,27 +56,26 @@ const settle = async () => {
|
|
|
54
56
|
await Promise.resolve();
|
|
55
57
|
};
|
|
56
58
|
describe("search controller attribution", () => {
|
|
57
|
-
it("
|
|
59
|
+
it("never resends a query id: each request is its own query", async () => {
|
|
58
60
|
const controller = createController();
|
|
59
61
|
search.mockResolvedValue(response());
|
|
60
62
|
controller.submit("shoes");
|
|
61
63
|
await settle();
|
|
62
64
|
controller.setPage(1);
|
|
63
65
|
await settle();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
queryId: "qid-1",
|
|
69
|
-
});
|
|
66
|
+
for (const [request] of search.mock.calls) {
|
|
67
|
+
expect(request.requests[0]).not.toHaveProperty("queryId");
|
|
68
|
+
expect(request.requests[0]).not.toHaveProperty("queryID");
|
|
69
|
+
}
|
|
70
70
|
});
|
|
71
|
-
it("declares the rendered
|
|
71
|
+
it("declares the rendered result's envelope to the impression tracker", async () => {
|
|
72
72
|
const controller = createController();
|
|
73
73
|
search.mockResolvedValue(response({ page: 1, query: "shoes" }));
|
|
74
74
|
controller.submit("shoes");
|
|
75
75
|
await settle();
|
|
76
76
|
expect(tracker.setContext).toHaveBeenCalledWith({
|
|
77
77
|
query_id: "qid-1",
|
|
78
|
+
index: "products_fr",
|
|
78
79
|
surface: "search_page",
|
|
79
80
|
search_type: "lexical",
|
|
80
81
|
page: 2,
|
|
@@ -82,12 +83,12 @@ describe("search controller attribution", () => {
|
|
|
82
83
|
query_length: 5,
|
|
83
84
|
});
|
|
84
85
|
});
|
|
85
|
-
it("
|
|
86
|
+
it("follows the response's queryID: every page is its own query", async () => {
|
|
86
87
|
const controller = createController();
|
|
87
88
|
search
|
|
88
89
|
.mockResolvedValueOnce(response())
|
|
89
|
-
.mockResolvedValueOnce(response({ page: 1,
|
|
90
|
-
.mockResolvedValueOnce(response({ query: "boots",
|
|
90
|
+
.mockResolvedValueOnce(response({ page: 1, queryID: "qid-2" }))
|
|
91
|
+
.mockResolvedValueOnce(response({ query: "boots", queryID: "qid-3" }));
|
|
91
92
|
controller.submit("shoes");
|
|
92
93
|
await settle();
|
|
93
94
|
controller.setPage(1);
|
|
@@ -95,7 +96,7 @@ describe("search controller attribution", () => {
|
|
|
95
96
|
controller.submit("boots");
|
|
96
97
|
await settle();
|
|
97
98
|
const queryIds = tracker.setContext.mock.calls.map(([envelope]) => envelope.query_id);
|
|
98
|
-
expect(queryIds).toEqual(["qid-1", "qid-
|
|
99
|
+
expect(queryIds).toEqual(["qid-1", "qid-2", "qid-3"]);
|
|
99
100
|
});
|
|
100
101
|
it("emits the zero-items view event for a rendered no-results state", async () => {
|
|
101
102
|
const controller = createController();
|
|
@@ -104,6 +105,7 @@ describe("search controller attribution", () => {
|
|
|
104
105
|
await settle();
|
|
105
106
|
expect(trackViewSearchResults).toHaveBeenCalledWith({
|
|
106
107
|
query_id: "qid-1",
|
|
108
|
+
index: "products_fr",
|
|
107
109
|
surface: "search_page",
|
|
108
110
|
search_type: "lexical",
|
|
109
111
|
page: 1,
|
|
@@ -149,6 +151,7 @@ describe("search controller attribution", () => {
|
|
|
149
151
|
});
|
|
150
152
|
expect(trackSelectSearchResult).toHaveBeenCalledWith({
|
|
151
153
|
query_id: "qid-1",
|
|
154
|
+
index: "products_fr",
|
|
152
155
|
surface: "search_page",
|
|
153
156
|
search_type: "lexical",
|
|
154
157
|
page: 1,
|
|
@@ -156,7 +159,7 @@ describe("search controller attribution", () => {
|
|
|
156
159
|
query_length: 5,
|
|
157
160
|
items: [{ product_id: "p1", position: 1 }],
|
|
158
161
|
});
|
|
159
|
-
expect(navigate).toHaveBeenCalledWith("https://shop.example/p1", landed.hits[0]);
|
|
162
|
+
expect(navigate).toHaveBeenCalledWith("https://shop.example/p1", landed.results[0].hits[0]);
|
|
160
163
|
expect(order).toEqual(["impression", "select", "navigate"]);
|
|
161
164
|
});
|
|
162
165
|
it("still records attribution when the hit has no URL to navigate to", async () => {
|
|
@@ -203,6 +206,7 @@ describe("search controller attribution", () => {
|
|
|
203
206
|
trackViewSearchResults,
|
|
204
207
|
trackSelectSearchResult,
|
|
205
208
|
},
|
|
209
|
+
locale: "fr",
|
|
206
210
|
});
|
|
207
211
|
search.mockResolvedValue(response());
|
|
208
212
|
controller.submit("shoes");
|
|
@@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
import { createSearchImpressionTracker } from "../utils/searchImpressions";
|
|
4
4
|
const envelope = {
|
|
5
5
|
query_id: "query-1",
|
|
6
|
+
index: "products_fr",
|
|
6
7
|
surface: "search_page",
|
|
7
8
|
search_type: "lexical",
|
|
8
9
|
page: 1,
|
|
@@ -133,7 +134,7 @@ describe("createSearchImpressionTracker", () => {
|
|
|
133
134
|
tracker.forceImpression({ product_id: "product-1", position: 1 });
|
|
134
135
|
expect(emit).toHaveBeenCalledTimes(1);
|
|
135
136
|
});
|
|
136
|
-
it("
|
|
137
|
+
it("restarts deduplication on every context change: each response mints a fresh query_id", () => {
|
|
137
138
|
const emit = vi.fn();
|
|
138
139
|
const tracker = createSearchImpressionTracker({ emit });
|
|
139
140
|
tracker.setContext(envelope);
|
|
@@ -141,29 +142,11 @@ describe("createSearchImpressionTracker", () => {
|
|
|
141
142
|
tracker.observe(card, { product_id: "product-1", position: 1 });
|
|
142
143
|
intersect(card, 0.6);
|
|
143
144
|
vi.advanceTimersByTime(2000);
|
|
144
|
-
tracker.setContext({ ...envelope, page: 2 });
|
|
145
|
+
tracker.setContext({ ...envelope, query_id: "query-2", page: 2 });
|
|
145
146
|
const cardOnPage2 = element();
|
|
146
147
|
tracker.observe(cardOnPage2, { product_id: "product-1", position: 1 });
|
|
147
148
|
intersect(cardOnPage2, 0.6);
|
|
148
149
|
vi.advanceTimersByTime(2000);
|
|
149
|
-
expect(emit).toHaveBeenCalledTimes(1);
|
|
150
|
-
});
|
|
151
|
-
it("drops a previous query's dedup keys when the query changes", () => {
|
|
152
|
-
const emit = vi.fn();
|
|
153
|
-
const tracker = createSearchImpressionTracker({ emit });
|
|
154
|
-
tracker.setContext(envelope);
|
|
155
|
-
const card = element();
|
|
156
|
-
tracker.observe(card, { product_id: "product-1", position: 1 });
|
|
157
|
-
intersect(card, 0.6);
|
|
158
|
-
vi.advanceTimersByTime(2000);
|
|
159
|
-
tracker.setContext({ ...envelope, query_id: "query-2" });
|
|
160
|
-
// Same query_id served again (e.g. a cached response): a fresh render is
|
|
161
|
-
// a new exposure, like the bfcache restore.
|
|
162
|
-
tracker.setContext(envelope);
|
|
163
|
-
const cardAgain = element();
|
|
164
|
-
tracker.observe(cardAgain, { product_id: "product-1", position: 1 });
|
|
165
|
-
intersect(cardAgain, 0.6);
|
|
166
|
-
vi.advanceTimersByTime(2000);
|
|
167
150
|
expect(emit).toHaveBeenCalledTimes(2);
|
|
168
151
|
});
|
|
169
152
|
it("flushes qualified impressions on pagehide", () => {
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { DialogSearchError } from "../DialogSearchError";
|
|
3
|
-
import {
|
|
3
|
+
import { searchIndexName, searchLexical } from "../services/search";
|
|
4
4
|
const API_KEY = "pk_test_abcdef";
|
|
5
|
+
const request = {
|
|
6
|
+
requests: [
|
|
7
|
+
{
|
|
8
|
+
indexName: "products_fr",
|
|
9
|
+
query: "running shoes",
|
|
10
|
+
page: 2,
|
|
11
|
+
hitsPerPage: 50,
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
5
15
|
const emptyResponse = {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
results: [
|
|
17
|
+
{
|
|
18
|
+
index: "products_fr",
|
|
19
|
+
hits: [],
|
|
20
|
+
nbHits: 0,
|
|
21
|
+
page: 0,
|
|
22
|
+
nbPages: 0,
|
|
23
|
+
hitsPerPage: 20,
|
|
24
|
+
processingTimeMS: 3,
|
|
25
|
+
query: "running shoes",
|
|
26
|
+
queryID: "0198c3f2-0000-7000-8000-000000000000",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
14
29
|
};
|
|
15
30
|
const jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
16
31
|
status,
|
|
@@ -24,72 +39,76 @@ afterEach(() => {
|
|
|
24
39
|
vi.unstubAllGlobals();
|
|
25
40
|
vi.clearAllMocks();
|
|
26
41
|
});
|
|
27
|
-
describe("
|
|
28
|
-
it(
|
|
42
|
+
describe("searchIndexName", () => {
|
|
43
|
+
it.each([
|
|
44
|
+
["fr-FR", "products_fr"],
|
|
45
|
+
["pt-BR", "products_pt"],
|
|
46
|
+
["fr", "products_fr"],
|
|
47
|
+
["not a locale", "products_not a locale"],
|
|
48
|
+
])("reduces the locale %s to a bare-language index name", (locale, name) => {
|
|
49
|
+
expect(searchIndexName("products", locale)).toBe(name);
|
|
50
|
+
});
|
|
51
|
+
it("names every logical index", () => {
|
|
52
|
+
expect(searchIndexName("collections", "en-US")).toBe("collections_en");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("searchLexical", () => {
|
|
56
|
+
it("POSTs the exact request to /public/search/lexical with only the api key header", async () => {
|
|
29
57
|
fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
|
|
30
|
-
await
|
|
31
|
-
query: "running shoes",
|
|
32
|
-
page: 2,
|
|
33
|
-
hitsPerPage: 50,
|
|
34
|
-
queryId: "0198c3f2-0000-7000-8000-000000000000",
|
|
35
|
-
});
|
|
58
|
+
await searchLexical(API_KEY, request);
|
|
36
59
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
37
60
|
const [url, init] = fetchMock.mock.calls[0];
|
|
38
|
-
expect(url).toMatch(/\/public\/search$/);
|
|
61
|
+
expect(url).toMatch(/\/public\/search\/lexical$/);
|
|
39
62
|
expect(init.method).toBe("POST");
|
|
40
63
|
expect(init.headers).toEqual({
|
|
41
64
|
"Content-Type": "application/json",
|
|
42
65
|
"x-dialog-api-key": API_KEY,
|
|
43
66
|
});
|
|
44
|
-
expect(JSON.parse(init.body)).toEqual(
|
|
45
|
-
query: "running shoes",
|
|
46
|
-
page: 2,
|
|
47
|
-
hitsPerPage: 50,
|
|
48
|
-
queryId: "0198c3f2-0000-7000-8000-000000000000",
|
|
49
|
-
});
|
|
67
|
+
expect(JSON.parse(init.body)).toEqual(request);
|
|
50
68
|
});
|
|
51
|
-
it("
|
|
69
|
+
it("keeps optional per-entry fields the caller did not provide off the wire", async () => {
|
|
52
70
|
fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
|
|
53
|
-
await
|
|
71
|
+
await searchLexical(API_KEY, {
|
|
72
|
+
requests: [{ indexName: "products_fr", query: "running shoes" }],
|
|
73
|
+
});
|
|
54
74
|
const [, init] = fetchMock.mock.calls[0];
|
|
55
75
|
expect(JSON.parse(init.body)).toEqual({
|
|
56
|
-
query: "running shoes",
|
|
76
|
+
requests: [{ indexName: "products_fr", query: "running shoes" }],
|
|
57
77
|
});
|
|
58
78
|
});
|
|
59
79
|
it("resolves the typed response, including empty results", async () => {
|
|
60
80
|
fetchMock.mockResolvedValue(jsonResponse(emptyResponse));
|
|
61
|
-
await expect(
|
|
81
|
+
await expect(searchLexical(API_KEY, request)).resolves.toEqual(emptyResponse);
|
|
62
82
|
});
|
|
63
83
|
it.each([
|
|
64
|
-
[
|
|
65
|
-
[
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
],
|
|
70
|
-
[429, "THROTTLED", "Too many requests"],
|
|
71
|
-
[503, "STOREFRONT_SEARCH_UNAVAILABLE", "Storefront search is unavailable"],
|
|
72
|
-
])("rejects a %i answer with a DialogSearchError carrying status, code and message", async (status, code, message) => {
|
|
73
|
-
fetchMock.mockResolvedValue(jsonResponse({ statusCode: status, error: code, message }, status));
|
|
74
|
-
const error = await searchProducts(API_KEY, {
|
|
75
|
-
query: "running shoes",
|
|
76
|
-
}).catch((caught) => caught);
|
|
84
|
+
[404, "Index products_xx does not exist"],
|
|
85
|
+
[400, "Unknown parameter: foo"],
|
|
86
|
+
])("rejects the Algolia-shaped %i error body with a DialogSearchError", async (status, message) => {
|
|
87
|
+
fetchMock.mockResolvedValue(jsonResponse({ message, status }, status));
|
|
88
|
+
const error = await searchLexical(API_KEY, request).catch((caught) => caught);
|
|
77
89
|
expect(error).toBeInstanceOf(DialogSearchError);
|
|
78
90
|
expect(error).toMatchObject({
|
|
79
91
|
name: "DialogSearchError",
|
|
80
92
|
status,
|
|
81
|
-
code,
|
|
93
|
+
code: undefined,
|
|
82
94
|
message,
|
|
83
95
|
});
|
|
84
96
|
});
|
|
97
|
+
it.each([
|
|
98
|
+
[401, "INVALID_WIDGET_API_KEY", "Invalid widget API key"],
|
|
99
|
+
[429, "THROTTLED", "Too many requests"],
|
|
100
|
+
])("still reads the code of a Nest-shaped %i guard error", async (status, code, message) => {
|
|
101
|
+
fetchMock.mockResolvedValue(jsonResponse({ statusCode: status, error: code, message }, status));
|
|
102
|
+
const error = await searchLexical(API_KEY, request).catch((caught) => caught);
|
|
103
|
+
expect(error).toBeInstanceOf(DialogSearchError);
|
|
104
|
+
expect(error).toMatchObject({ status, code, message });
|
|
105
|
+
});
|
|
85
106
|
it("falls back to statusText when the error body is not JSON", async () => {
|
|
86
107
|
fetchMock.mockResolvedValue(new Response("<html>Bad Gateway</html>", {
|
|
87
108
|
status: 502,
|
|
88
109
|
statusText: "Bad Gateway",
|
|
89
110
|
}));
|
|
90
|
-
const error = await
|
|
91
|
-
query: "running shoes",
|
|
92
|
-
}).catch((caught) => caught);
|
|
111
|
+
const error = await searchLexical(API_KEY, request).catch((caught) => caught);
|
|
93
112
|
expect(error).toBeInstanceOf(DialogSearchError);
|
|
94
113
|
expect(error).toMatchObject({
|
|
95
114
|
status: 502,
|
|
@@ -100,13 +119,13 @@ describe("searchProducts", () => {
|
|
|
100
119
|
it("propagates network failures untouched", async () => {
|
|
101
120
|
const networkError = new TypeError("Failed to fetch");
|
|
102
121
|
fetchMock.mockRejectedValue(networkError);
|
|
103
|
-
await expect(
|
|
122
|
+
await expect(searchLexical(API_KEY, request)).rejects.toBe(networkError);
|
|
104
123
|
});
|
|
105
124
|
it("forwards the AbortSignal and preserves the native AbortError", async () => {
|
|
106
125
|
const abortError = new DOMException("The operation was aborted.", "AbortError");
|
|
107
126
|
fetchMock.mockRejectedValue(abortError);
|
|
108
127
|
const controller = new AbortController();
|
|
109
|
-
await expect(
|
|
128
|
+
await expect(searchLexical(API_KEY, request, { signal: controller.signal })).rejects.toBe(abortError);
|
|
110
129
|
const [, init] = fetchMock.mock.calls[0];
|
|
111
130
|
expect(init.signal).toBe(controller.signal);
|
|
112
131
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
baseApiUrl: "https://hr5buzenb1.execute-api.eu-west-1.amazonaws.com",
|
|
3
|
-
// Nest monolith (staging) — serves POST /public/search.
|
|
3
|
+
// Nest monolith (staging) — serves POST /public/search/lexical.
|
|
4
4
|
monolithApiUrl: "https://fvcphlqyle.execute-api.eu-west-1.amazonaws.com",
|
|
5
5
|
assistantUrl: "https://d2bycosa71tnxv.cloudfront.net/assets/index.js",
|
|
6
6
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
baseApiUrl: "https://rtbzcxkmwj.execute-api.eu-west-1.amazonaws.com",
|
|
3
|
-
// Nest monolith (production) — serves POST /public/search. Custom domain,
|
|
3
|
+
// Nest monolith (production) — serves POST /public/search/lexical. Custom domain,
|
|
4
4
|
// same as the dashboard's VITE_MONOLITH_API_URL: the raw execute-api
|
|
5
5
|
// gateway URL answers 500 on every route and must not be used.
|
|
6
6
|
monolithApiUrl: "https://api.askdialog.ai",
|
package/dist/config/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const config = {
|
|
2
2
|
baseApiUrl: "https://rtbzcxkmwj.execute-api.eu-west-1.amazonaws.com",
|
|
3
|
-
// Nest monolith (production) — serves POST /public/search. Custom domain,
|
|
3
|
+
// Nest monolith (production) — serves POST /public/search/lexical. Custom domain,
|
|
4
4
|
// same as the dashboard's VITE_MONOLITH_API_URL: the raw execute-api
|
|
5
5
|
// gateway URL answers 500 on every route and must not be used.
|
|
6
6
|
monolithApiUrl: "https://api.askdialog.ai",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { SearchController, SearchControllerOptions } from "./types/searchController";
|
|
2
|
-
export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, locale,
|
|
2
|
+
export declare function createSearchController({ search, analytics, navigate, debounceMs, hitsPerPage, locale, }: SearchControllerOptions): SearchController;
|
|
3
3
|
//# sourceMappingURL=searchController.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../src/searchController.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EAGxB,MAAM,0BAA0B,CAAC;AAgBlC,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,SAAS,EACT,QAAQ,EACR,UAAgC,EAChC,WAAmC,EACnC,MAAM,GACP,EAAE,uBAAuB,GAAG,gBAAgB,CA8K5C"}
|
package/dist/searchController.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// This controller is one cohesive unit. `selectResult` now returns whether the
|
|
2
|
+
// navigate adapter handled the transition, nudging the file just past the
|
|
3
|
+
// default 200-line cap — raised modestly rather than split artificially.
|
|
4
|
+
/* eslint max-lines: ["error", 220] */
|
|
5
|
+
import { searchIndexName } from "./services/search";
|
|
1
6
|
import { SearchStatus, } from "./types/searchController";
|
|
2
7
|
import { createControllerAnalytics } from "./utils/searchControllerAnalytics";
|
|
3
8
|
const DEFAULT_DEBOUNCE_MS = 250;
|
|
@@ -11,7 +16,8 @@ const INITIAL_STATE = {
|
|
|
11
16
|
response: undefined,
|
|
12
17
|
error: undefined,
|
|
13
18
|
};
|
|
14
|
-
export function createSearchController({ search, analytics, navigate, debounceMs = DEFAULT_DEBOUNCE_MS, hitsPerPage = DEFAULT_HITS_PER_PAGE, locale,
|
|
19
|
+
export function createSearchController({ search, analytics, navigate, debounceMs = DEFAULT_DEBOUNCE_MS, hitsPerPage = DEFAULT_HITS_PER_PAGE, locale, }) {
|
|
20
|
+
const indexName = searchIndexName("products", locale);
|
|
15
21
|
let state = INITIAL_STATE;
|
|
16
22
|
const listeners = new Set();
|
|
17
23
|
const controllerAnalytics = createControllerAnalytics(analytics);
|
|
@@ -34,22 +40,9 @@ export function createSearchController({ search, analytics, navigate, debounceMs
|
|
|
34
40
|
abortController?.abort();
|
|
35
41
|
abortController = undefined;
|
|
36
42
|
};
|
|
37
|
-
const buildRequest = (query, page) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
request.locale = locale;
|
|
41
|
-
}
|
|
42
|
-
if (countryCode !== undefined) {
|
|
43
|
-
request.countryCode = countryCode;
|
|
44
|
-
}
|
|
45
|
-
// Query unchanged since the last response: resend its queryId; a new
|
|
46
|
-
// query gets a fresh engine-generated one.
|
|
47
|
-
const previous = state.response;
|
|
48
|
-
if (previous?.query === query) {
|
|
49
|
-
request.queryId = previous.queryId;
|
|
50
|
-
}
|
|
51
|
-
return request;
|
|
52
|
-
};
|
|
43
|
+
const buildRequest = (query, page) => ({
|
|
44
|
+
requests: [{ indexName, query, page, hitsPerPage }],
|
|
45
|
+
});
|
|
53
46
|
const run = async (query, page) => {
|
|
54
47
|
cancelInFlight();
|
|
55
48
|
abortController = new AbortController();
|
|
@@ -63,10 +56,14 @@ export function createSearchController({ search, analytics, navigate, debounceMs
|
|
|
63
56
|
if (id !== requestId) {
|
|
64
57
|
return; // A newer request landed first: this response is stale.
|
|
65
58
|
}
|
|
66
|
-
|
|
59
|
+
const result = response.results.find((entry) => entry.index === indexName);
|
|
60
|
+
if (result === undefined) {
|
|
61
|
+
throw new Error(`Dialog search returned no ${indexName} entry`);
|
|
62
|
+
}
|
|
63
|
+
controllerAnalytics.onResponse(result);
|
|
67
64
|
setState({
|
|
68
|
-
status:
|
|
69
|
-
response,
|
|
65
|
+
status: result.nbHits === 0 ? SearchStatus.EMPTY : SearchStatus.SUCCESS,
|
|
66
|
+
response: result,
|
|
70
67
|
error: undefined,
|
|
71
68
|
});
|
|
72
69
|
}
|
|
@@ -146,7 +143,7 @@ export function createSearchController({ search, analytics, navigate, debounceMs
|
|
|
146
143
|
return false;
|
|
147
144
|
}
|
|
148
145
|
controllerAnalytics.select(response, index);
|
|
149
|
-
const url = response.hits[index].
|
|
146
|
+
const url = response.hits[index].url;
|
|
150
147
|
const runAdapter = options?.navigate ?? true;
|
|
151
148
|
if (runAdapter && navigate !== undefined && url !== undefined) {
|
|
152
149
|
navigate(url, response.hits[index]);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { SearchOptions, SearchRequest, SearchResponse } from "../types/search";
|
|
1
|
+
import { SearchIndex, SearchOptions, SearchRequest, SearchResponse } from "../types/search";
|
|
2
|
+
export declare const searchIndexName: (index: SearchIndex, locale: string) => string;
|
|
2
3
|
/**
|
|
3
4
|
* One POST per invocation — no debounce, cache, retry or request state; the
|
|
4
5
|
* caller owns cancellation through `options.signal`.
|
|
5
6
|
*/
|
|
6
|
-
export declare const
|
|
7
|
+
export declare const searchLexical: (apiKey: string, request: SearchRequest, options?: SearchOptions) => Promise<SearchResponse>;
|
|
7
8
|
//# sourceMappingURL=search.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,iBAAiB,CAAC;AAMzB,eAAO,MAAM,eAAe,GAAI,OAAO,WAAW,EAAE,QAAQ,MAAM,KAAG,MACzB,CAAC;AAoB7C;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,SAAS,aAAa,EACtB,UAAU,aAAa,KACtB,OAAO,CAAC,cAAc,CAgBxB,CAAC"}
|
package/dist/services/search.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { config } from "../config";
|
|
2
2
|
import { DialogSearchError } from "../DialogSearchError";
|
|
3
|
-
|
|
3
|
+
import { toIso639LanguageCode } from "../utils/localization";
|
|
4
|
+
const SEARCH_PATH = "/public/search/lexical";
|
|
4
5
|
const API_KEY_HEADER = "x-dialog-api-key";
|
|
6
|
+
export const searchIndexName = (index, locale) => `${index}_${toIso639LanguageCode(locale)}`;
|
|
5
7
|
const toSearchError = async (response) => {
|
|
6
8
|
let body;
|
|
7
9
|
try {
|
|
@@ -20,7 +22,7 @@ const toSearchError = async (response) => {
|
|
|
20
22
|
* One POST per invocation — no debounce, cache, retry or request state; the
|
|
21
23
|
* caller owns cancellation through `options.signal`.
|
|
22
24
|
*/
|
|
23
|
-
export const
|
|
25
|
+
export const searchLexical = async (apiKey, request, options) => {
|
|
24
26
|
const response = await fetch(`${config.monolithApiUrl}${SEARCH_PATH}`, {
|
|
25
27
|
method: "POST",
|
|
26
28
|
headers: {
|
|
@@ -5,6 +5,11 @@ export interface DialogCallbacks {
|
|
|
5
5
|
addToCart: (input: AddToCartInput) => Promise<void>;
|
|
6
6
|
getProduct: (productId: string, variantId?: string) => Promise<SimplifiedProduct>;
|
|
7
7
|
}
|
|
8
|
+
export interface CurrentProduct {
|
|
9
|
+
/** The catalog product id — must match the id in the Dialog product feed. */
|
|
10
|
+
id: string;
|
|
11
|
+
variantId?: string;
|
|
12
|
+
}
|
|
8
13
|
export interface DialogConstructor {
|
|
9
14
|
apiKey: string;
|
|
10
15
|
locale: string;
|
|
@@ -40,5 +45,15 @@ export interface DialogConstructor {
|
|
|
40
45
|
* Product links and recommendation browsing are unaffected.
|
|
41
46
|
*/
|
|
42
47
|
disableAddToCart?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The product of the page the SDK is instantiated on, when it is a product
|
|
50
|
+
* page. The assistant uses it as the conversation's product context for
|
|
51
|
+
* entry points that carry no product of their own (floating bookmark,
|
|
52
|
+
* resume surface, free-text questions), so answers stay grounded on the
|
|
53
|
+
* product under the visitor's eyes after PDP-to-PDP navigation. On
|
|
54
|
+
* single-page storefronts, update it on navigation with
|
|
55
|
+
* `setCurrentProduct()` / `clearCurrentProduct()`.
|
|
56
|
+
*/
|
|
57
|
+
product?: CurrentProduct;
|
|
43
58
|
}
|
|
44
59
|
//# 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;IAChB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;
|
|
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,cAAc;IAC7B,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;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;IAClC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;CAC1B"}
|
package/dist/types/search.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/** Logical indices the public search serves; the wire name adds the locale. */
|
|
2
|
+
export declare const SEARCH_INDICES: readonly ["products", "collections", "articles", "pages"];
|
|
3
|
+
export type SearchIndex = (typeof SEARCH_INDICES)[number];
|
|
4
|
+
export interface SearchQuery {
|
|
5
|
+
/** `<index>_<locale>` (e.g. `products_fr`), one shared locale per request; unknown or unserved → 404. */
|
|
6
|
+
indexName: string;
|
|
2
7
|
/** Trimmed server-side; must keep at least two visible characters (code points). */
|
|
3
8
|
query: string;
|
|
4
9
|
/** Zero-indexed results page. Defaults to 0 server-side. */
|
|
5
10
|
page?: number;
|
|
6
11
|
/** Between 1 and 100. Defaults to 20 server-side. */
|
|
7
12
|
hitsPerPage?: number;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
countryCode?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchRequest {
|
|
15
|
+
requests: SearchQuery[];
|
|
12
16
|
}
|
|
13
17
|
export interface SearchOptions {
|
|
14
18
|
/** Forwarded to fetch untouched: aborting rejects with the native AbortError. */
|
|
@@ -17,41 +21,39 @@ export interface SearchOptions {
|
|
|
17
21
|
export interface SearchPrice {
|
|
18
22
|
/** Decimal amount as a string, exactly as indexed (e.g. "24.90"). */
|
|
19
23
|
amount: string;
|
|
20
|
-
|
|
24
|
+
/** Absent when the price was indexed without a currency. */
|
|
25
|
+
currencyCode?: string;
|
|
21
26
|
}
|
|
22
27
|
export interface SearchPriceRange {
|
|
23
28
|
min: SearchPrice;
|
|
24
29
|
max: SearchPrice;
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
|
-
*
|
|
28
|
-
* best-effort
|
|
32
|
+
* A flat, storefront-ready record: `objectID` plus the record's attributes.
|
|
33
|
+
* Every display field is best-effort — a hit may carry only its id;
|
|
34
|
+
* `priceRange` only comes from the products index.
|
|
29
35
|
*/
|
|
30
|
-
export interface
|
|
31
|
-
|
|
36
|
+
export interface SearchHit {
|
|
37
|
+
objectID: string;
|
|
32
38
|
title?: string;
|
|
33
39
|
url?: string;
|
|
34
|
-
/** Shopify URL handle, lets the storefront build `/products/{handle}` when `url` is absent (DEC-2543). */
|
|
35
40
|
handle?: string;
|
|
36
41
|
imageUrl?: string;
|
|
37
42
|
priceRange?: SearchPriceRange;
|
|
38
|
-
compareAtPriceRange?: SearchPriceRange;
|
|
39
|
-
inStock?: boolean;
|
|
40
43
|
}
|
|
41
|
-
export interface
|
|
42
|
-
|
|
43
|
-
score: number;
|
|
44
|
-
product: SearchProduct;
|
|
45
|
-
}
|
|
46
|
-
export interface SearchResponse {
|
|
47
|
-
/** Engine-generated id for search attribution analytics. */
|
|
48
|
-
queryId: string;
|
|
44
|
+
export interface SearchResult {
|
|
45
|
+
index: string;
|
|
49
46
|
hits: SearchHit[];
|
|
50
47
|
nbHits: number;
|
|
51
48
|
page: number;
|
|
52
49
|
nbPages: number;
|
|
53
50
|
hitsPerPage: number;
|
|
54
|
-
|
|
51
|
+
processingTimeMS: number;
|
|
55
52
|
query: string;
|
|
53
|
+
queryID: string;
|
|
54
|
+
}
|
|
55
|
+
/** One entry per request entry, in request order. */
|
|
56
|
+
export interface SearchResponse {
|
|
57
|
+
results: SearchResult[];
|
|
56
58
|
}
|
|
57
59
|
//# sourceMappingURL=search.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/types/search.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,eAAO,MAAM,cAAc,2DAKjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,MAAM,WAAW,WAAW;IAC1B,yGAAyG;IACzG,SAAS,EAAE,MAAM,CAAC;IAClB,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,WAAW,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB"}
|
package/dist/types/search.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** Logical indices the public search serves; the wire name adds the locale. */
|
|
2
|
+
export const SEARCH_INDICES = [
|
|
3
|
+
"products",
|
|
4
|
+
"collections",
|
|
5
|
+
"articles",
|
|
6
|
+
"pages",
|
|
7
|
+
];
|