@askdialog/dialog-sdk 2.10.0 → 2.12.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 (40) hide show
  1. package/README.md +32 -20
  2. package/dist/Dialog.d.ts +12 -21
  3. package/dist/Dialog.d.ts.map +1 -1
  4. package/dist/Dialog.js +25 -51
  5. package/dist/__tests__/dialogSearchAnalytics.spec.js +2 -4
  6. package/dist/__tests__/publicTypes.spec.js +1 -0
  7. package/dist/__tests__/searchController.spec.js +116 -11
  8. package/dist/__tests__/searchControllerAttribution.spec.js +8 -6
  9. package/dist/__tests__/searchImpressions.spec.js +2 -3
  10. package/dist/__tests__/searchService.spec.js +13 -11
  11. package/dist/searchController.d.ts +1 -1
  12. package/dist/searchController.d.ts.map +1 -1
  13. package/dist/searchController.js +47 -57
  14. package/dist/services/search.d.ts +3 -5
  15. package/dist/services/search.d.ts.map +1 -1
  16. package/dist/services/search.js +11 -6
  17. package/dist/types/constructor.d.ts +3 -0
  18. package/dist/types/constructor.d.ts.map +1 -1
  19. package/dist/types/search.d.ts +14 -14
  20. package/dist/types/search.d.ts.map +1 -1
  21. package/dist/types/search.js +1 -1
  22. package/dist/types/searchAnalytics.d.ts +12 -6
  23. package/dist/types/searchAnalytics.d.ts.map +1 -1
  24. package/dist/types/searchAnalytics.js +1 -4
  25. package/dist/types/searchController.d.ts +29 -52
  26. package/dist/types/searchController.d.ts.map +1 -1
  27. package/dist/types/searchController.js +1 -1
  28. package/dist/utils/localization.d.ts +0 -1
  29. package/dist/utils/localization.d.ts.map +1 -1
  30. package/dist/utils/localization.js +0 -11
  31. package/dist/utils/searchControllerAnalytics.d.ts +1 -4
  32. package/dist/utils/searchControllerAnalytics.d.ts.map +1 -1
  33. package/dist/utils/searchControllerAnalytics.js +5 -8
  34. package/dist/utils/searchImpressions.d.ts +4 -4
  35. package/dist/utils/searchImpressions.d.ts.map +1 -1
  36. package/dist/utils/searchImpressions.js +4 -11
  37. package/dist/utils/searchRequests.d.ts +17 -0
  38. package/dist/utils/searchRequests.d.ts.map +1 -0
  39. package/dist/utils/searchRequests.js +36 -0
  40. package/package.json +1 -1
@@ -1,97 +1,74 @@
1
- import { SearchHit, SearchOptions, SearchRequest, SearchResponse, SearchResult } from "./search";
1
+ import { SearchHit, SearchIndex, SearchOptions, SearchRequest, SearchResponse, SearchResult } from "./search";
2
2
  import { SearchSurface, SelectSearchResultParams, ViewSearchResultsParams } from "./searchAnalytics";
3
3
  export declare const SearchStatus: {
4
4
  readonly IDLE: "idle";
5
5
  readonly LOADING: "loading";
6
6
  readonly SUCCESS: "success";
7
- /** The request succeeded with zero hits — distinct from `success` so views render a no-results state. */
7
+ /** Successful request with zero hits. */
8
8
  readonly EMPTY: "empty";
9
9
  readonly ERROR: "error";
10
10
  };
11
11
  export type SearchStatus = (typeof SearchStatus)[keyof typeof SearchStatus];
12
+ export interface SearchSection {
13
+ index: Exclude<SearchIndex, "products">;
14
+ hitsPerPage?: number;
15
+ }
12
16
  export interface SearchControllerState {
13
17
  status: SearchStatus;
14
- /** Last committed (run) query not the text currently being typed. */
18
+ /** Last submitted query, excluding pending input. */
15
19
  query: string;
16
- /** Zero-indexed page of the last run. */
20
+ /** Zero-based page of the last request. */
17
21
  page: number;
18
- /**
19
- * Products entry of the last landed response; kept while the next page
20
- * loads, cleared on error. Hits are flat records (`objectID`, `title`, …).
21
- */
22
+ /** Last products result; retained while loading and cleared on error or reset. */
22
23
  response?: SearchResult;
24
+ sections?: Partial<Record<SearchIndex, SearchResult>>;
23
25
  error?: unknown;
24
26
  }
25
27
  export type SearchFunction = (request: SearchRequest, options?: SearchOptions) => Promise<SearchResponse>;
26
- /**
27
- * Analytics sinks for the DEC-2448 storefront-search contract — wire the two
28
- * track methods to the `Dialog` instance. Impressions (viewport batching,
29
- * dedup, click-forced) and the zero-items no-results event are owned by the
30
- * controller; integrations only declare where results are displayed.
31
- */
28
+ /** Event callbacks; the controller handles impression batching and deduplication. */
32
29
  export interface SearchControllerAnalytics {
33
30
  surface: SearchSurface;
34
31
  trackViewSearchResults: (params: ViewSearchResultsParams) => void;
35
32
  trackSelectSearchResult: (params: SelectSearchResultParams) => void;
36
33
  }
37
34
  export interface SearchControllerOptions {
38
- /** The bound `dialog.search` function. */
35
+ /** Search function bound to its client, or a custom transport. */
39
36
  search: SearchFunction;
37
+ /** Lowercase ISO 639-1 language code, e.g. `fr`. */
38
+ language: string;
39
+ /** ISO 4217 currency, independent of language. */
40
+ currency: string;
40
41
  analytics: SearchControllerAnalytics;
41
- /**
42
- * Platform navigation adapter, called by `selectResult` AFTER attribution
43
- * is recorded, with the product URL of the selected hit. Omit it when the
44
- * rendering layer navigates natively (plain `<a href>`): attribution still
45
- * fires, the events are designed to survive navigation.
46
- */
42
+ /** Navigate after recording selection. Omit to use native link navigation. */
47
43
  navigate?: (url: string, hit: SearchHit) => void;
48
44
  debounceMs?: number;
49
45
  hitsPerPage?: number;
50
- locale: string;
46
+ sections?: readonly SearchSection[] | (() => readonly SearchSection[]);
51
47
  }
52
- /**
53
- * Framework-agnostic stateful search behavior around the stateless
54
- * `dialog.search()` transport: debounce, cancellation, stale-response
55
- * protection, pagination, retry and DEC-2448 attribution. Rendering and
56
- * routing stay in the integration (raw JavaScript, React, Vue, Shopify).
57
- */
48
+ /** Manage search requests, pagination and analytics independently of rendering. */
58
49
  export interface SearchController {
59
- /** Debounced entry point for keystrokes. */
50
+ /** Submit after the debounce delay. */
60
51
  setQuery(rawQuery: string): void;
61
- /** Immediate (non-debounced) submission, e.g. Enter or a search button. */
52
+ /** Submit immediately. */
62
53
  submit(rawQuery: string): void;
63
- /** Immediate pagination of the current results (zero-indexed). */
54
+ /** Request a zero-based page immediately. Pending input starts at page zero. */
64
55
  setPage(page: number): void;
65
- /** Re-run the failed request; no-op unless the state is `error`. */
56
+ /** Retry the last request when the state is `error`. */
66
57
  retry(): void;
67
- /**
68
- * Watch the rendered element of `state.response.hits[index]` for a
69
- * viewport impression. Call it for every result element after rendering.
70
- */
58
+ /** Track impressions for the element representing `response.hits[index]`. */
71
59
  observeResult(element: Element, index: number): void;
72
60
  /**
73
- * Record selection attribution for `state.response.hits[index]` (forced
74
- * impression + select event), then unless `options.navigate` is `false` —
75
- * hand navigation to the `navigate` adapter. Call it on click AND
76
- * middle-click/cmd+click.
77
- *
78
- * Pass `{ navigate: false }` for gestures the browser should handle natively
79
- * (middle-click, or cmd/ctrl/shift/alt+click → new tab/window): attribution
80
- * is still recorded, but the in-app adapter — which cannot open a new tab —
81
- * is skipped so the native `<a href>` navigation runs.
82
- *
83
- * Returns `true` when the adapter handled the transition, so a rendering
84
- * layer wrapping results in `<a href>` can `preventDefault()` and avoid a
85
- * second (native) navigation. Returns `false` otherwise (adapter skipped, no
86
- * adapter, or the hit has no URL) — let the native navigation proceed.
61
+ * Record the impression and selection for `response.hits[index]`.
62
+ * Pass `{ navigate: false }` for middle-clicks or modified clicks.
63
+ * Returns true if the navigation adapter ran; call `preventDefault()` then.
87
64
  */
88
65
  selectResult(index: number, options?: {
89
66
  navigate?: boolean;
90
67
  }): boolean;
91
- /** Register a state listener; returns its unsubscribe function. */
68
+ /** Subscribe to state changes; returns an unsubscribe function. */
92
69
  subscribe(listener: (state: SearchControllerState) => void): () => void;
93
70
  getState(): SearchControllerState;
94
- /** Cancel everything in flight and detach listeners/observers. */
71
+ /** Cancel pending requests and remove listeners and observers. */
95
72
  dispose(): void;
96
73
  }
97
74
  //# sourceMappingURL=searchController.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../../src/types/searchController.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,YAAY;;;;IAIvB,yGAAyG;;;CAGjG,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,YAAY,CAAC;IACrB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,aAAa,KACpB,OAAO,CAAC,cAAc,CAAC,CAAC;AAE7B;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,aAAa,CAAC;IACvB,sBAAsB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClE,uBAAuB,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,uBAAuB;IACtC,0CAA0C;IAC1C,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,yBAAyB,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,KAAK,IAAI,CAAC;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,2EAA2E;IAC3E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD;;;;;;;;;;;;;;;OAeG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC;IACvE,mEAAmE;IACnE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,QAAQ,IAAI,qBAAqB,CAAC;IAClC,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAAC;CACjB"}
1
+ {"version":3,"file":"searchController.d.ts","sourceRoot":"","sources":["../../src/types/searchController.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,WAAW,EACX,aAAa,EACb,aAAa,EACb,cAAc,EACd,YAAY,EACb,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,uBAAuB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,YAAY;;;;IAIvB,yCAAyC;;;CAGjC,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,YAAY,CAAC;IACrB,qDAAqD;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG,CAC3B,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,aAAa,KACpB,OAAO,CAAC,cAAc,CAAC,CAAC;AAE7B,qFAAqF;AACrF,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,aAAa,CAAC;IACvB,sBAAsB,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAClE,uBAAuB,EAAE,CAAC,MAAM,EAAE,wBAAwB,KAAK,IAAI,CAAC;CACrE;AAED,MAAM,WAAW,uBAAuB;IACtC,kEAAkE;IAClE,MAAM,EAAE,cAAc,CAAC;IACvB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,yBAAyB,CAAC;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,KAAK,IAAI,CAAC;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,CAAC,MAAM,SAAS,aAAa,EAAE,CAAC,CAAC;CACxE;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,0BAA0B;IAC1B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gFAAgF;IAChF,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,wDAAwD;IACxD,KAAK,IAAI,IAAI,CAAC;IACd,6EAA6E;IAC7E,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrD;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC;IACvE,mEAAmE;IACnE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACxE,QAAQ,IAAI,qBAAqB,CAAC;IAClC,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAAC;CACjB"}
@@ -2,7 +2,7 @@ export const SearchStatus = {
2
2
  IDLE: "idle",
3
3
  LOADING: "loading",
4
4
  SUCCESS: "success",
5
- /** The request succeeded with zero hits — distinct from `success` so views render a no-results state. */
5
+ /** Successful request with zero hits. */
6
6
  EMPTY: "empty",
7
7
  ERROR: "error",
8
8
  };
@@ -23,5 +23,4 @@ export declare const resolveTextDirection: (code?: string) => TextDirection;
23
23
  * therefore keeps a generic language name ("French", not "French (France)").
24
24
  */
25
25
  export declare const getDetailedLocaleInfo: (locale: string, countryCodeOverride?: string) => DetailedLocaleInfo | null;
26
- export declare const toIso639LanguageCode: (locale: string) => string;
27
26
  //# sourceMappingURL=localization.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"localization.d.ts","sourceRoot":"","sources":["../../src/utils/localization.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,GAC7B,aAAa,MAAM,GAAG,SAAS,KAC9B,WAAW,IAAI,MAC8C,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,KAAK,CAAC;AAQ1C,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,OAEa,CAAC;AAEhE,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,KAAG,aACZ,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,sBAAsB,MAAM,KAC3B,kBAAkB,GAAG,IAoCvB,CAAC;AAKF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,KAAG,MAMrD,CAAC"}
1
+ {"version":3,"file":"localization.d.ts","sourceRoot":"","sources":["../../src/utils/localization.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,kBAAkB,GAC7B,aAAa,MAAM,GAAG,SAAS,KAC9B,WAAW,IAAI,MAC8C,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,KAAK,CAAC;AAQ1C,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,OAEa,CAAC;AAEhE,eAAO,MAAM,oBAAoB,GAAI,OAAO,MAAM,KAAG,aACZ,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,sBAAsB,MAAM,KAC3B,kBAAkB,GAAG,IAoCvB,CAAC"}
@@ -52,14 +52,3 @@ export const getDetailedLocaleInfo = (locale, countryCodeOverride) => {
52
52
  return null;
53
53
  }
54
54
  };
55
- // The search backend keys its indexes on the bare ISO 639-1 language, while
56
- // storefronts usually hold full BCP-47 tags ("fr-FR", "pt-BR"). An unparsable
57
- // value is returned as-is so a search is never dropped over its locale.
58
- export const toIso639LanguageCode = (locale) => {
59
- try {
60
- return new Intl.Locale(locale).language;
61
- }
62
- catch {
63
- return locale;
64
- }
65
- };
@@ -1,9 +1,6 @@
1
1
  import { SearchResult } from "../types/search";
2
2
  import { SearchControllerAnalytics } from "../types/searchController";
3
- /**
4
- * Internal DEC-2448 wiring of the search controller: envelope computation,
5
- * viewport impressions and selection attribution. Not part of the public API.
6
- */
3
+ /** Internal binding for search impressions and selections. */
7
4
  export interface ControllerAnalyticsBinding {
8
5
  onResponse(result: SearchResult): void;
9
6
  observeResult(element: Element, result: SearchResult, index: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"searchControllerAnalytics.d.ts","sourceRoot":"","sources":["../../src/utils/searchControllerAnalytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK/C,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAMtE;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACvC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3E,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,OAAO,IAAI,IAAI,CAAC;CACjB;AAQD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,yBAAyB,GACnC,0BAA0B,CAkD5B"}
1
+ {"version":3,"file":"searchControllerAnalytics.d.ts","sourceRoot":"","sources":["../../src/utils/searchControllerAnalytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK/C,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AAMtE,8DAA8D;AAC9D,MAAM,WAAW,0BAA0B;IACzC,UAAU,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IACvC,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3E,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAClD,OAAO,IAAI,IAAI,CAAC;CACjB;AAQD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,yBAAyB,GACnC,0BAA0B,CAgD5B"}
@@ -1,14 +1,13 @@
1
1
  import { createSearchImpressionTracker, } from "./searchImpressions";
2
2
  const resultItem = (result, index) => ({
3
3
  product_id: result.hits[index].objectID,
4
- // 1-based and absolute across pages (page 2, first item, 20/page → 21).
4
+ // One-based position across all pages.
5
5
  position: result.page * result.hitsPerPage + index + 1,
6
6
  });
7
7
  export function createControllerAnalytics(analytics) {
8
8
  let impressions;
9
9
  let envelope;
10
- // Lazy: the tracker needs window/IntersectionObserver, so a controller
11
- // constructed in a non-browser context stays inert until a response lands.
10
+ // Defer browser-dependent tracking until a response arrives.
12
11
  const tracker = () => {
13
12
  impressions ??= createSearchImpressionTracker({
14
13
  emit: analytics.trackViewSearchResults,
@@ -21,17 +20,15 @@ export function createControllerAnalytics(analytics) {
21
20
  query_id: result.queryID,
22
21
  index: result.index,
23
22
  surface: analytics.surface,
24
- // The storefront search API is lexical-only today.
25
23
  search_type: "lexical",
26
- // Analytics pages are 1-based; the wire response is 0-based.
24
+ // Convert the API page to one-based analytics numbering.
27
25
  page: result.page + 1,
28
26
  total_hits: result.nbHits,
29
27
  query_length: [...result.query].length,
30
28
  };
31
29
  tracker().setContext(envelope);
32
30
  if (result.nbHits === 0) {
33
- // A rendered no-results state is the view event with zero items; it
34
- // shows above the fold, no viewport gating needed.
31
+ // Empty results emit immediately without visibility tracking.
35
32
  analytics.trackViewSearchResults({ ...envelope, items: [] });
36
33
  }
37
34
  },
@@ -43,7 +40,7 @@ export function createControllerAnalytics(analytics) {
43
40
  return;
44
41
  }
45
42
  const item = resultItem(result, index);
46
- // The click forces the item's impression so CTR by position stays ≤100%.
43
+ // Count the impression before recording the selection.
47
44
  tracker().forceImpression(item);
48
45
  analytics.trackSelectSearchResult({ ...envelope, items: [item] });
49
46
  },
@@ -1,6 +1,6 @@
1
1
  import { SearchAnalyticsEnvelope, SearchResultItem, ViewSearchResultsParams } from "../types/searchAnalytics";
2
2
  export interface SearchImpressionTrackerOptions {
3
- /** Receives each flushed batch wire it to `dialog.trackViewSearchResults`. */
3
+ /** Receive each batch, typically through `dialog.trackViewSearchResults`. */
4
4
  emit: (params: ViewSearchResultsParams) => void;
5
5
  visibilityThreshold?: number;
6
6
  dwellMs?: number;
@@ -8,11 +8,11 @@ export interface SearchImpressionTrackerOptions {
8
8
  maxItemsPerEvent?: number;
9
9
  }
10
10
  export interface SearchImpressionTracker {
11
- /** Declare the envelope of the response being rendered; flushes the previous batch. */
11
+ /** Flush the previous batch and start tracking the new response. */
12
12
  setContext(envelope: SearchAnalyticsEnvelope): void;
13
- /** Watch a rendered result element for viewport impressions. */
13
+ /** Track viewport impressions for a result element. */
14
14
  observe(element: Element, item: SearchResultItem): void;
15
- /** A click counts as an impression, otherwise CTR by position can exceed 100%. */
15
+ /** Record and flush an impression without waiting for visibility. */
16
16
  forceImpression(item: SearchResultItem): void;
17
17
  flush(): void;
18
18
  disconnect(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"searchImpressions.d.ts","sourceRoot":"","sources":["../../src/utils/searchImpressions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAWlC,MAAM,WAAW,8BAA8B;IAC7C,gFAAgF;IAChF,IAAI,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,uFAAuF;IACvF,UAAU,CAAC,QAAQ,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACpD,gEAAgE;IAChE,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxD,kFAAkF;IAClF,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC9C,KAAK,IAAI,IAAI,CAAC;IACd,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,IAAI,EACJ,mBAAkD,EAClD,OAA0B,EAC1B,WAAmC,EACnC,gBAA8C,GAC/C,EAAE,8BAA8B,GAAG,uBAAuB,CA+H1D"}
1
+ {"version":3,"file":"searchImpressions.d.ts","sourceRoot":"","sources":["../../src/utils/searchImpressions.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACxB,MAAM,0BAA0B,CAAC;AAQlC,MAAM,WAAW,8BAA8B;IAC7C,6EAA6E;IAC7E,IAAI,EAAE,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,CAAC;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,UAAU,CAAC,QAAQ,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACpD,uDAAuD;IACvD,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACxD,qEAAqE;IACrE,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAC9C,KAAK,IAAI,IAAI,CAAC;IACd,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,IAAI,EACJ,mBAAkD,EAClD,OAA0B,EAC1B,WAAmC,EACnC,gBAA8C,GAC/C,EAAE,8BAA8B,GAAG,uBAAuB,CA2H1D"}
@@ -1,7 +1,4 @@
1
- // An item counts as seen after being ≥50% visible for ≥500ms; qualified items
2
- // are batched and flushed on qualification inactivity, context change,
3
- // pagehide or the batch cap — in practice 1-3 events per search, not one per
4
- // item.
1
+ // Defaults: 50% visibility for 500ms. Flush on idle, context change, pagehide or batch limit.
5
2
  const DEFAULT_VISIBILITY_THRESHOLD = 0.5;
6
3
  const DEFAULT_DWELL_MS = 500;
7
4
  const DEFAULT_IDLE_FLUSH_MS = 1500;
@@ -72,15 +69,11 @@ export function createSearchImpressionTracker({ emit, visibilityThreshold = DEFA
72
69
  dwellTimers.clear();
73
70
  observedItems.clear();
74
71
  };
75
- // The page can die mid-batch: ship what already qualified. The host bridge
76
- // captures this last event with a beacon-capable transport.
72
+ // Flush qualified impressions before leaving the page.
77
73
  const handlePagehide = () => {
78
74
  flush();
79
75
  };
80
- // A back/forward-cache restore is a new exposure: re-qualify what is on
81
- // screen instead of keeping it deduplicated forever. Dwell timers frozen
82
- // mid-count resume with their remaining delay, so cancel them — the
83
- // restored exposure must earn its full dwell.
76
+ // After a back/forward-cache restore, reset deduplication and restart dwell timers.
84
77
  const handlePageshow = (event) => {
85
78
  if (!event.persisted) {
86
79
  return;
@@ -96,7 +89,7 @@ export function createSearchImpressionTracker({ emit, visibilityThreshold = DEFA
96
89
  window.addEventListener("pageshow", handlePageshow);
97
90
  return {
98
91
  setContext(nextEnvelope) {
99
- // Pending impressions belong to the envelope they qualified under.
92
+ // Flush under the previous context before replacing it.
100
93
  flush();
101
94
  seen.clear();
102
95
  envelope = nextEnvelope;
@@ -0,0 +1,17 @@
1
+ import { SearchIndex, SearchRequest, SearchResponse, SearchResult } from "../types/search";
2
+ import { SearchSection } from "../types/searchController";
3
+ export declare const normalizeQuery: (rawQuery: string) => string | undefined;
4
+ interface SearchRequestConfig {
5
+ indexName: string;
6
+ language: string;
7
+ currency: string;
8
+ hitsPerPage: number;
9
+ }
10
+ interface SearchResults {
11
+ response: SearchResult;
12
+ sections: Partial<Record<SearchIndex, SearchResult>> | undefined;
13
+ }
14
+ export declare const buildSearchRequest: (query: string, page: number, sections: readonly SearchSection[], { indexName, language, currency, hitsPerPage }: SearchRequestConfig) => SearchRequest;
15
+ export declare const readSearchResults: (result: SearchResponse, requested: readonly SearchSection[], { indexName, language, currency }: SearchRequestConfig) => SearchResults;
16
+ export {};
17
+ //# sourceMappingURL=searchRequests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchRequests.d.ts","sourceRoot":"","sources":["../../src/utils/searchRequests.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,aAAa,EACb,cAAc,EACd,YAAY,EACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAK1D,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,MAAM,GAAG,SAI1D,CAAC;AAEF,UAAU,mBAAmB;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,YAAY,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,GAAG,SAAS,CAAC;CAClE;AAED,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,EACb,MAAM,MAAM,EACZ,UAAU,SAAS,aAAa,EAAE,EAClC,gDAAgD,mBAAmB,KAClE,aAUD,CAAC;AAEH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,cAAc,EACtB,WAAW,SAAS,aAAa,EAAE,EACnC,mCAAmC,mBAAmB,KACrD,aAwBF,CAAC"}
@@ -0,0 +1,36 @@
1
+ import { searchIndexName } from "../services/search";
2
+ // Match the API minimum query length.
3
+ const MIN_QUERY_CODE_POINTS = 2;
4
+ export const normalizeQuery = (rawQuery) => {
5
+ const query = rawQuery.trim();
6
+ return [...query].length >= MIN_QUERY_CODE_POINTS ? query : undefined;
7
+ };
8
+ export const buildSearchRequest = (query, page, sections, { indexName, language, currency, hitsPerPage }) => ({
9
+ requests: [
10
+ { indexName, query, page, hitsPerPage },
11
+ ...sections.map((section) => ({
12
+ indexName: searchIndexName(section.index, language, currency),
13
+ query,
14
+ page: 0,
15
+ hitsPerPage: section.hitsPerPage ?? hitsPerPage,
16
+ })),
17
+ ],
18
+ });
19
+ export const readSearchResults = (result, requested, { indexName, language, currency }) => {
20
+ const response = result.results.find((entry) => entry.index === indexName);
21
+ if (response === undefined) {
22
+ throw new Error(`Dialog search returned no ${indexName} entry`);
23
+ }
24
+ const sections = {};
25
+ for (const section of requested) {
26
+ const requestedIndexName = searchIndexName(section.index, language, currency);
27
+ const entry = result.results.find((candidate) => candidate.index === requestedIndexName);
28
+ if (entry !== undefined) {
29
+ sections[section.index] = entry;
30
+ }
31
+ }
32
+ return {
33
+ response,
34
+ sections: requested.length === 0 ? undefined : sections,
35
+ };
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askdialog/dialog-sdk",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "private": false,
5
5
  "description": "Dialog SDK",
6
6
  "main": "dist/index.js",