@ekomerc/storefront 0.1.0 → 0.1.3
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 +237 -10
- package/dist/README.md +679 -0
- package/dist/errors.d.cts +74 -0
- package/dist/errors.d.ts +74 -80
- package/dist/index.cjs +2840 -1810
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1493 -0
- package/dist/index.d.ts +1493 -952
- package/dist/index.js +2841 -1811
- package/dist/index.js.map +1 -1
- package/dist/package.json +45 -0
- package/dist/types.d.cts +656 -0
- package/dist/types.d.ts +656 -1
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -42,9 +42,89 @@ interface StorefrontClientConfig {
|
|
|
42
42
|
apiKey: string; // API key (sfk_...)
|
|
43
43
|
storage?: StorageAdapter; // Cart token storage (default: auto-detect)
|
|
44
44
|
cacheTTL?: number; // Query cache TTL in ms (default: 5 minutes)
|
|
45
|
+
trackingAttributionTTL?: number; // Landing attribution TTL in ms (default: 30 days)
|
|
46
|
+
tracking?: StorefrontTrackingRuntimeConfig; // Unified tracking runtime config
|
|
45
47
|
}
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
### Tracking Runtime Configuration
|
|
51
|
+
|
|
52
|
+
`tracking` wires up the unified tracking runtime — consent signaling, GTM adapter dispatch, and optional diagnostics.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
interface StorefrontTrackingRuntimeConfig {
|
|
56
|
+
// Controls whether browser adapters may dispatch when consent is "unknown".
|
|
57
|
+
// Defaults to true when not set.
|
|
58
|
+
dispatchOnUnknownConsent?: boolean;
|
|
59
|
+
|
|
60
|
+
// Called before every event to get the current consent state.
|
|
61
|
+
// Return "granted", "denied", or "unknown". Defaults to "unknown".
|
|
62
|
+
resolveConsentState?: () => AnalyticsConsentState | Promise<AnalyticsConsentState>;
|
|
63
|
+
|
|
64
|
+
// Browser adapters to forward events to (GTM-only supported path in v1).
|
|
65
|
+
adapters?: AnalyticsBrowserAdapter[];
|
|
66
|
+
|
|
67
|
+
// Optional diagnostic observer — called after every ingest/adapter/consent operation.
|
|
68
|
+
// Never throws; errors inside are swallowed.
|
|
69
|
+
onDiagnostic?: (diagnostic: AnalyticsDiagnostic) => void;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Consent states:** `"granted"` | `"denied"` | `"unknown"`
|
|
74
|
+
|
|
75
|
+
- `dispatchOnUnknownConsent: false`: events forwarded only when consent is `"granted"`.
|
|
76
|
+
- `dispatchOnUnknownConsent: true` (default): events forwarded unless consent is `"denied"`.
|
|
77
|
+
|
|
78
|
+
Example with a consent management platform:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const client = createStorefrontClient({
|
|
82
|
+
endpoint: "https://api.yourstore.com/graphql",
|
|
83
|
+
apiKey: "sfk_...",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await client.init({
|
|
87
|
+
analytics: {
|
|
88
|
+
dispatchOnUnknownConsent: true,
|
|
89
|
+
resolveConsentState: () => myCmp.getConsentState(), // "granted" | "denied" | "unknown"
|
|
90
|
+
adapters: [gtmAdapter].filter(Boolean),
|
|
91
|
+
onDiagnostic: (d) => console.debug("[tracking]", d),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The SDK persists landing attribution (`utm_*`, `gclid`, `gbraid`, `wbraid`, `fbclid`, `ttclid`) in `localStorage` under `ekomerc_tracking_attribution` and expires it on read. Fresh URL params always overwrite the stored snapshot.
|
|
97
|
+
|
|
98
|
+
Call `client.init()` once on storefront bootstrap. It loads the store analytics config from the storefront API, captures landing attribution, and removes known attribution params from the address bar by default after capture. If you need to keep them in the URL, pass:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
await client.init({
|
|
102
|
+
analytics: {
|
|
103
|
+
stripUrl: false,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### GTM-First Browser Model (v1)
|
|
109
|
+
|
|
110
|
+
- SDK remains the single browser event producer.
|
|
111
|
+
- First-party ingest (`/analytics/ingest`) remains authoritative for Ekomerc analytics/audit.
|
|
112
|
+
- Browser-side vendor routing is GTM-only in this phase.
|
|
113
|
+
- Backend provider forwarding is env-gated and defaults to disabled (`TRACKING_PROVIDER_FORWARDING_ENABLED=false`).
|
|
114
|
+
- Hosted/default storefront GTM support is deferred (follow-up TODO).
|
|
115
|
+
|
|
116
|
+
Custom storefront setup in this phase:
|
|
117
|
+
|
|
118
|
+
1. Install the GTM snippet in storefront code (merchant-owned, outside SDK).
|
|
119
|
+
2. Import `packages/sdk/storefront/src/gtm-recipes/ekomerc-gtm-browser-recipe-v1.export-v2.json` into GTM.
|
|
120
|
+
3. Fill provider IDs/settings in GTM and publish manually.
|
|
121
|
+
|
|
122
|
+
Anti-duplication checklist (required):
|
|
123
|
+
|
|
124
|
+
1. Disable GA4 Enhanced Measurement in the GA4 data stream used by this GTM setup.
|
|
125
|
+
2. Disable Meta Automatic Event Setup in Meta Events Manager for the pixel used by this setup.
|
|
126
|
+
3. Disable TikTok automatic event setup or equivalent automatic browser-event features for this setup.
|
|
127
|
+
|
|
48
128
|
### Custom Endpoint URLs
|
|
49
129
|
|
|
50
130
|
`endpoint` is used exactly as provided. The SDK sends `POST` requests directly to that URL and does not rewrite hosts or paths.
|
|
@@ -123,15 +203,31 @@ const result = await client.products.list({
|
|
|
123
203
|
});
|
|
124
204
|
// Returns: { items: Product[], pageInfo: PageInfo }
|
|
125
205
|
|
|
126
|
-
// Get single product by handle or
|
|
206
|
+
// Get single product by handle or GID
|
|
127
207
|
const result = await client.products.get("blue-shirt");
|
|
128
|
-
const result = await client.products.get("UHJvZHVjdDoxMjM="); //
|
|
208
|
+
const result = await client.products.get("UHJvZHVjdDoxMjM="); // GID (global ID)
|
|
129
209
|
|
|
130
210
|
// Batch fetch by handles
|
|
131
211
|
const result = await client.products.getByHandles(["shirt-1", "shirt-2"]);
|
|
132
212
|
// Returns: (Product | null)[]
|
|
133
213
|
```
|
|
134
214
|
|
|
215
|
+
Each `Product` includes a `detailSections: DetailSection[]` field containing structured sections for use in product detail pages. Each section has a `sectionType` (`"RICH_TEXT"` | `"BULLET_LIST"` | `"TABLE"`), `displayIntent` (`"ACCORDION"` | `"SPECIFICATIONS"` | `"BOTH"`), `title`, `position`, and type-specific `content`.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
if (result.isOk()) {
|
|
219
|
+
for (const section of result.value.detailSections) {
|
|
220
|
+
if (section.sectionType === "RICH_TEXT") {
|
|
221
|
+
console.log(section.content.html);
|
|
222
|
+
} else if (section.sectionType === "BULLET_LIST") {
|
|
223
|
+
console.log(section.content.items);
|
|
224
|
+
} else if (section.sectionType === "TABLE") {
|
|
225
|
+
console.log(section.content.rows); // [string, string][]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
135
231
|
### Collections
|
|
136
232
|
|
|
137
233
|
```typescript
|
|
@@ -166,17 +262,17 @@ const result = await client.cart.create();
|
|
|
166
262
|
// Returns: Cart
|
|
167
263
|
|
|
168
264
|
// Add item to cart
|
|
169
|
-
// variantId must be a
|
|
265
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
170
266
|
const result = await client.cart.addItem(variantId, quantity);
|
|
171
267
|
// Returns: Cart
|
|
172
268
|
|
|
173
269
|
// Update item quantity
|
|
174
|
-
// variantId must be a
|
|
270
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
175
271
|
const result = await client.cart.updateItem(variantId, newQuantity);
|
|
176
272
|
// Returns: Cart
|
|
177
273
|
|
|
178
274
|
// Remove item from cart
|
|
179
|
-
// variantId must be a
|
|
275
|
+
// variantId must be a GID (global ID) — use variant.id from product queries
|
|
180
276
|
const result = await client.cart.removeItem(variantId);
|
|
181
277
|
// Returns: Cart
|
|
182
278
|
|
|
@@ -196,7 +292,7 @@ const result = await client.cart.removePromoCode();
|
|
|
196
292
|
// Returns: Cart (with appliedPromoCode cleared)
|
|
197
293
|
```
|
|
198
294
|
|
|
199
|
-
Note: `variantId` in `cart.addItem`, `cart.updateItem`, and `cart.removeItem` must be a
|
|
295
|
+
Note: `variantId` in `cart.addItem`, `cart.updateItem`, and `cart.removeItem` must be a GID (global ID), not a raw UUID. Use variant IDs returned by product queries.
|
|
200
296
|
|
|
201
297
|
### Pricing Display
|
|
202
298
|
|
|
@@ -276,6 +372,44 @@ const result = await client.checkout.abandon();
|
|
|
276
372
|
// Returns: Cart
|
|
277
373
|
```
|
|
278
374
|
|
|
375
|
+
### Analytics
|
|
376
|
+
|
|
377
|
+
`client.analytics.track()` sends a first-party event to the storefront ingest endpoint and, when GTM adapter is configured, mirrors supported events to GTM `dataLayer`.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
// Page view
|
|
381
|
+
await client.analytics.track("page_view");
|
|
382
|
+
|
|
383
|
+
// Product view (productId can be a raw UUID, GID, or base64-encoded GID)
|
|
384
|
+
await client.analytics.track("product_view", { productId: product.id, variantId: variant.id });
|
|
385
|
+
|
|
386
|
+
// Collection view
|
|
387
|
+
await client.analytics.track("collection_view", { collectionId: collection.id });
|
|
388
|
+
|
|
389
|
+
// Search
|
|
390
|
+
await client.analytics.track("search_performed", { query: "shirt", resultsCount: 42 });
|
|
391
|
+
|
|
392
|
+
// Cart events
|
|
393
|
+
await client.analytics.track("add_to_cart", { cartId: cart.id, quantity: 1, itemsCount: 3, cartValue: 29.99 });
|
|
394
|
+
await client.analytics.track("remove_from_cart", { cartId: cart.id, quantity: 1, itemsCount: 2, cartValue: 19.99 });
|
|
395
|
+
|
|
396
|
+
// Checkout funnel
|
|
397
|
+
await client.analytics.track("checkout_started", { cartId: cart.id });
|
|
398
|
+
await client.analytics.track("checkout_step_completed", { cartId: cart.id, step: "contact" }); // "contact" | "shipping" | "payment" | "review"
|
|
399
|
+
await client.analytics.track("checkout_completed", { orderId: order.id, cartId: cart.id, orderTotal: 49.99 });
|
|
400
|
+
|
|
401
|
+
// Custom event — sent as analytics.custom with eventName in properties
|
|
402
|
+
await client.analytics.track("homepage.banner_clicked", { position: 0 });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
All preset events also accept an optional `AnalyticsContext` as the last argument to override path, referrer, UTM params, consent state, or visitor/session IDs.
|
|
406
|
+
|
|
407
|
+
Important purchase behavior:
|
|
408
|
+
|
|
409
|
+
- `track("checkout_completed", ...)` is ingest-only.
|
|
410
|
+
- Browser purchase event (`ekomerc.purchase`) is emitted only on successful `checkout.complete()` using server-confirmed `purchaseTracking`.
|
|
411
|
+
- `checkout.complete()` API remains `Result<Order, StorefrontError>` (non-breaking).
|
|
412
|
+
|
|
279
413
|
### Cache
|
|
280
414
|
|
|
281
415
|
```typescript
|
|
@@ -296,6 +430,81 @@ client.setCartToken("cart-token-uuid");
|
|
|
296
430
|
client.clearCartToken();
|
|
297
431
|
```
|
|
298
432
|
|
|
433
|
+
### Store
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const result = await client.store.get();
|
|
437
|
+
if (result.isOk()) {
|
|
438
|
+
const store = result.value;
|
|
439
|
+
// store.analytics — derived analytics availability and GTM config for this store
|
|
440
|
+
console.log(store.analytics);
|
|
441
|
+
// {
|
|
442
|
+
// enabled: true,
|
|
443
|
+
// dispatchOnUnknownConsent: true,
|
|
444
|
+
// gtm: { enabled: true, containerId: "GTM-XXXXXX" },
|
|
445
|
+
// }
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Use `store.analytics` to decide whether GTM forwarding should be active at runtime:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import {
|
|
453
|
+
createGtmBrowserAdapter,
|
|
454
|
+
} from "@ekomerc/storefront";
|
|
455
|
+
|
|
456
|
+
const storeResult = await client.store.get();
|
|
457
|
+
if (storeResult.isOk()) {
|
|
458
|
+
const { analytics } = storeResult.value;
|
|
459
|
+
const adapters = [
|
|
460
|
+
createGtmBrowserAdapter(analytics.gtm),
|
|
461
|
+
].filter(Boolean);
|
|
462
|
+
|
|
463
|
+
console.log(adapters, analytics.dispatchOnUnknownConsent);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Browser Adapters
|
|
468
|
+
|
|
469
|
+
GTM is the supported first-party browser adapter in this phase. The adapter returns `null` when GTM is disabled or container ID is missing.
|
|
470
|
+
|
|
471
|
+
### GTM (`createGtmBrowserAdapter`)
|
|
472
|
+
|
|
473
|
+
Pushes namespaced Ekomerc events to `window.dataLayer` (`ekomerc.*`) and updates GTM Consent Mode via `gtag("consent", "update", ...)`.
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { createGtmBrowserAdapter } from "@ekomerc/storefront";
|
|
477
|
+
|
|
478
|
+
const gtmAdapter = createGtmBrowserAdapter(
|
|
479
|
+
store.browserTrackingConfig.gtm, // { enabled, containerId }
|
|
480
|
+
{ dataLayerName: "dataLayer" }, // optional — defaults to "dataLayer"
|
|
481
|
+
);
|
|
482
|
+
// Returns null if disabled or containerId is missing
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
GTM mapping example: `page_view` -> `ekomerc.page_view`, `product_view` -> `ekomerc.view_item`, `add_to_cart` -> `ekomerc.add_to_cart`, `checkout_started` -> `ekomerc.begin_checkout`, `search_performed` -> `ekomerc.search`.
|
|
486
|
+
`checkout_step_completed` and `analytics.custom` remain ingest-only in this phase.
|
|
487
|
+
|
|
488
|
+
### Custom Adapters
|
|
489
|
+
|
|
490
|
+
Implement `AnalyticsBrowserAdapter` to add your own integration:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import type { AnalyticsBrowserAdapter } from "@ekomerc/storefront";
|
|
494
|
+
|
|
495
|
+
const myAdapter: AnalyticsBrowserAdapter = {
|
|
496
|
+
provider: "gtm", // reuse an existing provider key
|
|
497
|
+
dispatch({ event, consent }) {
|
|
498
|
+
if (event.eventType === "analytics.page_view") {
|
|
499
|
+
myAnalytics.page(event.context.path);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
updateConsent(consent) {
|
|
503
|
+
myAnalytics.setConsent(consent.consentState === "granted");
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
```
|
|
507
|
+
|
|
299
508
|
## Error Handling
|
|
300
509
|
|
|
301
510
|
The SDK uses Result types for error handling. Errors are typed and categorized:
|
|
@@ -348,15 +557,13 @@ Always use a full absolute URL like `https://api.example.com/storefront`. The SD
|
|
|
348
557
|
|
|
349
558
|
### Cart State Errors
|
|
350
559
|
|
|
351
|
-
Cart
|
|
560
|
+
Cart mutations (add/update/remove/clear) work on `active` carts. If the cart is in `checkout` state, the backend automatically reactivates it (releases reservations, reverts to `active`) before applying the mutation. This does not mark the cart as abandoned. Attempting to modify a `converted` or `expired` cart returns a `StateError`:
|
|
352
561
|
|
|
353
562
|
```typescript
|
|
354
|
-
// Cart is in checkout state
|
|
355
563
|
const result = await client.cart.addItem(variantId, 1);
|
|
356
564
|
|
|
357
565
|
if (result.isErr() && result.error instanceof StateError) {
|
|
358
566
|
console.log(`Cannot modify cart: ${result.error.message}`);
|
|
359
|
-
// "Cannot modify cart in 'checkout' state. Cart operations are only allowed when cart is in 'active' state."
|
|
360
567
|
}
|
|
361
568
|
```
|
|
362
569
|
|
|
@@ -377,6 +584,11 @@ import type {
|
|
|
377
584
|
ProductOption,
|
|
378
585
|
ProductImage,
|
|
379
586
|
QuantityPricingTier,
|
|
587
|
+
DetailSection,
|
|
588
|
+
RichTextDetailSection,
|
|
589
|
+
BulletListDetailSection,
|
|
590
|
+
TableDetailSection,
|
|
591
|
+
DisplayIntent,
|
|
380
592
|
|
|
381
593
|
// Collections
|
|
382
594
|
Collection,
|
|
@@ -398,8 +610,23 @@ import type {
|
|
|
398
610
|
PaginatedResult,
|
|
399
611
|
|
|
400
612
|
// Common
|
|
401
|
-
Money,
|
|
402
613
|
Store,
|
|
614
|
+
BrowserTrackingConfig,
|
|
615
|
+
GtmBrowserTrackingConfig,
|
|
616
|
+
|
|
617
|
+
// Tracking runtime
|
|
618
|
+
StorefrontTrackingRuntimeConfig,
|
|
619
|
+
AnalyticsBrowserAdapter,
|
|
620
|
+
AnalyticsConsentState,
|
|
621
|
+
AnalyticsDispatchOnUnknownConsent,
|
|
622
|
+
AnalyticsDiagnostic,
|
|
623
|
+
AnalyticsContext,
|
|
624
|
+
AnalyticsIngestResponse,
|
|
625
|
+
|
|
626
|
+
// Browser adapter types
|
|
627
|
+
GtmBrowserAdapterOptions,
|
|
628
|
+
GtmDataLayerEvent,
|
|
629
|
+
GtmConsentModeUpdate,
|
|
403
630
|
} from "@ekomerc/storefront";
|
|
404
631
|
```
|
|
405
632
|
|