@asstio-dev/storefront 0.19.0 → 0.20.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 CHANGED
@@ -243,6 +243,28 @@ config. Holes in the data never throw: a missing image, description or price onl
243
243
 
244
244
  hreflang sets are only as reciprocal as the API's `alternateUrls`; the SDK renders what the API gives.
245
245
 
246
+ ### Search events
247
+
248
+ `trackSearchEvents(client, events, { consent })` posts search exposures, product clicks and add-to-carts to
249
+ `POST /v1/events`, for measuring search quality. It sends **nothing** unless `consent === true`: the storefront stores
250
+ no PII and cannot verify consent, so the frontend decides, and passes `sessionId` (opaque, frontend-generated) only
251
+ under the same consent.
252
+
253
+ ```ts
254
+ const exposure = { type: "search_exposure", occurredAt: new Date().toISOString(), resultId: listing.search!.resultId, query, page: 1, productIds };
255
+ await trackSearchEvents(client, [exposure], { consent: shopperAcceptedMeasurement });
256
+ await trackSearchEvents(client, [{ type: "product_click", occurredAt: new Date().toISOString(), exposureId: exposure.eventId, productId, position: 3 }], { consent });
257
+ ```
258
+
259
+ - An event without `eventId` gets a UUID **written into the object**. Retry the same objects after an uncertain
260
+ answer (a timeout, a dropped connection) and the ids are reused, so the server counts the repeats as `duplicates`.
261
+ A second click from the same exposure is a new event: build a new object.
262
+ - `occurredAt` is UTC ISO-8601 (`new Date().toISOString()`), within 7 days back and 5 minutes ahead.
263
+ - At most 50 events per call (a larger batch throws `RangeError`), 64 KB per body. The answer lists `rejected`
264
+ events with their `index` and `member`; the others are kept. Any non-2xx answer throws: a malformed envelope (a
265
+ `400` problem) and a bare `5xx` alike, so a failed send is never mistaken for "no consent" (`null`).
266
+ - Exposures come only from the client; a catalog GET is never one.
267
+
246
268
  ### Product facts and GA4
247
269
 
248
270
  Cards and cart lines carry the product facts the product page has: `brand`, `categories`, `shortDescription` and
@@ -0,0 +1,20 @@
1
+ import type { Client } from "openapi-fetch";
2
+ import type { components } from "../schema";
3
+ import type { StorefrontPaths } from "../index.js";
4
+ type Schemas = components["schemas"];
5
+ /** One search event. `eventId` may be left out: `trackSearchEvents` writes a UUID into the object. */
6
+ export type SearchEvent = Omit<Schemas["SearchEventDto"], "eventId"> & {
7
+ eventId?: string | null;
8
+ };
9
+ export type SearchEventsResponse = Schemas["SearchEventsResponse"];
10
+ /**
11
+ * `POST /v1/events` (spec 2026-10-13 §9.2). A no-op unless `consent` is exactly `true`: the storefront cannot verify
12
+ * consent, so the caller decides (and sends `sessionId` only under the same consent). An event without `eventId` gets
13
+ * a UUID written into the object, so retrying the same objects after an uncertain response reuses the ids and the
14
+ * server counts them as duplicates. Throws a `RangeError` for more than 50 events (split the batch) and an `Error` on any
15
+ * non-2xx answer (a problem body or none). Build `occurredAt` with `new Date().toISOString()`: the server wants UTC.
16
+ */
17
+ export declare function trackSearchEvents(client: Client<StorefrontPaths>, events: SearchEvent[], options: {
18
+ consent: boolean;
19
+ }): Promise<SearchEventsResponse | null>;
20
+ export {};
@@ -0,0 +1,23 @@
1
+ /** The server's per-envelope cap (spec 2026-10-13 §9.2); a larger batch is a `400` that stores nothing. */
2
+ const MAX_EVENTS_PER_REQUEST = 50;
3
+ /**
4
+ * `POST /v1/events` (spec 2026-10-13 §9.2). A no-op unless `consent` is exactly `true`: the storefront cannot verify
5
+ * consent, so the caller decides (and sends `sessionId` only under the same consent). An event without `eventId` gets
6
+ * a UUID written into the object, so retrying the same objects after an uncertain response reuses the ids and the
7
+ * server counts them as duplicates. Throws a `RangeError` for more than 50 events (split the batch) and an `Error` on any
8
+ * non-2xx answer (a problem body or none). Build `occurredAt` with `new Date().toISOString()`: the server wants UTC.
9
+ */
10
+ export async function trackSearchEvents(client, events, options) {
11
+ if (options?.consent !== true || events.length === 0)
12
+ return null;
13
+ if (events.length > MAX_EVENTS_PER_REQUEST)
14
+ throw new RangeError(`trackSearchEvents takes at most ${MAX_EVENTS_PER_REQUEST} events per call, got ${events.length}`);
15
+ for (const e of events)
16
+ if (!e.eventId)
17
+ e.eventId = crypto.randomUUID();
18
+ const { data, error, response } = await client.POST("/v1/events", { body: { events: events } });
19
+ // `error` is undefined for an empty-body 5xx (openapi-fetch parses nothing), so the status decides: a non-2xx always throws.
20
+ if (!response.ok)
21
+ throw new Error(`trackSearchEvents failed: ${response.status} ${error === undefined ? "" : JSON.stringify(error)}`.trimEnd());
22
+ return data ?? null;
23
+ }
package/dist/index.d.ts CHANGED
@@ -58,5 +58,6 @@ export { productJsonLd } from "./seo/product.js";
58
58
  export { jsonLdScript } from "./seo/serialize.js";
59
59
  export { productFacts } from "./analytics/facts.js";
60
60
  export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
61
+ export { trackSearchEvents, type SearchEvent, type SearchEventsResponse } from "./analytics/events.js";
61
62
  export { AnalyticsError, type Ga4Ecommerce, type Ga4Item, type Ga4ItemOptions, type Ga4ItemSource, type Ga4Purchase, type Ga4SharedOptions, type ProductFacts, type SelectedVariant, } from "./analytics/types.js";
62
63
  export { SeoError, type BreadcrumbLink, type CollectionHeadOptions, type HeadOptions, type JsonLd, type NextMetadataLike, type ProductJsonLdOptions, type SchemaProperty, type SeoContext, type SeoHead, type SeoMeta, type VariantAvailability, } from "./seo/types.js";
package/dist/index.js CHANGED
@@ -47,5 +47,6 @@ export { productJsonLd } from "./seo/product.js";
47
47
  export { jsonLdScript } from "./seo/serialize.js";
48
48
  export { productFacts } from "./analytics/facts.js";
49
49
  export { toGa4Ecommerce, toGa4Item } from "./analytics/ga4.js";
50
+ export { trackSearchEvents } from "./analytics/events.js";
50
51
  export { AnalyticsError, } from "./analytics/types.js";
51
52
  export { SeoError, } from "./seo/types.js";
package/dist/schema.d.ts CHANGED
@@ -249,6 +249,23 @@ export interface paths {
249
249
  patch?: never;
250
250
  trace?: never;
251
251
  };
252
+ "/v1/events": {
253
+ parameters: {
254
+ query?: never;
255
+ header?: never;
256
+ path?: never;
257
+ cookie?: never;
258
+ };
259
+ get?: never;
260
+ put?: never;
261
+ /** Search exposures, clicks and add-to-carts for measurement (spec 2026-10-13 §9.2). Send only with the shopper's consent. Each event is validated on its own: a bad one is listed in rejected and the others are kept; an eventId the site already has is a duplicate and changes nothing. */
262
+ post: operations["TrackSearchEvents"];
263
+ delete?: never;
264
+ options?: never;
265
+ head?: never;
266
+ patch?: never;
267
+ trace?: never;
268
+ };
252
269
  "/v1/orders/{orderId}": {
253
270
  parameters: {
254
271
  query?: never;
@@ -422,6 +439,8 @@ export interface components {
422
439
  AppliedFilterDto: {
423
440
  facet: string;
424
441
  label: string;
442
+ /** @default filter */
443
+ source: string;
425
444
  value: string;
426
445
  };
427
446
  AttributeDto: {
@@ -752,6 +771,7 @@ export interface components {
752
771
  pageSize: number | string;
753
772
  queryCorrection: null | string;
754
773
  redirect: null | string;
774
+ search?: null | components["schemas"]["SearchInfoDto"];
755
775
  sort: components["schemas"]["SortDto"];
756
776
  /** Format: int64 */
757
777
  total: number | string;
@@ -1020,6 +1040,60 @@ export interface components {
1020
1040
  importer: null | components["schemas"]["PartyDto"];
1021
1041
  manufacturer: null | components["schemas"]["PartyDto"];
1022
1042
  };
1043
+ SearchEventDto: {
1044
+ eventId: null | string;
1045
+ exposureId?: null | string;
1046
+ /** Format: date-time */
1047
+ occurredAt: null | string;
1048
+ /** Format: int32 */
1049
+ page?: number | null;
1050
+ /** Format: int32 */
1051
+ position?: number | null;
1052
+ productId?: null | string;
1053
+ productIds?: string[] | null;
1054
+ /** Format: int32 */
1055
+ quantity?: number | null;
1056
+ query?: null | string;
1057
+ resultId?: null | string;
1058
+ sessionId?: null | string;
1059
+ type: null | string;
1060
+ variantId?: null | string;
1061
+ };
1062
+ SearchEventRejection: {
1063
+ code: string;
1064
+ /** Format: int32 */
1065
+ index: number | string;
1066
+ member: null | string;
1067
+ };
1068
+ SearchEventsRequest: {
1069
+ events: components["schemas"]["SearchEventDto"][] | null;
1070
+ };
1071
+ SearchEventsResponse: {
1072
+ /** Format: int32 */
1073
+ accepted: number | string;
1074
+ /** Format: int32 */
1075
+ duplicates: number | string;
1076
+ rejected: components["schemas"]["SearchEventRejection"][];
1077
+ /** Format: int32 */
1078
+ stored: number | string;
1079
+ };
1080
+ SearchInfoDto: {
1081
+ codeMatch: boolean;
1082
+ intents: components["schemas"]["SearchIntentDto"][];
1083
+ rescue: null | components["schemas"]["SearchRescueDto"];
1084
+ resultId: string;
1085
+ tier: string;
1086
+ };
1087
+ SearchIntentDto: {
1088
+ facet: string;
1089
+ queryWithout: string;
1090
+ values: string[];
1091
+ };
1092
+ SearchRescueDto: {
1093
+ collectionId: string;
1094
+ name: string;
1095
+ slug: string;
1096
+ };
1023
1097
  SellabilityDto: {
1024
1098
  mode: components["schemas"]["SiteSellabilityMode"];
1025
1099
  };
@@ -2612,7 +2686,7 @@ export interface operations {
2612
2686
  GetCollection: {
2613
2687
  parameters: {
2614
2688
  query?: {
2615
- /** @description Free-text search. Omit to browse rather than search. */
2689
+ /** @description Free-text search. Omit to browse rather than search; a q of separators only (such as !!!) browses too. A search answers with search (how it was answered), and may set queryCorrection and redirect. */
2616
2690
  q?: string;
2617
2691
  /** @description 1-based page. page × pageSize must not exceed 10000 (default pageSize 24, max 100). */
2618
2692
  page?: number;
@@ -2624,6 +2698,8 @@ export interface operations {
2624
2698
  filter?: {
2625
2699
  [key: string]: string;
2626
2700
  };
2701
+ /** @description Where a search came from, for measurement only: typed, suggestion, recent or other. It never changes the answer. Anything else is ignored with an ORIGIN_UNKNOWN warning. */
2702
+ origin?: "other" | "recent" | "suggestion" | "typed";
2627
2703
  };
2628
2704
  header: {
2629
2705
  /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
@@ -2788,6 +2864,91 @@ export interface operations {
2788
2864
  };
2789
2865
  };
2790
2866
  };
2867
+ TrackSearchEvents: {
2868
+ parameters: {
2869
+ query?: never;
2870
+ header: {
2871
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2872
+ "X-Site-Key": string;
2873
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2874
+ "X-Market"?: string;
2875
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2876
+ "Accept-Language"?: string;
2877
+ };
2878
+ path?: never;
2879
+ cookie?: never;
2880
+ };
2881
+ requestBody: {
2882
+ content: {
2883
+ "application/json": components["schemas"]["SearchEventsRequest"];
2884
+ };
2885
+ };
2886
+ responses: {
2887
+ /** @description Accepted */
2888
+ 202: {
2889
+ headers: {
2890
+ [name: string]: unknown;
2891
+ };
2892
+ content: {
2893
+ "application/json": components["schemas"]["SearchEventsResponse"];
2894
+ };
2895
+ };
2896
+ /** @description problem+json. EVENTS_INVALID (not JSON, not { "events": [ … ] }, no event or more than 50, or a body over 64 KB; nothing is stored), or MARKET_UNKNOWN. One bad event is not a 400: it is listed in the 202's rejected. */
2897
+ 400: {
2898
+ headers: {
2899
+ [name: string]: unknown;
2900
+ };
2901
+ content: {
2902
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2903
+ };
2904
+ };
2905
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2906
+ 401: {
2907
+ headers: {
2908
+ [name: string]: unknown;
2909
+ };
2910
+ content: {
2911
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2912
+ };
2913
+ };
2914
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2915
+ 403: {
2916
+ headers: {
2917
+ [name: string]: unknown;
2918
+ };
2919
+ content: {
2920
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2921
+ };
2922
+ };
2923
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2924
+ 404: {
2925
+ headers: {
2926
+ [name: string]: unknown;
2927
+ };
2928
+ content: {
2929
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2930
+ };
2931
+ };
2932
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2933
+ 429: {
2934
+ headers: {
2935
+ [name: string]: unknown;
2936
+ };
2937
+ content: {
2938
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2939
+ };
2940
+ };
2941
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2942
+ 503: {
2943
+ headers: {
2944
+ [name: string]: unknown;
2945
+ };
2946
+ content: {
2947
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2948
+ };
2949
+ };
2950
+ };
2951
+ };
2791
2952
  GetOrder: {
2792
2953
  parameters: {
2793
2954
  query?: never;
@@ -2878,7 +3039,7 @@ export interface operations {
2878
3039
  ListProducts: {
2879
3040
  parameters: {
2880
3041
  query?: {
2881
- /** @description Free-text search. Omit to browse rather than search. */
3042
+ /** @description Free-text search. Omit to browse rather than search; a q of separators only (such as !!!) browses too. A search answers with search (how it was answered), and may set queryCorrection and redirect. */
2882
3043
  q?: string;
2883
3044
  /** @description 1-based page. page × pageSize must not exceed 10000 (default pageSize 24, max 100). */
2884
3045
  page?: number;
@@ -2890,6 +3051,8 @@ export interface operations {
2890
3051
  filter?: {
2891
3052
  [key: string]: string;
2892
3053
  };
3054
+ /** @description Where a search came from, for measurement only: typed, suggestion, recent or other. It never changes the answer. Anything else is ignored with an ORIGIN_UNKNOWN warning. */
3055
+ origin?: "other" | "recent" | "suggestion" | "typed";
2893
3056
  /** @description Narrow to a collection by slug path or id. An unknown one is answered with a COLLECTION_UNKNOWN warning, not a 404. */
2894
3057
  collection?: string;
2895
3058
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asstio-dev/storefront",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Typed client for the Asstio Storefront API (generated from OpenAPI).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,7 +21,7 @@
21
21
  "test": "vitest run",
22
22
  "test:browser": "playwright test -c browser/playwright.config.ts",
23
23
  "check": "npm run generate && npm run typecheck-examples && npm test && git diff --exit-code -- src/schema.d.ts && test -z \"$(git status --porcelain -- src/schema.d.ts)\"",
24
- "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"liveShippingFor\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"gtinJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\", \\\"productFacts\\\", \\\"toGa4Item\\\", \\\"toGa4Ecommerce\\\", \\\"AnalyticsError\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
24
+ "smoke": "node --input-type=module -e \"import(\\\"./dist/index.js\\\").then(m => { for (const f of [\\\"createStorefrontClient\\\", \\\"filterQuery\\\", \\\"encodeFilterValues\\\", \\\"decodeFilterValues\\\", \\\"pictureSources\\\", \\\"mediaUrl\\\", \\\"formatImage\\\", \\\"styleToCss\\\", \\\"startCheckout\\\", \\\"syncCheckout\\\", \\\"completeCheckout\\\", \\\"getCheckoutStatus\\\", \\\"liveShippingFor\\\", \\\"getOrder\\\", \\\"awaitCompletion\\\", \\\"mountCheckout\\\", \\\"productJsonLd\\\", \\\"gtinJsonLd\\\", \\\"breadcrumbJsonLd\\\", \\\"productHead\\\", \\\"collectionHead\\\", \\\"brandHead\\\", \\\"toNextMetadata\\\", \\\"jsonLdScript\\\", \\\"SeoError\\\", \\\"productFacts\\\", \\\"toGa4Item\\\", \\\"toGa4Ecommerce\\\", \\\"AnalyticsError\\\", \\\"trackSearchEvents\\\"]) if (typeof m[f] !== \\\"function\\\") throw new Error(\\\"missing export \\\" + f) })\""
25
25
  },
26
26
  "dependencies": {
27
27
  "openapi-fetch": "0.17.0"
package/src/schema.d.ts CHANGED
@@ -249,6 +249,23 @@ export interface paths {
249
249
  patch?: never;
250
250
  trace?: never;
251
251
  };
252
+ "/v1/events": {
253
+ parameters: {
254
+ query?: never;
255
+ header?: never;
256
+ path?: never;
257
+ cookie?: never;
258
+ };
259
+ get?: never;
260
+ put?: never;
261
+ /** Search exposures, clicks and add-to-carts for measurement (spec 2026-10-13 §9.2). Send only with the shopper's consent. Each event is validated on its own: a bad one is listed in rejected and the others are kept; an eventId the site already has is a duplicate and changes nothing. */
262
+ post: operations["TrackSearchEvents"];
263
+ delete?: never;
264
+ options?: never;
265
+ head?: never;
266
+ patch?: never;
267
+ trace?: never;
268
+ };
252
269
  "/v1/orders/{orderId}": {
253
270
  parameters: {
254
271
  query?: never;
@@ -422,6 +439,8 @@ export interface components {
422
439
  AppliedFilterDto: {
423
440
  facet: string;
424
441
  label: string;
442
+ /** @default filter */
443
+ source: string;
425
444
  value: string;
426
445
  };
427
446
  AttributeDto: {
@@ -752,6 +771,7 @@ export interface components {
752
771
  pageSize: number | string;
753
772
  queryCorrection: null | string;
754
773
  redirect: null | string;
774
+ search?: null | components["schemas"]["SearchInfoDto"];
755
775
  sort: components["schemas"]["SortDto"];
756
776
  /** Format: int64 */
757
777
  total: number | string;
@@ -1020,6 +1040,60 @@ export interface components {
1020
1040
  importer: null | components["schemas"]["PartyDto"];
1021
1041
  manufacturer: null | components["schemas"]["PartyDto"];
1022
1042
  };
1043
+ SearchEventDto: {
1044
+ eventId: null | string;
1045
+ exposureId?: null | string;
1046
+ /** Format: date-time */
1047
+ occurredAt: null | string;
1048
+ /** Format: int32 */
1049
+ page?: number | null;
1050
+ /** Format: int32 */
1051
+ position?: number | null;
1052
+ productId?: null | string;
1053
+ productIds?: string[] | null;
1054
+ /** Format: int32 */
1055
+ quantity?: number | null;
1056
+ query?: null | string;
1057
+ resultId?: null | string;
1058
+ sessionId?: null | string;
1059
+ type: null | string;
1060
+ variantId?: null | string;
1061
+ };
1062
+ SearchEventRejection: {
1063
+ code: string;
1064
+ /** Format: int32 */
1065
+ index: number | string;
1066
+ member: null | string;
1067
+ };
1068
+ SearchEventsRequest: {
1069
+ events: components["schemas"]["SearchEventDto"][] | null;
1070
+ };
1071
+ SearchEventsResponse: {
1072
+ /** Format: int32 */
1073
+ accepted: number | string;
1074
+ /** Format: int32 */
1075
+ duplicates: number | string;
1076
+ rejected: components["schemas"]["SearchEventRejection"][];
1077
+ /** Format: int32 */
1078
+ stored: number | string;
1079
+ };
1080
+ SearchInfoDto: {
1081
+ codeMatch: boolean;
1082
+ intents: components["schemas"]["SearchIntentDto"][];
1083
+ rescue: null | components["schemas"]["SearchRescueDto"];
1084
+ resultId: string;
1085
+ tier: string;
1086
+ };
1087
+ SearchIntentDto: {
1088
+ facet: string;
1089
+ queryWithout: string;
1090
+ values: string[];
1091
+ };
1092
+ SearchRescueDto: {
1093
+ collectionId: string;
1094
+ name: string;
1095
+ slug: string;
1096
+ };
1023
1097
  SellabilityDto: {
1024
1098
  mode: components["schemas"]["SiteSellabilityMode"];
1025
1099
  };
@@ -2612,7 +2686,7 @@ export interface operations {
2612
2686
  GetCollection: {
2613
2687
  parameters: {
2614
2688
  query?: {
2615
- /** @description Free-text search. Omit to browse rather than search. */
2689
+ /** @description Free-text search. Omit to browse rather than search; a q of separators only (such as !!!) browses too. A search answers with search (how it was answered), and may set queryCorrection and redirect. */
2616
2690
  q?: string;
2617
2691
  /** @description 1-based page. page × pageSize must not exceed 10000 (default pageSize 24, max 100). */
2618
2692
  page?: number;
@@ -2624,6 +2698,8 @@ export interface operations {
2624
2698
  filter?: {
2625
2699
  [key: string]: string;
2626
2700
  };
2701
+ /** @description Where a search came from, for measurement only: typed, suggestion, recent or other. It never changes the answer. Anything else is ignored with an ORIGIN_UNKNOWN warning. */
2702
+ origin?: "other" | "recent" | "suggestion" | "typed";
2627
2703
  };
2628
2704
  header: {
2629
2705
  /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
@@ -2788,6 +2864,91 @@ export interface operations {
2788
2864
  };
2789
2865
  };
2790
2866
  };
2867
+ TrackSearchEvents: {
2868
+ parameters: {
2869
+ query?: never;
2870
+ header: {
2871
+ /** @description Publishable, preview or secret site key. Required on every request; secret keys are server-side only. */
2872
+ "X-Site-Key": string;
2873
+ /** @description Market code (e.g. "se"). Omit for the site's default market. */
2874
+ "X-Market"?: string;
2875
+ /** @description BCP-47 language tag. Omit to use the market's language. */
2876
+ "Accept-Language"?: string;
2877
+ };
2878
+ path?: never;
2879
+ cookie?: never;
2880
+ };
2881
+ requestBody: {
2882
+ content: {
2883
+ "application/json": components["schemas"]["SearchEventsRequest"];
2884
+ };
2885
+ };
2886
+ responses: {
2887
+ /** @description Accepted */
2888
+ 202: {
2889
+ headers: {
2890
+ [name: string]: unknown;
2891
+ };
2892
+ content: {
2893
+ "application/json": components["schemas"]["SearchEventsResponse"];
2894
+ };
2895
+ };
2896
+ /** @description problem+json. EVENTS_INVALID (not JSON, not { "events": [ … ] }, no event or more than 50, or a body over 64 KB; nothing is stored), or MARKET_UNKNOWN. One bad event is not a 400: it is listed in the 202's rejected. */
2897
+ 400: {
2898
+ headers: {
2899
+ [name: string]: unknown;
2900
+ };
2901
+ content: {
2902
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2903
+ };
2904
+ };
2905
+ /** @description problem+json. SITE_KEY_MISSING or SITE_KEY_INVALID. */
2906
+ 401: {
2907
+ headers: {
2908
+ [name: string]: unknown;
2909
+ };
2910
+ content: {
2911
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2912
+ };
2913
+ };
2914
+ /** @description problem+json. SECRET_KEY_FROM_BROWSER: a secret key was sent from a browser. */
2915
+ 403: {
2916
+ headers: {
2917
+ [name: string]: unknown;
2918
+ };
2919
+ content: {
2920
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2921
+ };
2922
+ };
2923
+ /** @description problem+json. SITE_INACTIVE, or NOT_FOUND for an unrouted path. */
2924
+ 404: {
2925
+ headers: {
2926
+ [name: string]: unknown;
2927
+ };
2928
+ content: {
2929
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2930
+ };
2931
+ };
2932
+ /** @description problem+json. RATE_LIMITED; retryAfterSeconds and the Retry-After header say how long to wait. */
2933
+ 429: {
2934
+ headers: {
2935
+ [name: string]: unknown;
2936
+ };
2937
+ content: {
2938
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2939
+ };
2940
+ };
2941
+ /** @description problem+json. SITE_REGISTRY_WARMING or INDEX_UNAVAILABLE — retry, the request was not wrong. */
2942
+ 503: {
2943
+ headers: {
2944
+ [name: string]: unknown;
2945
+ };
2946
+ content: {
2947
+ "application/problem+json": components["schemas"]["StorefrontProblem"];
2948
+ };
2949
+ };
2950
+ };
2951
+ };
2791
2952
  GetOrder: {
2792
2953
  parameters: {
2793
2954
  query?: never;
@@ -2878,7 +3039,7 @@ export interface operations {
2878
3039
  ListProducts: {
2879
3040
  parameters: {
2880
3041
  query?: {
2881
- /** @description Free-text search. Omit to browse rather than search. */
3042
+ /** @description Free-text search. Omit to browse rather than search; a q of separators only (such as !!!) browses too. A search answers with search (how it was answered), and may set queryCorrection and redirect. */
2882
3043
  q?: string;
2883
3044
  /** @description 1-based page. page × pageSize must not exceed 10000 (default pageSize 24, max 100). */
2884
3045
  page?: number;
@@ -2890,6 +3051,8 @@ export interface operations {
2890
3051
  filter?: {
2891
3052
  [key: string]: string;
2892
3053
  };
3054
+ /** @description Where a search came from, for measurement only: typed, suggestion, recent or other. It never changes the answer. Anything else is ignored with an ORIGIN_UNKNOWN warning. */
3055
+ origin?: "other" | "recent" | "suggestion" | "typed";
2893
3056
  /** @description Narrow to a collection by slug path or id. An unknown one is answered with a COLLECTION_UNKNOWN warning, not a 404. */
2894
3057
  collection?: string;
2895
3058
  };