@01.software/sdk 0.32.0 → 0.34.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 +253 -38
- package/dist/analytics/react.cjs.map +1 -1
- package/dist/analytics/react.js.map +1 -1
- package/dist/analytics.cjs.map +1 -1
- package/dist/analytics.js.map +1 -1
- package/dist/client.cjs +368 -24
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +7 -6
- package/dist/client.d.ts +7 -6
- package/dist/client.js +368 -24
- package/dist/client.js.map +1 -1
- package/dist/{collection-client-CORhppPb.d.cts → collection-client-CR2B8c1v.d.cts} +7 -3
- package/dist/{collection-client-DPGXnhoF.d.ts → collection-client-DkREjhQ9.d.ts} +7 -3
- package/dist/{const-DcY2_z9O.d.ts → const-BTvdrXtY.d.cts} +5 -5
- package/dist/{const-Brk2Ff0q.d.cts → const-CdqCauHQ.d.ts} +5 -5
- package/dist/index-CjA3U6X3.d.cts +186 -0
- package/dist/index-DK8_NXkh.d.ts +186 -0
- package/dist/index.cjs +1651 -260
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -9
- package/dist/index.d.ts +74 -9
- package/dist/index.js +1651 -260
- package/dist/index.js.map +1 -1
- package/dist/{payload-types-DVK1QCeU.d.cts → payload-types-C7tb7Xbs.d.cts} +2115 -1833
- package/dist/{payload-types-DVK1QCeU.d.ts → payload-types-C7tb7Xbs.d.ts} +2115 -1833
- package/dist/query.cjs +194 -35
- package/dist/query.cjs.map +1 -1
- package/dist/query.d.cts +45 -18
- package/dist/query.d.ts +45 -18
- package/dist/query.js +194 -35
- package/dist/query.js.map +1 -1
- package/dist/realtime.cjs.map +1 -1
- package/dist/realtime.d.cts +2 -2
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js.map +1 -1
- package/dist/{server-CrsPyqEc.d.cts → server-nXOezi4b.d.cts} +22 -6
- package/dist/{server-CrsPyqEc.d.ts → server-nXOezi4b.d.ts} +22 -6
- package/dist/server.cjs +474 -36
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +11 -179
- package/dist/server.d.ts +11 -179
- package/dist/server.js +474 -36
- package/dist/server.js.map +1 -1
- package/dist/{types-DUPC7Xn6.d.ts → types-1ylMrCuW.d.ts} +1 -1
- package/dist/{types-ByMrR_Z_.d.cts → types-Bx558PU6.d.cts} +1 -1
- package/dist/{types-CYMSBkJC.d.ts → types-Byo_Rty4.d.ts} +728 -75
- package/dist/{types-CAkWqIr6.d.cts → types-DDhtZI6E.d.cts} +728 -75
- package/dist/ui/canvas/server.cjs +231 -38
- package/dist/ui/canvas/server.cjs.map +1 -1
- package/dist/ui/canvas/server.d.cts +1 -1
- package/dist/ui/canvas/server.d.ts +1 -1
- package/dist/ui/canvas/server.js +221 -38
- package/dist/ui/canvas/server.js.map +1 -1
- package/dist/ui/canvas.cjs +320 -257
- package/dist/ui/canvas.cjs.map +1 -1
- package/dist/ui/canvas.d.cts +5 -19
- package/dist/ui/canvas.d.ts +5 -19
- package/dist/ui/canvas.js +323 -260
- package/dist/ui/canvas.js.map +1 -1
- package/dist/ui/form.d.cts +1 -1
- package/dist/ui/form.d.ts +1 -1
- package/dist/ui/video.d.cts +1 -1
- package/dist/ui/video.d.ts +1 -1
- package/dist/webhook.cjs +95 -0
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +20 -104
- package/dist/webhook.d.ts +20 -104
- package/dist/webhook.js +95 -0
- package/dist/webhook.js.map +1 -1
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -168,8 +168,9 @@ const preview = await server.preview.detail(
|
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
For product pages, `server.commerce.product.previewDetail({ id }, {
|
|
171
|
-
previewToken })` returns the
|
|
172
|
-
|
|
171
|
+
previewToken })` returns the raw product detail payload for the saved
|
|
172
|
+
draft/unpublished record addressed by the preview token. `detail()` wraps the
|
|
173
|
+
published storefront payload in a `{ found, product | reason }` result.
|
|
173
174
|
|
|
174
175
|
## Getting product detail
|
|
175
176
|
|
|
@@ -179,19 +180,64 @@ The recommended way to fetch a single product is the shaped helper:
|
|
|
179
180
|
import { createClient } from '@01.software/sdk'
|
|
180
181
|
|
|
181
182
|
const client = createClient({
|
|
182
|
-
publishableKey:
|
|
183
|
+
publishableKey: '<publishable-key>',
|
|
183
184
|
})
|
|
184
185
|
|
|
185
|
-
const
|
|
186
|
+
const result = await client.commerce.product.detail({
|
|
186
187
|
slug: 'every-peach-tee',
|
|
187
188
|
})
|
|
188
|
-
if (!
|
|
189
|
-
return notFound()
|
|
189
|
+
if (!result.found) {
|
|
190
|
+
return notFound()
|
|
190
191
|
}
|
|
192
|
+
|
|
193
|
+
const { product } = result
|
|
191
194
|
// product: { product, variants, options, brand, categories, tags, images, videos, listing }
|
|
192
195
|
```
|
|
193
196
|
|
|
194
|
-
`detail()` returns `
|
|
197
|
+
`detail()` returns `ProductDetailResult`, a discriminated union:
|
|
198
|
+
`{ found: true, product: ProductDetail } | { found: false, reason }`. The
|
|
199
|
+
`reason` value is one of `not_found`, `not_published`, or
|
|
200
|
+
`feature_disabled`, so storefronts can choose between a standard 404, preview
|
|
201
|
+
CTA, or feature gating UI. Permission/auth errors, including 403 tenant
|
|
202
|
+
mismatches, still throw
|
|
203
|
+
typed `SDKError` subclasses and preserve request IDs through the existing
|
|
204
|
+
`lastRequestId` / `onRequestId` path.
|
|
205
|
+
|
|
206
|
+
The successful product payload exposes inventory rollups without sentinel
|
|
207
|
+
values: `product.totalInventory` is the tracked stock sum across non-unlimited
|
|
208
|
+
variants, `null` when no variants are tracked, and
|
|
209
|
+
`product.hasUnlimitedVariant` signals whether any variant is unlimited.
|
|
210
|
+
|
|
211
|
+
### Edge-cached catalog + live stock (storefront migration)
|
|
212
|
+
|
|
213
|
+
When a PDP or listing UI is served behind the Console Edge CDN, prefer the
|
|
214
|
+
catalog/stock split instead of reading `variant.stock` from a cached
|
|
215
|
+
`detail()` response (inventory in that payload can lag for the catalog TTL).
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import {
|
|
219
|
+
createClient,
|
|
220
|
+
mergeProductDetailWithStock,
|
|
221
|
+
} from '@01.software/sdk'
|
|
222
|
+
|
|
223
|
+
const client = createClient({ publishableKey: '<publishable-key>' })
|
|
224
|
+
|
|
225
|
+
const catalog = await client.commerce.product.detailCatalog({ slug: 'every-peach-tee' })
|
|
226
|
+
if (!catalog.found) return notFound()
|
|
227
|
+
|
|
228
|
+
const variantIds = catalog.product.variants.map((v) => v.id)
|
|
229
|
+
const snapshot = await client.commerce.product.stockSnapshot({ variantIds })
|
|
230
|
+
const { product, stockMergeStatus } = mergeProductDetailWithStock(
|
|
231
|
+
catalog.product,
|
|
232
|
+
snapshot,
|
|
233
|
+
)
|
|
234
|
+
// stockMergeStatus: 'complete' | 'partial' — partial when a variant id is missing from snapshot
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Listing groups use the same pattern: `listingGroupsCatalog()` for the cacheable
|
|
238
|
+
shell, then `stockSnapshot()` (or `stock-check` at cart/checkout) for live
|
|
239
|
+
availability. `detail()` and POST detail/listing endpoints are unchanged during
|
|
240
|
+
the migration window; see ADR 0012 addendum in `docs/decisions/0012-sdk-public-commerce-contract.md`.
|
|
195
241
|
|
|
196
242
|
### Product selection helpers
|
|
197
243
|
|
|
@@ -224,9 +270,23 @@ media first, then matching variant media, before falling back to listing or
|
|
|
224
270
|
product media. This keeps listing-card selection links and detail-page images
|
|
225
271
|
aligned without rebuilding media priority in storefront code.
|
|
226
272
|
|
|
273
|
+
### Commerce media note (pool + galleries)
|
|
274
|
+
|
|
275
|
+
For new storefront work, prefer pool-pointer and gallery-aware resolution from
|
|
276
|
+
`commerce.product.detail()` + `resolveProductSelection()` (and
|
|
277
|
+
`getProductSelectionImages()` when a list is needed). Direct legacy fields like
|
|
278
|
+
`variant.thumbnail` and option-value direct `images` are still accepted as
|
|
279
|
+
compatibility input, but are deprecated as primary storefront media sources.
|
|
280
|
+
|
|
227
281
|
`availableValuesByOptionSlug` / `availableValuesByOptionId` include
|
|
228
282
|
`availableStock`, `isUnlimited`, and `availableForSale` per value so option UIs
|
|
229
|
-
can render stock state without recalculating from variants.
|
|
283
|
+
can render stock state without recalculating from variants. Each entry also
|
|
284
|
+
exposes handoff-aligned aliases (`exists` == `available`, `label` == `value`) and
|
|
285
|
+
an optional Shopify-shaped `swatch: { color, image }` alongside flat `thumbnail`
|
|
286
|
+
and `images`. Only `swatch.image` falls back to the first entry in `images` when
|
|
287
|
+
`thumbnail` is absent; flat `thumbnail` and `images` on the value object are
|
|
288
|
+
unchanged from the matrix source. Option-value upsert and detail/matrix shapes use
|
|
289
|
+
nested `swatch` only (`swatch.color` for hex); flat `swatchColor` is rejected.
|
|
230
290
|
|
|
231
291
|
### With React Query
|
|
232
292
|
|
|
@@ -242,10 +302,11 @@ Cache key is `['products', 'detail', { slug }]`. Mutations on products, product-
|
|
|
242
302
|
### Selection URL contract
|
|
243
303
|
|
|
244
304
|
Use `createProductSelectionCodec(detail)` when product pages need to keep option
|
|
245
|
-
selection in the URL.
|
|
246
|
-
selections
|
|
247
|
-
|
|
248
|
-
|
|
305
|
+
selection in the URL. By default, complete selections emit `variant=<variantId>`
|
|
306
|
+
and partial selections emit slug-compat params such as `?opt.color=ivory`.
|
|
307
|
+
Inbound canonical ID params (`?opt.<optionId>=<valueId>`) and compatibility slug
|
|
308
|
+
params (`?opt.<optionSlug>=<valueSlug>`) still parse. Plain bare keys such as
|
|
309
|
+
`?color=ivory` are rejected.
|
|
249
310
|
|
|
250
311
|
```typescript
|
|
251
312
|
import {
|
|
@@ -254,26 +315,73 @@ import {
|
|
|
254
315
|
} from '@01.software/sdk'
|
|
255
316
|
|
|
256
317
|
const codec = createProductSelectionCodec(product)
|
|
257
|
-
const normalizedSelection = codec.parse('?opt.
|
|
318
|
+
const normalizedSelection = codec.parse('?opt.color=ivory')
|
|
258
319
|
const selection = resolveProductSelection(product, normalizedSelection)
|
|
259
320
|
const selectionQuery = codec.stringify(normalizedSelection)
|
|
260
|
-
// selectionQuery === 'opt.
|
|
261
|
-
// selectionQuery === 'variant=variant-black-
|
|
321
|
+
// selectionQuery === 'opt.color=ivory' for partial selections
|
|
322
|
+
// selectionQuery === 'variant=variant-black-s' once a complete variant is selected
|
|
262
323
|
// selection.selectedVariant, selection.price, selection.stock, selection.media
|
|
263
324
|
```
|
|
264
325
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
326
|
+
#### Empty vs partial selection
|
|
327
|
+
|
|
328
|
+
When selection input is omitted, `resolveProductSelection()` applies
|
|
329
|
+
`listing.selectionHintVariant` so PDP defaults match listing cards. That is
|
|
330
|
+
separate from `fillDefaults` and is not gated by a flag.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// PDP default (uses listing.selectionHintVariant when selection is omitted)
|
|
334
|
+
resolveProductSelection(product)
|
|
335
|
+
|
|
336
|
+
// Catalog: keep price range / no concrete variant
|
|
337
|
+
resolveProductSelection(product, { valueIds: [] })
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
By default, partial selections (for example color only) leave
|
|
341
|
+
`selectedVariant` as `null`. Opt in to Shopify-style
|
|
342
|
+
`selectedOrFirstAvailableVariant` behavior with `fillDefaults: true`:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const resolution = resolveProductSelection(product, codec.parse('?opt.color=ivory'), {
|
|
346
|
+
fillDefaults: true,
|
|
347
|
+
})
|
|
348
|
+
// resolution.selectedVariant is concrete; unselected options are filled
|
|
349
|
+
// using the same available-by-order rules as listing selectionHintVariant.
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
For option-click handlers, use `selectNext()` to apply a slug transition,
|
|
353
|
+
keep compatible prior selections, and re-default incompatible ones.
|
|
354
|
+
`selectNext()` already fills missing options internally:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
import { resolveProductSelection, selectNext } from '@01.software/sdk'
|
|
358
|
+
|
|
359
|
+
const nextSelection = selectNext(product, currentSelection, 'color', 'ivory')
|
|
360
|
+
const resolution = resolveProductSelection(product, nextSelection)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Use `fillDefaults: true` on `resolveProductSelection()` when you have a
|
|
364
|
+
partial URL or selection state and need a concrete variant without calling
|
|
365
|
+
`selectNext()`. It does not change codec parse/stringify behavior.
|
|
366
|
+
|
|
367
|
+
Opt out of slug-compat outbound URLs with
|
|
368
|
+
`createProductSelectionCodec(product, { emit: 'canonical-id' })`.
|
|
369
|
+
|
|
370
|
+
Normalized selection state uses stable option/value/variant IDs internally.
|
|
371
|
+
Slugs in URLs are a compatibility/readability layer, not the identity source.
|
|
268
372
|
|
|
269
373
|
For listing cards, pass the listing group returned by
|
|
270
374
|
`buildProductListingGroupsByOption()` or the listing-groups endpoint into
|
|
271
|
-
`buildProductHref(product, group, { detail })`.
|
|
272
|
-
|
|
273
|
-
|
|
375
|
+
`buildProductHref(product, group, { detail })`. Listing swatch hrefs emit
|
|
376
|
+
partial slug-compat hints such as `?opt.color=ivory` by default. When full
|
|
377
|
+
detail is not available on a product-list page, pass the group without
|
|
274
378
|
`detail`; `buildProductHref()` still emits the best available selection hint and
|
|
275
379
|
the detail page can resolve it through `resolveProductSelection()`.
|
|
276
380
|
|
|
381
|
+
Use `preferCompleteVariantFromHint: true` on `buildProductHref()` only when a
|
|
382
|
+
listing card should deep-link a complete hint variant instead of a color-only
|
|
383
|
+
partial hint.
|
|
384
|
+
|
|
277
385
|
Do not use bare option query keys such as `?size=large`. The SDK rejects them
|
|
278
386
|
as ambiguous because product pages commonly share URLs with unrelated search,
|
|
279
387
|
filter, analytics, or framework parameters. Namespacing selection keys under
|
|
@@ -281,22 +389,32 @@ filter, analytics, or framework parameters. Namespacing selection keys under
|
|
|
281
389
|
parameters while still allowing unrelated parameters such as `utm_campaign` to
|
|
282
390
|
coexist without being interpreted as selection state.
|
|
283
391
|
|
|
392
|
+
For SEO, treat the product path without selection params as the canonical URL.
|
|
393
|
+
Selection query params are share/deep-link state, not index targets.
|
|
394
|
+
|
|
284
395
|
### Product listing card helper
|
|
285
396
|
|
|
286
397
|
`buildProductListingCard(item, options?)` turns a single
|
|
287
398
|
`commerce.product.listingGroups()` response item into a render-ready
|
|
288
399
|
`ProductListingCard`. Each item includes `listingGroupingState` (`grouped`,
|
|
289
|
-
`no_primary_option`, or `empty`)
|
|
400
|
+
`no_primary_option`, or `empty`) and, when empty, `listingGroupingEmptyReason`
|
|
401
|
+
(`primary_option_not_linked`, `primary_option_has_no_values`, or
|
|
402
|
+
`no_variants_for_primary_option`). Each group includes public-safe
|
|
290
403
|
`variants[]` alongside `variantIds`/`variantCount` so storefronts can render or
|
|
291
404
|
inspect grouped variant fields without a follow-up fetch. The by-ids response
|
|
292
405
|
also returns `missing: string[]` for requested product IDs that were not found,
|
|
293
406
|
not published, or not accessible; `docs` preserve the input `productIds` order
|
|
294
|
-
for returned products. The
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
emit `swatches: []`;
|
|
299
|
-
directly.
|
|
407
|
+
for returned products. The helper populates optional `representativeVariant`, a
|
|
408
|
+
PDP-seeded `href`, representative media (`product.thumbnail` -> first
|
|
409
|
+
`product.images` -> representative variant media -> `null`), an aggregated price
|
|
410
|
+
range across all option-value groups, and a `swatches[]` array derived from
|
|
411
|
+
groups when there is more than one. Single-group products emit `swatches: []`;
|
|
412
|
+
storefronts that disagree can read `item.groups` directly.
|
|
413
|
+
|
|
414
|
+
`buildProductListingCard()` derives card swatches from listing-group
|
|
415
|
+
`optionValueSwatch`. Image swatches use `swatch.mediaItemId`; color swatches use
|
|
416
|
+
`swatch.color`. Option-value thumbnail/gallery fields are no longer part of the
|
|
417
|
+
public listing-group or product-detail contract.
|
|
300
418
|
|
|
301
419
|
```ts
|
|
302
420
|
import {
|
|
@@ -309,9 +427,28 @@ const cards: ProductListingCard[] = response.docs.map((item) =>
|
|
|
309
427
|
)
|
|
310
428
|
```
|
|
311
429
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
430
|
+
The card href is the product path by default; the PDP resolves the
|
|
431
|
+
representative variant through `resolveProductSelection(detail)` without a
|
|
432
|
+
selection param. Each swatch carries a hint-only slug-compat href such as
|
|
433
|
+
`?opt.color=ivory`; the detail page resolves it through
|
|
434
|
+
`resolveProductSelection(detail, { search })`. Use
|
|
435
|
+
`preferCompleteVariantFromHint: true` on `buildProductListingCard()` only when
|
|
436
|
+
the card should deep-link a complete hint variant.
|
|
437
|
+
|
|
438
|
+
## Storefront performance defaults
|
|
439
|
+
|
|
440
|
+
- **PLP:** prefer `useProductListingGroupsCatalogQuery()` (GET `/api/products/listing-groups/query/catalog`, CDN-cacheable) or `commerce.product.listingGroupsCatalog()` + `buildProductListingCard()`. Use `useProductListingGroupsQuery()` when you need the full POST `/query` response shape. Avoid fetching a product list and then calling `detail()` per card.
|
|
441
|
+
- **PDP:** prefer `useProductDetailBySlug()` / `commerce.product.detail()`. Override `staleTime` / `retry` on the hook when you need fresher catalog data or faster failure on errors.
|
|
442
|
+
- **CDN-friendly reads:** server/edge code can use `detailCatalog()` and `listingGroupsCatalog()` (GET, cacheable) plus batched `stockSnapshot()` for live inventory.
|
|
443
|
+
- **React Query in the browser:** default `getQueryClient()` keeps SSR data fresh forever (`staleTime: Infinity`). For client-only storefronts, use `getStorefrontQueryClient()` (~1 minute staleTime) when creating query hooks:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
import { createClient } from '@01.software/sdk'
|
|
447
|
+
import { createQueryHooks, getStorefrontQueryClient } from '@01.software/sdk/query'
|
|
448
|
+
|
|
449
|
+
const client = createClient({ publishableKey: '...' })
|
|
450
|
+
const query = createQueryHooks(client, getStorefrontQueryClient())
|
|
451
|
+
```
|
|
315
452
|
|
|
316
453
|
## Advanced: direct Payload queries (escape hatch)
|
|
317
454
|
|
|
@@ -401,10 +538,11 @@ export const revalidate = 60 // ISR — adjust per page freshness need
|
|
|
401
538
|
|
|
402
539
|
export default async function ProductPage({ params }) {
|
|
403
540
|
const client = createClient({
|
|
404
|
-
publishableKey:
|
|
541
|
+
publishableKey: '<publishable-key>',
|
|
405
542
|
})
|
|
406
|
-
const
|
|
407
|
-
if (!
|
|
543
|
+
const result = await client.commerce.product.detail({ slug: params.slug })
|
|
544
|
+
if (!result.found) return notFound()
|
|
545
|
+
const { product } = result
|
|
408
546
|
// ...
|
|
409
547
|
}
|
|
410
548
|
```
|
|
@@ -739,7 +877,13 @@ await server.commerce.orders.updateTransaction({ pgPaymentId, status, paymentMet
|
|
|
739
877
|
// Returns
|
|
740
878
|
await server.commerce.orders.createReturn({ orderNumber, returnItems, refundAmount, reason? })
|
|
741
879
|
await server.commerce.orders.updateReturn({ returnId, status })
|
|
742
|
-
await server.commerce.orders.returnWithRefund({
|
|
880
|
+
await server.commerce.orders.returnWithRefund({
|
|
881
|
+
orderNumber,
|
|
882
|
+
returnItems: [{ orderItem, quantity, restockingFee? }],
|
|
883
|
+
refundAmount,
|
|
884
|
+
returnShippingFee?,
|
|
885
|
+
pgPaymentId,
|
|
886
|
+
})
|
|
743
887
|
```
|
|
744
888
|
|
|
745
889
|
### Commerce Discounts / Shipping
|
|
@@ -761,6 +905,20 @@ options, option values, and variants. It is the tenant-admin safe path because
|
|
|
761
905
|
it applies the product/option/variant transaction that raw collection writes do
|
|
762
906
|
not provide.
|
|
763
907
|
|
|
908
|
+
`commerce.product.upsert()` is a graph-write API. For Admin Panel product
|
|
909
|
+
documents, Payload saves product fields first; upsert receives `productId`,
|
|
910
|
+
`graphRevision` (required when updating an existing product via `productId` -
|
|
911
|
+
load from `GET /api/products/:id/composer-draft`), options, and variants. Do
|
|
912
|
+
not send removed legacy media inputs (`optionValue.thumbnail`,
|
|
913
|
+
`optionValue.images`, `variant.thumbnail`); use `swatch.mediaItemId` and
|
|
914
|
+
variant `images[]`. Unknown keys are not part of the published upsert contract.
|
|
915
|
+
|
|
916
|
+
| Payload | Valid | Invalid |
|
|
917
|
+
| ------- | ----- | ------- |
|
|
918
|
+
| Create | `product: { title, ... }` + graph | `productId` on create; missing `product.title` |
|
|
919
|
+
| Edit graph | `productId` + `graphRevision?` + graph | `product.title` / SEO on upsert; conflicting `productId` vs `product.id` |
|
|
920
|
+
| Edit (legacy) | `product: { id }` + graph only | `product: { id, title }` on edit |
|
|
921
|
+
|
|
764
922
|
```typescript
|
|
765
923
|
const result = await server.commerce.product.upsert({
|
|
766
924
|
product: {
|
|
@@ -773,8 +931,16 @@ const result = await server.commerce.product.upsert({
|
|
|
773
931
|
title: 'Color',
|
|
774
932
|
slug: 'color',
|
|
775
933
|
values: [
|
|
776
|
-
{
|
|
777
|
-
|
|
934
|
+
{
|
|
935
|
+
value: 'Black',
|
|
936
|
+
slug: 'black',
|
|
937
|
+
swatch: { type: 'color', color: '#111111' },
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
value: 'White',
|
|
941
|
+
slug: 'white',
|
|
942
|
+
swatch: { type: 'color', color: '#ffffff' },
|
|
943
|
+
},
|
|
778
944
|
],
|
|
779
945
|
},
|
|
780
946
|
{
|
|
@@ -809,6 +975,18 @@ if (!result.ok) {
|
|
|
809
975
|
}
|
|
810
976
|
```
|
|
811
977
|
|
|
978
|
+
Existing product graph edits send the saved product id and the composer draft
|
|
979
|
+
baseline, without product document fields:
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
await client.commerce.product.upsert({
|
|
983
|
+
productId: 'prod_123',
|
|
984
|
+
graphRevision: draft.graphRevision,
|
|
985
|
+
options: draft.options,
|
|
986
|
+
variants: draft.variants,
|
|
987
|
+
})
|
|
988
|
+
```
|
|
989
|
+
|
|
812
990
|
For updates to existing options or option-values, prefer `id` / `valueId` when
|
|
813
991
|
available so rename-safe updates do not depend on slugs.
|
|
814
992
|
|
|
@@ -869,6 +1047,18 @@ await server.community.moderation.unbanCustomer({ customerId })
|
|
|
869
1047
|
|
|
870
1048
|
### Webhook
|
|
871
1049
|
|
|
1050
|
+
Create or open the endpoint in Console → Integrations → Webhooks. After saving,
|
|
1051
|
+
use **Reveal signing secret** once for the initial value, or **Rotate secret** to
|
|
1052
|
+
invalidate the previous secret and receive a new one-time plaintext value. Set the
|
|
1053
|
+
copied value in the receiver environment as `WEBHOOK_SECRET`.
|
|
1054
|
+
|
|
1055
|
+
`Webhook` list/read responses do not include plaintext signing secrets — only
|
|
1056
|
+
the Console reveal/rotate flows return them. Use `WebhookWithoutSecret` and
|
|
1057
|
+
`WebhookSigningSecretReveal` from `@01.software/sdk/webhook` for typed clients.
|
|
1058
|
+
This value is not
|
|
1059
|
+
`SOFTWARE_SECRET_KEY`; multiple webhook endpoints may require multiple receiver
|
|
1060
|
+
secrets.
|
|
1061
|
+
|
|
872
1062
|
Use HMAC-SHA256 signature verification:
|
|
873
1063
|
|
|
874
1064
|
```typescript
|
|
@@ -1097,12 +1287,37 @@ API keys created without explicit scopes use the default `['read', 'write']`. Co
|
|
|
1097
1287
|
|
|
1098
1288
|
### v0.23.0 (Product option-value visuals)
|
|
1099
1289
|
|
|
1100
|
-
- Added reusable option-value visuals (`
|
|
1290
|
+
- Added reusable option-value visuals (nested `swatch`, `thumbnail`, `images`) to Payload types and ecommerce utility shapes.
|
|
1101
1291
|
- Listing group summaries now include option-value visual metadata and can use one colorway image across every size variant.
|
|
1102
1292
|
- Product/listing sellability now uses `stock - reservedStock`, matching checkout stock checks.
|
|
1103
1293
|
|
|
1294
|
+
### v0.34.0 (Breaking — swatch-only option-value visuals)
|
|
1295
|
+
|
|
1296
|
+
**Breaking change.** Option-value visual fields were collapsed into nested
|
|
1297
|
+
`swatch` (`type`, `color`, `mediaItemId`). Legacy public fields are removed
|
|
1298
|
+
from SDK input/output helpers and from listing/detail contracts.
|
|
1299
|
+
|
|
1300
|
+
| Surface | Removed | Replacement |
|
|
1301
|
+
| ------- | ------- | ----------- |
|
|
1302
|
+
| Upsert option value | `swatchColor`, `thumbnail`, `images` | `swatch.type`, `swatch.color`, `swatch.mediaItemId` |
|
|
1303
|
+
| Listing groups | `optionValueSwatchColor`, `optionValueThumbnail`, `optionValueImages` | `optionValueSwatch` |
|
|
1304
|
+
| Product detail option value | `swatchColor`, `thumbnail`, `images` | `swatch` |
|
|
1305
|
+
|
|
1306
|
+
Migration steps:
|
|
1307
|
+
|
|
1308
|
+
1. Replace color-only values with `swatch: { type: 'color', color: '#111111' }`.
|
|
1309
|
+
2. Replace thumbnail/gallery values with `swatch: { type: 'media', mediaItemId: '<product-pool-image-id>' }`.
|
|
1310
|
+
3. Ensure media swatches reference an image already attached to the parent product (`products.images` / `products.thumbnail`).
|
|
1311
|
+
4. Remove reads of legacy response fields; consume `optionValueSwatch` / `optionValue.swatch` instead.
|
|
1312
|
+
|
|
1104
1313
|
## Migration Guide
|
|
1105
1314
|
|
|
1315
|
+
### v0.34.0 (Product option-value visuals — breaking)
|
|
1316
|
+
|
|
1317
|
+
See the v0.34.0 changelog entry above for the field mapping table and migration
|
|
1318
|
+
steps. There is no compatibility alias window for removed option-value visual
|
|
1319
|
+
fields; callers must migrate to nested `swatch` before upgrading.
|
|
1320
|
+
|
|
1106
1321
|
### v0.16.0 (Phase 1–7 sync — additive)
|
|
1107
1322
|
|
|
1108
1323
|
New error codes propagated via `SDKError.code` (no breaking change; existing callers ignore unknown codes safely):
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analytics/react.tsx","../../src/core/client/types.ts","../../src/analytics.ts","../../src/analytics/env.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\nimport { createAnalytics, type AnalyticsConfig } from '../analytics'\nimport { resolveAnalyticsPublishableKey } from './env'\n\nexport type AnalyticsProps = Omit<AnalyticsConfig, 'publishableKey'> & {\n publishableKey?: string\n}\n\nexport function Analytics({\n autoTrack,\n endpoint,\n publishableKey,\n respectDnt,\n}: AnalyticsProps) {\n useEffect(() => {\n const resolvedPublishableKey =\n resolveAnalyticsPublishableKey(publishableKey)\n if (!resolvedPublishableKey) return\n\n const analytics = createAnalytics({\n autoTrack,\n endpoint,\n publishableKey: resolvedPublishableKey,\n respectDnt,\n })\n\n return () => analytics.destroy()\n }, [autoTrack, endpoint, publishableKey, respectDnt])\n\n return null\n}\n\nexport default Analytics\n","import type {\n Collection,\n PublicCollection,\n ServerCollection,\n ServerOnlyCollection,\n} from '../collection/const'\nimport type { CollectionType } from '../collection/types'\nimport type { CommunityClient } from '../community/community-client'\nimport type {\n BanCustomerParams,\n CommunityBan,\n UnbanCustomerParams,\n} from '../community/moderation-api'\nimport type { CommerceClient } from '../commerce/commerce-client'\nimport type { ServerCommerceClient } from '../commerce/server-commerce-client'\nimport type { CustomerNamespace } from '../customer/customer-namespace'\nimport type { TenantIntrospectionClient } from '../api/tenant-introspection-api'\n\nexport type {\n Collection,\n PublicCollection,\n ServerCollection,\n ServerOnlyCollection,\n}\n\n// ============================================================================\n// API URL Configuration\n// ============================================================================\n\ndeclare const __DEFAULT_API_URL__: string\n\nexport function resolveApiUrl(apiUrl?: string): string {\n if (apiUrl) {\n return apiUrl.replace(/\\/$/, '')\n }\n\n if (typeof process !== 'undefined' && process.env) {\n const envUrl =\n process.env.SOFTWARE_API_URL || process.env.NEXT_PUBLIC_SOFTWARE_API_URL\n if (envUrl) {\n return envUrl.replace(/\\/$/, '')\n }\n }\n return __DEFAULT_API_URL__\n}\n\n// ============================================================================\n// Client Configuration\n// ============================================================================\n\nexport interface ClientConfig {\n publishableKey: string\n /** API base URL for staging, self-hosted, preview, or proxy deployments. */\n apiUrl?: string\n /**\n * Customer authentication options.\n * Used to initialize CustomerAuth on Client.\n */\n customer?: {\n /**\n * Persist token in localStorage. Defaults to `true`.\n * - `true` (default): uses key `'customer-token'`\n * - `string`: uses the given string as localStorage key\n * - `false`: disables persistence (token/onTokenChange used instead)\n *\n * Handles SSR safely (no-op on server).\n * When enabled, `token` and `onTokenChange` are ignored.\n */\n persist?: boolean | string\n /** Initial token (e.g. from SSR cookie) */\n token?: string\n /** Called when token changes (login/logout) — use to persist in localStorage/cookie */\n onTokenChange?: (token: string | null) => void\n }\n}\n\n// Server client: requires both publishableKey (for CDN routing + rate limit +\n// monthly quota enforcement via the edge proxy) and secretKey (sk01_ opaque\n// bearer token, the authentication credential).\n// The proxy keys its tenant lookup off `X-Publishable-Key`, so omitting\n// publishableKey would silently bypass rate limiting and plan-based quota\n// enforcement.\nexport interface ClientServerConfig extends ClientConfig {\n secretKey: string\n}\n\nexport interface ClientMetadata {\n userAgent?: string\n timestamp: number\n}\n\nexport interface ClientState {\n metadata: ClientMetadata\n}\n\nexport interface PaginationMeta {\n page: number\n limit: number\n totalDocs: number\n totalPages: number\n hasNextPage: boolean\n hasPrevPage: boolean\n pagingCounter: number\n prevPage: number | null\n nextPage: number | null\n}\n\n// ============================================================================\n// Payload CMS Native Response Types\n// ============================================================================\n\n/**\n * Payload CMS Find (List) Response\n * GET /api/{collection}\n */\nexport interface PayloadFindResponse<T = unknown> {\n docs: T[]\n totalDocs: number\n limit: number\n totalPages: number\n page: number\n pagingCounter: number\n hasPrevPage: boolean\n hasNextPage: boolean\n prevPage: number | null\n nextPage: number | null\n}\n\n/**\n * Payload CMS Create/Update Response\n * POST /api/{collection}\n * PATCH /api/{collection}/{id}\n */\nexport interface PayloadMutationResponse<T = unknown> {\n message: string\n doc: T\n errors?: unknown[]\n}\n\n// ============================================================================\n// Query Options\n// ============================================================================\n\nexport type Sort = string | string[]\nexport type Where = Record<string, unknown>\n\n/**\n * Do NOT replace with `Pick<FindOptions>` from `payload` or import Payload\n * types here. Payload's generic query types depend on `PayloadTypes` module\n * augmentation; external SDK consumers who only use `createClient` should not\n * install Payload just to type REST query objects. Excluded vs native:\n * Local-API-only fields, `locale`/`fallbackLocale`.\n */\nexport interface ApiQueryOptions {\n page?: number\n limit?: number\n sort?: Sort\n /**\n * Filter documents. Id-based relation filters (`where: { product: { equals: id } }`) are the\n * most reliable pattern. Dotted-path relation filters (`where: { 'product.slug': { equals } }`)\n * are Payload-native but may silently return empty when access control restricts the related\n * document or when the relation is polymorphic. String shorthand (`where: { slug: 'x' }`)\n * silently matches nothing — always use `{ slug: { equals: 'x' } }`.\n */\n where?: Where\n /**\n * Controls how deeply relationship fields are populated. This is the primary control for\n * populating relationships like `category`, `images`, `brand`. The configured Payload default\n * applies when unset.\n */\n depth?: number\n select?: Record<string, boolean>\n /**\n * Controls which fields are returned for already-populated relationships, keyed by collection\n * slug. Does NOT control which relationships to populate — that is `depth`.\n *\n * @example\n * // depth: 2 populates category; populate trims which fields come back\n * populate: { categories: { title: true, slug: true } }\n */\n populate?: Record<string, boolean | Record<string, boolean>>\n /**\n * Controls Payload `type: 'join'` virtual reverse-relation fields only (pagination, sort,\n * filter, count per join field, or `false` to disable all join-field population).\n *\n * Does NOT populate normal relationship fields like `category`, `images`, or `brand`.\n * For normal relationship population use `depth` (and optionally `populate` for field\n * selection).\n *\n * Pass `joins: false` to disable all join-field population — useful for lightweight list\n * queries where join fields are not needed.\n *\n * @example\n * // `article-authors` has a `type: 'join'` field `articles` (reverse-relation)\n * joins: { articles: { limit: 10, sort: '-publishedAt' } }\n *\n * // depth: 2 populates product.category — joins has no effect on this\n * depth: 2\n *\n * // Disable all join-field population\n * joins: false\n */\n joins?:\n | Record<\n string,\n | {\n limit?: number\n page?: number\n sort?: string\n where?: Where\n count?: boolean\n }\n | false\n >\n | false\n /** Set to `false` to skip the count query — returns docs without totalDocs/totalPages */\n pagination?: boolean\n /** Include draft versions (access control still applies on the server) */\n draft?: boolean\n /** Include soft-deleted documents (requires `trash` enabled on the collection) */\n trash?: boolean\n}\n\n// ============================================================================\n// Debug & Retry Configuration\n// ============================================================================\n\nexport interface DebugConfig {\n logRequests?: boolean\n logResponses?: boolean\n logErrors?: boolean\n}\n\nexport interface RetryConfig {\n maxRetries?: number\n retryableStatuses?: number[]\n retryDelay?: (attempt: number) => number\n}\n\n// ============================================================================\n// Lightweight root entry contracts\n// ============================================================================\n\ninterface RootQueryLookup<T extends string> {\n find(options?: ApiQueryOptions): Promise<PayloadFindResponse<CollectionType<T>>>\n findById(\n id: string | number,\n options?: ApiQueryOptions,\n ): Promise<CollectionType<T>>\n count(options?: ApiQueryOptions): Promise<{ totalDocs: number }>\n}\n\nexport type RootReadOnlyQueryBuilder<T extends PublicCollection> =\n RootQueryLookup<T>\n\nexport interface RootServerQueryBuilder<T extends ServerCollection>\n extends RootQueryLookup<T> {\n create(\n data: Partial<CollectionType<T>>,\n options?: { file?: File | Blob; filename?: string },\n ): Promise<PayloadMutationResponse<CollectionType<T>>>\n update(\n id: string,\n data: Partial<CollectionType<T>>,\n options?: { file?: File | Blob; filename?: string },\n ): Promise<PayloadMutationResponse<CollectionType<T>>>\n updateMany(\n where: ApiQueryOptions['where'],\n data: Partial<CollectionType<T>>,\n ): Promise<PayloadFindResponse<CollectionType<T>>>\n remove(id: string): Promise<CollectionType<T>>\n removeMany(\n where: ApiQueryOptions['where'],\n ): Promise<PayloadFindResponse<CollectionType<T>>>\n}\n\nexport interface RootCollectionClient {\n from<T extends PublicCollection>(collection: T): RootReadOnlyQueryBuilder<T>\n}\n\nexport interface RootServerCollectionClient {\n from<T extends ServerCollection>(collection: T): RootServerQueryBuilder<T>\n}\n\nexport interface RootClient {\n commerce: CommerceClient\n community: CommunityClient\n customer: CustomerNamespace\n collections: RootCollectionClient\n lastRequestId: string | null\n getState(): ClientState\n getConfig(): ClientConfig\n}\n\nexport interface RootServerClient {\n commerce: ServerCommerceClient\n tenant: TenantIntrospectionClient\n community: CommunityClient & {\n moderation: {\n banCustomer: (p: BanCustomerParams) => Promise<CommunityBan>\n unbanCustomer: (p: UnbanCustomerParams) => Promise<{ success: true }>\n }\n }\n collections: RootServerCollectionClient\n lastRequestId: string | null\n getState(): ClientState\n getConfig(): Omit<ClientServerConfig, 'secretKey'>\n}\n\n// ============================================================================\n// Type Utilities\n// ============================================================================\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]\n}\n\nexport type ExtractArrayType<T> = T extends (infer U)[] ? U : never\n","/**\n * @01.software/sdk — Analytics Helper\n */\n\n/* ANALYTICS INVARIANTS START\n * @01.software/sdk — Analytics Helper\n *\n * ANALYTICS INVARIANTS\n * ====================\n * These invariants are the single source of truth for observable behavior.\n * They are mirrored verbatim in apps/console/src/app/api/analytics/script.js/route.ts.\n * Any change here MUST be reflected there, and vice versa.\n *\n * 1. DNT/GPC respect: when config.respectDnt !== false (default true) AND\n * (navigator.doNotTrack === '1' OR navigator.globalPrivacyControl === true),\n * all methods become no-ops. Zero network requests are made.\n *\n * 2. Prerender skip: when document.prerendering === true OR\n * document.visibilityState === 'prerender', pageview() sends zero requests.\n *\n * 3. 500ms same-path dedup: a pageview for the same pathname within 500ms of\n * the previous send is silently dropped. After 500ms the next call sends.\n *\n * 4. Transport: sendBeacon → fetch keepalive fallback.\n * Primary: navigator.sendBeacon(endpoint, new Blob([json], { type: 'text/plain' })).\n * Fallback (sendBeacon unavailable OR returns false):\n * fetch(endpoint, { method: 'POST', keepalive: true,\n * headers: { 'Content-Type': 'application/json' }, body: json }).catch(() => {})\n *\n * 5. Body-only publishableKey: publishableKey is always in the request body,\n * never in any HTTP header.\n *\n * 6. SSR no-op: when typeof window === 'undefined', createAnalytics() returns\n * a stub where all methods are no-ops. No side effects occur.\n *\n * 7. Error swallowing: all transport errors are caught and swallowed.\n * createAnalytics() and all returned methods never throw into the caller.\n *\n * 8. Client timestamp: every send carries eventTs (milliseconds since epoch)\n * captured with Date.now() immediately before transport. The collect\n * endpoint uses eventTs (a) to bucket the event into the client's\n * tenant-local day and (b) to enforce the late-arrival cutoff; events\n * submitted after the local-day-end grace window are dropped with\n * reason \"late\".\n * ANALYTICS INVARIANTS END */\n\nimport { resolveApiUrl } from './core/client/types'\n\n// ============================================================================\n// Public Types\n// ============================================================================\n\nexport interface AnalyticsConfig {\n publishableKey: string\n /** Override the collect endpoint URL. Defaults to {SDK_BASE_URL}/api/analytics/collect */\n endpoint?: string\n /** Auto-patch history.pushState/replaceState and listen to popstate. Default: true */\n autoTrack?: boolean\n /** Respect navigator.doNotTrack and navigator.globalPrivacyControl. Default: true */\n respectDnt?: boolean\n}\n\nexport interface Analytics {\n pageview(path?: string): void\n track(name: string, props?: Record<string, string | number | boolean>): void\n destroy(): void\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nexport function createAnalytics(config: AnalyticsConfig): Analytics {\n // INVARIANT 6: SSR no-op\n if (typeof window === 'undefined') {\n return { pageview() {}, track() {}, destroy() {} }\n }\n\n const endpoint =\n config.endpoint ?? `${resolveApiUrl()}/api/analytics/collect`\n\n // INVARIANT 1: DNT/GPC check (evaluated once at init; stays as closure)\n const respectDnt = config.respectDnt !== false\n function isDntActive(): boolean {\n if (!respectDnt) return false\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean }\n return nav.doNotTrack === '1' || nav.globalPrivacyControl === true\n }\n\n // INVARIANT 3: 500ms same-path dedup state\n let lastPath: string | null = null\n let lastAt = 0\n\n // autoTrack state — save originals for destroy()\n const autoTrack = config.autoTrack !== false\n const originalPushState = history.pushState\n const originalReplaceState = history.replaceState\n let destroyed = false\n\n // -------------------------------------------------------------------------\n // Core send logic\n // -------------------------------------------------------------------------\n\n // Generate a unique event ID (crypto.randomUUID when available, Date+Math.random fallback)\n function newEventId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : String(Date.now()) + String(Math.random())\n }\n\n // INVARIANT 4: sendBeacon → fetch keepalive fallback\n // INVARIANT 5: publishableKey in body only\n function sendBeaconOrFetch(body: string): void {\n try {\n if (typeof navigator.sendBeacon === 'function') {\n const blob = new Blob([body], { type: 'text/plain' })\n const sent = navigator.sendBeacon(endpoint, blob)\n if (sent) return\n // sent === false → fall through to fetch\n }\n // Fetch fallback\n fetch(endpoint, {\n method: 'POST',\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n body,\n }).catch(() => {})\n } catch {\n // INVARIANT 7: swallow all errors\n }\n }\n\n function sendPageview(pathname: string): void {\n // INVARIANT 1: DNT/GPC\n if (isDntActive()) return\n\n // INVARIANT 2: prerender skip\n const doc = document as Document & { prerendering?: boolean }\n // visibilityState cast to string to accommodate non-standard 'prerender' value\n if (doc.prerendering === true || (document.visibilityState as string) === 'prerender') return\n\n // INVARIANT 3: 500ms same-path dedup\n const now = Date.now()\n if (pathname === lastPath && now - lastAt < 500) return\n lastPath = pathname\n lastAt = now\n\n const body = JSON.stringify({\n publishableKey: config.publishableKey,\n pathname,\n referrer: document.referrer || '',\n eventId: newEventId(),\n eventTs: Date.now(),\n })\n\n sendBeaconOrFetch(body)\n }\n\n // -------------------------------------------------------------------------\n // autoTrack: patch history methods + listen to popstate\n // -------------------------------------------------------------------------\n function trackCurrentPath(): void {\n if (destroyed) return\n sendPageview(location.pathname)\n }\n\n function patchedPushState(\n this: History,\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalPushState.apply(this, [data, unused, url] as Parameters<typeof history.pushState>)\n if (!destroyed) setTimeout(trackCurrentPath, 0)\n }\n\n function patchedReplaceState(\n this: History,\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalReplaceState.apply(this, [data, unused, url] as Parameters<typeof history.replaceState>)\n if (!destroyed) setTimeout(trackCurrentPath, 0)\n }\n\n if (autoTrack) {\n history.pushState = patchedPushState\n history.replaceState = patchedReplaceState\n window.addEventListener('popstate', trackCurrentPath)\n\n // Initial pageview\n if (document.readyState === 'complete') {\n trackCurrentPath()\n } else {\n window.addEventListener('load', trackCurrentPath, { once: true })\n }\n }\n\n // -------------------------------------------------------------------------\n // track() — client-side validation + send\n // -------------------------------------------------------------------------\n\n // Dev-mode detection: warn in dev, silent in production.\n // process.env.NODE_ENV is unreliable in browser bundles (tsup does not replace it\n // by default). Instead we detect production at runtime via hostname heuristics.\n // SSR (window undefined) is caught at the top of createAnalytics and returns a\n // stub, so window is always defined here.\n const isProduction: boolean = (() => {\n try {\n const hostname = location.hostname\n return (\n hostname !== 'localhost' &&\n hostname !== '127.0.0.1' &&\n !hostname.endsWith('.local')\n )\n } catch {\n // hostname access failed (non-browser) — default to silent\n return true\n }\n })()\n\n // One-shot warn dedup per reason per page load (keyed by reason only)\n const warnedReasons = new Set<string>()\n\n function devWarn(name: string, reason: string): void {\n if (isProduction) return\n if (warnedReasons.has(reason)) return\n warnedReasons.add(reason)\n console.warn(`[01 analytics] dropped event ${name}: ${reason}`)\n }\n\n const EVENT_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_:-]{0,49}$/\n const RESERVED_PREFIXES = ['__', '_pv_']\n\n function validateEventName(name: string): string | null {\n if (!name || typeof name !== 'string') return 'name-empty'\n for (const prefix of RESERVED_PREFIXES) {\n if (name.startsWith(prefix)) return 'name-reserved'\n }\n if (!EVENT_NAME_RE.test(name)) return 'name-regex'\n return null\n }\n\n const PROP_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,31}$/\n\n function validateEventProps(\n props: Record<string, string | number | boolean> | undefined,\n ): string | null {\n if (props === undefined || props === null) return null\n if (typeof props !== 'object' || Array.isArray(props)) return 'props-value-type'\n const keys = Object.keys(props)\n if (keys.length > 10) return 'props-too-many-keys'\n for (const k of keys) {\n const v = props[k]\n if (!PROP_KEY_RE.test(k)) return 'props-key-regex'\n if (typeof v === 'string') {\n if (v.length > 80) return 'props-value-too-long'\n } else if (typeof v === 'number') {\n if (!isFinite(v)) return 'props-value-not-finite'\n } else if (typeof v === 'boolean') {\n // ok\n } else {\n return 'props-value-type'\n }\n }\n return null\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n return {\n pageview(path?: string): void {\n if (destroyed) return\n sendPageview(path ?? location.pathname)\n },\n\n track(name: string, props?: Record<string, string | number | boolean>): void {\n if (destroyed) return\n\n // INVARIANT 1: DNT/GPC (same as pageview)\n if (isDntActive()) return\n\n // INVARIANT 2: prerender skip\n const doc = document as Document & { prerendering?: boolean }\n if (doc.prerendering === true || (document.visibilityState as string) === 'prerender') return\n\n // Client-side validation\n const nameErr = validateEventName(name)\n if (nameErr) {\n devWarn(name, nameErr)\n return\n }\n\n if (props !== undefined) {\n const propsErr = validateEventProps(props)\n if (propsErr) {\n devWarn(name, propsErr)\n return\n }\n }\n\n // Build body — no dedup for track() events\n const body = JSON.stringify({\n publishableKey: config.publishableKey,\n pathname: location.pathname,\n referrer: document.referrer || '',\n eventId: newEventId(),\n eventName: name,\n eventProps: props,\n eventTs: Date.now(),\n })\n\n sendBeaconOrFetch(body)\n },\n\n destroy(): void {\n if (destroyed) return\n destroyed = true\n\n if (autoTrack) {\n // Restore original history methods\n history.pushState = originalPushState\n history.replaceState = originalReplaceState\n window.removeEventListener('popstate', trackCurrentPath)\n }\n\n // Null out dedup state\n lastPath = null\n lastAt = 0\n },\n }\n}\n","export type AnalyticsRuntimeEnv = {\n nextPublicKey?: string\n vitePublicKey?: string\n}\n\nexport function readAnalyticsRuntimeEnv(): AnalyticsRuntimeEnv {\n const nextPublicKey =\n typeof process !== 'undefined'\n ? process.env?.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY\n : undefined\n\n const viteEnv = (\n import.meta as ImportMeta & {\n env?: Record<string, string | undefined>\n }\n ).env\n\n return {\n nextPublicKey,\n vitePublicKey: viteEnv?.VITE_SOFTWARE_PUBLISHABLE_KEY,\n }\n}\n\nexport function resolveAnalyticsPublishableKey(\n explicit?: string,\n env: AnalyticsRuntimeEnv = readAnalyticsRuntimeEnv(),\n): string | undefined {\n if (explicit !== undefined) return explicit || undefined\n\n return env.nextPublicKey || env.vitePublicKey\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/react.tsx","../../src/core/client/types.ts","../../src/analytics.ts","../../src/analytics/env.ts"],"sourcesContent":["'use client'\n\nimport { useEffect } from 'react'\nimport { createAnalytics, type AnalyticsConfig } from '../analytics'\nimport { resolveAnalyticsPublishableKey } from './env'\n\nexport type AnalyticsProps = Omit<AnalyticsConfig, 'publishableKey'> & {\n publishableKey?: string\n}\n\nexport function Analytics({\n autoTrack,\n endpoint,\n publishableKey,\n respectDnt,\n}: AnalyticsProps) {\n useEffect(() => {\n const resolvedPublishableKey =\n resolveAnalyticsPublishableKey(publishableKey)\n if (!resolvedPublishableKey) return\n\n const analytics = createAnalytics({\n autoTrack,\n endpoint,\n publishableKey: resolvedPublishableKey,\n respectDnt,\n })\n\n return () => analytics.destroy()\n }, [autoTrack, endpoint, publishableKey, respectDnt])\n\n return null\n}\n\nexport default Analytics\n","import type {\n Collection,\n PublicCollection,\n ServerCollection,\n ServerOnlyCollection,\n} from '../collection/const'\nimport type { CollectionType } from '../collection/types'\nimport type { CommunityClient } from '../community/community-client'\nimport type {\n BanCustomerParams,\n CommunityBan,\n UnbanCustomerParams,\n} from '../community/moderation-api'\nimport type { CommerceClient } from '../commerce/commerce-client'\nimport type { ServerCommerceClient } from '../commerce/server-commerce-client'\nimport type { CustomerNamespace } from '../customer/customer-namespace'\nimport type { EventsClient } from '../events/events-client'\nimport type { TenantIntrospectionClient } from '../api/tenant-introspection-api'\n\nexport type {\n Collection,\n PublicCollection,\n ServerCollection,\n ServerOnlyCollection,\n}\n\n// ============================================================================\n// API URL Configuration\n// ============================================================================\n\ndeclare const __DEFAULT_API_URL__: string\n\nexport function resolveApiUrl(apiUrl?: string): string {\n if (apiUrl) {\n return apiUrl.replace(/\\/$/, '')\n }\n\n if (typeof process !== 'undefined' && process.env) {\n const envUrl =\n process.env.SOFTWARE_API_URL || process.env.NEXT_PUBLIC_SOFTWARE_API_URL\n if (envUrl) {\n return envUrl.replace(/\\/$/, '')\n }\n }\n return __DEFAULT_API_URL__\n}\n\n// ============================================================================\n// Client Configuration\n// ============================================================================\n\nexport interface ClientConfig {\n publishableKey: string\n /** API base URL for staging, self-hosted, preview, or proxy deployments. */\n apiUrl?: string\n /**\n * Customer authentication options.\n * Used to initialize CustomerAuth on Client.\n */\n customer?: {\n /**\n * Persist token in localStorage. Defaults to `true`.\n * - `true` (default): uses key `'customer-token'`\n * - `string`: uses the given string as localStorage key\n * - `false`: disables persistence (token/onTokenChange used instead)\n *\n * Handles SSR safely (no-op on server).\n * When enabled, `token` and `onTokenChange` are ignored.\n */\n persist?: boolean | string\n /** Initial token (e.g. from SSR cookie) */\n token?: string\n /** Called when token changes (login/logout) — use to persist in localStorage/cookie */\n onTokenChange?: (token: string | null) => void\n }\n}\n\n// Server client: requires both publishableKey (for CDN routing + rate limit +\n// monthly quota enforcement via the edge proxy) and secretKey (sk01_ opaque\n// bearer token, the authentication credential).\n// The proxy keys its tenant lookup off `X-Publishable-Key`, so omitting\n// publishableKey would silently bypass rate limiting and plan-based quota\n// enforcement.\nexport interface ClientServerConfig extends ClientConfig {\n secretKey: string\n}\n\nexport interface ClientMetadata {\n userAgent?: string\n timestamp: number\n}\n\nexport interface ClientState {\n metadata: ClientMetadata\n}\n\nexport interface PaginationMeta {\n page: number\n limit: number\n totalDocs: number\n totalPages: number\n hasNextPage: boolean\n hasPrevPage: boolean\n pagingCounter: number\n prevPage: number | null\n nextPage: number | null\n}\n\n// ============================================================================\n// Payload CMS Native Response Types\n// ============================================================================\n\n/**\n * Payload CMS Find (List) Response\n * GET /api/{collection}\n */\nexport interface PayloadFindResponse<T = unknown> {\n docs: T[]\n totalDocs: number\n limit: number\n totalPages: number\n page: number\n pagingCounter: number\n hasPrevPage: boolean\n hasNextPage: boolean\n prevPage: number | null\n nextPage: number | null\n}\n\n/**\n * Payload CMS Create/Update Response\n * POST /api/{collection}\n * PATCH /api/{collection}/{id}\n */\nexport interface PayloadMutationResponse<T = unknown> {\n message: string\n doc: T\n errors?: unknown[]\n}\n\n// ============================================================================\n// Query Options\n// ============================================================================\n\nexport type Sort = string | string[]\nexport type Where = Record<string, unknown>\n\n/**\n * Do NOT replace with `Pick<FindOptions>` from `payload` or import Payload\n * types here. Payload's generic query types depend on `PayloadTypes` module\n * augmentation; external SDK consumers who only use `createClient` should not\n * install Payload just to type REST query objects. Excluded vs native:\n * Local-API-only fields, `locale`/`fallbackLocale`.\n */\nexport interface ApiQueryOptions {\n page?: number\n limit?: number\n sort?: Sort\n /**\n * Filter documents. Id-based relation filters (`where: { product: { equals: id } }`) are the\n * most reliable pattern. Dotted-path relation filters (`where: { 'product.slug': { equals } }`)\n * are Payload-native but may silently return empty when access control restricts the related\n * document or when the relation is polymorphic. String shorthand (`where: { slug: 'x' }`)\n * silently matches nothing — always use `{ slug: { equals: 'x' } }`.\n */\n where?: Where\n /**\n * Controls how deeply relationship fields are populated. This is the primary control for\n * populating relationships like `category`, `images`, `brand`. The configured Payload default\n * applies when unset.\n */\n depth?: number\n select?: Record<string, boolean>\n /**\n * Controls which fields are returned for already-populated relationships, keyed by collection\n * slug. Does NOT control which relationships to populate — that is `depth`.\n *\n * @example\n * // depth: 2 populates category; populate trims which fields come back\n * populate: { categories: { title: true, slug: true } }\n */\n populate?: Record<string, boolean | Record<string, boolean>>\n /**\n * Controls Payload `type: 'join'` virtual reverse-relation fields only (pagination, sort,\n * filter, count per join field, or `false` to disable all join-field population).\n *\n * Does NOT populate normal relationship fields like `category`, `images`, or `brand`.\n * For normal relationship population use `depth` (and optionally `populate` for field\n * selection).\n *\n * Pass `joins: false` to disable all join-field population — useful for lightweight list\n * queries where join fields are not needed.\n *\n * @example\n * // `article-authors` has a `type: 'join'` field `articles` (reverse-relation)\n * joins: { articles: { limit: 10, sort: '-publishedAt' } }\n *\n * // depth: 2 populates product.category — joins has no effect on this\n * depth: 2\n *\n * // Disable all join-field population\n * joins: false\n */\n joins?:\n | Record<\n string,\n | {\n limit?: number\n page?: number\n sort?: string\n where?: Where\n count?: boolean\n }\n | false\n >\n | false\n /** Set to `false` to skip the count query — returns docs without totalDocs/totalPages */\n pagination?: boolean\n /** Include draft versions (access control still applies on the server) */\n draft?: boolean\n /** Include soft-deleted documents (requires `trash` enabled on the collection) */\n trash?: boolean\n}\n\n// ============================================================================\n// Debug & Retry Configuration\n// ============================================================================\n\nexport interface DebugConfig {\n logRequests?: boolean\n logResponses?: boolean\n logErrors?: boolean\n}\n\nexport interface RetryConfig {\n maxRetries?: number\n retryableStatuses?: number[]\n retryDelay?: (attempt: number) => number\n}\n\n// ============================================================================\n// Lightweight root entry contracts\n// ============================================================================\n\ninterface RootQueryLookup<T extends string> {\n find(\n options?: ApiQueryOptions,\n ): Promise<PayloadFindResponse<CollectionType<T>>>\n findById(\n id: string | number,\n options?: ApiQueryOptions,\n ): Promise<CollectionType<T>>\n count(options?: ApiQueryOptions): Promise<{ totalDocs: number }>\n}\n\nexport type RootReadOnlyQueryBuilder<T extends PublicCollection> =\n RootQueryLookup<T>\n\nexport interface RootServerQueryBuilder<\n T extends ServerCollection,\n> extends RootQueryLookup<T> {\n create(\n data: Partial<CollectionType<T>>,\n options?: { file?: File | Blob; filename?: string },\n ): Promise<PayloadMutationResponse<CollectionType<T>>>\n update(\n id: string,\n data: Partial<CollectionType<T>>,\n options?: { file?: File | Blob; filename?: string },\n ): Promise<PayloadMutationResponse<CollectionType<T>>>\n updateMany(\n where: ApiQueryOptions['where'],\n data: Partial<CollectionType<T>>,\n ): Promise<PayloadFindResponse<CollectionType<T>>>\n remove(id: string): Promise<CollectionType<T>>\n removeMany(\n where: ApiQueryOptions['where'],\n ): Promise<PayloadFindResponse<CollectionType<T>>>\n}\n\nexport interface RootCollectionClient {\n from<T extends PublicCollection>(collection: T): RootReadOnlyQueryBuilder<T>\n}\n\nexport interface RootServerCollectionClient {\n from<T extends ServerCollection>(collection: T): RootServerQueryBuilder<T>\n}\n\nexport interface RootClient {\n commerce: CommerceClient\n community: CommunityClient\n /** Set on {@link createClient} return values; optional for structural mocks. */\n events?: EventsClient\n customer: CustomerNamespace\n collections: RootCollectionClient\n lastRequestId: string | null\n getState(): ClientState\n getConfig(): ClientConfig\n}\n\nexport interface RootServerClient {\n commerce: ServerCommerceClient\n tenant: TenantIntrospectionClient\n /** Set on {@link createServerClient} return values; optional for structural mocks. */\n events?: EventsClient\n community: CommunityClient & {\n moderation: {\n banCustomer: (p: BanCustomerParams) => Promise<CommunityBan>\n unbanCustomer: (p: UnbanCustomerParams) => Promise<{ success: true }>\n }\n }\n collections: RootServerCollectionClient\n lastRequestId: string | null\n getState(): ClientState\n getConfig(): Omit<ClientServerConfig, 'secretKey'>\n}\n\nexport type RootClientWithEvents = RootClient & { events: EventsClient }\n\nexport type RootServerClientWithEvents = RootServerClient & { events: EventsClient }\n\n// ============================================================================\n// Type Utilities\n// ============================================================================\n\nexport type DeepPartial<T> = {\n [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]\n}\n\nexport type ExtractArrayType<T> = T extends (infer U)[] ? U : never\n","/**\n * @01.software/sdk — Analytics Helper\n */\n\n/* ANALYTICS INVARIANTS START\n * @01.software/sdk — Analytics Helper\n *\n * ANALYTICS INVARIANTS\n * ====================\n * These invariants are the single source of truth for observable behavior.\n * They are mirrored verbatim in apps/console/src/app/api/analytics/script.js/route.ts.\n * Any change here MUST be reflected there, and vice versa.\n *\n * 1. DNT/GPC respect: when config.respectDnt !== false (default true) AND\n * (navigator.doNotTrack === '1' OR navigator.globalPrivacyControl === true),\n * all methods become no-ops. Zero network requests are made.\n *\n * 2. Prerender skip: when document.prerendering === true OR\n * document.visibilityState === 'prerender', pageview() sends zero requests.\n *\n * 3. 500ms same-path dedup: a pageview for the same pathname within 500ms of\n * the previous send is silently dropped. After 500ms the next call sends.\n *\n * 4. Transport: sendBeacon → fetch keepalive fallback.\n * Primary: navigator.sendBeacon(endpoint, new Blob([json], { type: 'text/plain' })).\n * Fallback (sendBeacon unavailable OR returns false):\n * fetch(endpoint, { method: 'POST', keepalive: true,\n * headers: { 'Content-Type': 'application/json' }, body: json }).catch(() => {})\n *\n * 5. Body-only publishableKey: publishableKey is always in the request body,\n * never in any HTTP header.\n *\n * 6. SSR no-op: when typeof window === 'undefined', createAnalytics() returns\n * a stub where all methods are no-ops. No side effects occur.\n *\n * 7. Error swallowing: all transport errors are caught and swallowed.\n * createAnalytics() and all returned methods never throw into the caller.\n *\n * 8. Client timestamp: every send carries eventTs (milliseconds since epoch)\n * captured with Date.now() immediately before transport. The collect\n * endpoint uses eventTs (a) to bucket the event into the client's\n * tenant-local day and (b) to enforce the late-arrival cutoff; events\n * submitted after the local-day-end grace window are dropped with\n * reason \"late\".\n * ANALYTICS INVARIANTS END */\n\nimport { resolveApiUrl } from './core/client/types'\n\n// ============================================================================\n// Public Types\n// ============================================================================\n\nexport interface AnalyticsConfig {\n publishableKey: string\n /** Override the collect endpoint URL. Defaults to {SDK_BASE_URL}/api/analytics/collect */\n endpoint?: string\n /** Auto-patch history.pushState/replaceState and listen to popstate. Default: true */\n autoTrack?: boolean\n /** Respect navigator.doNotTrack and navigator.globalPrivacyControl. Default: true */\n respectDnt?: boolean\n}\n\nexport interface Analytics {\n pageview(path?: string): void\n track(name: string, props?: Record<string, string | number | boolean>): void\n destroy(): void\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\nexport function createAnalytics(config: AnalyticsConfig): Analytics {\n // INVARIANT 6: SSR no-op\n if (typeof window === 'undefined') {\n return { pageview() {}, track() {}, destroy() {} }\n }\n\n const endpoint =\n config.endpoint ?? `${resolveApiUrl()}/api/analytics/collect`\n\n // INVARIANT 1: DNT/GPC check (evaluated once at init; stays as closure)\n const respectDnt = config.respectDnt !== false\n function isDntActive(): boolean {\n if (!respectDnt) return false\n const nav = navigator as Navigator & { globalPrivacyControl?: boolean }\n return nav.doNotTrack === '1' || nav.globalPrivacyControl === true\n }\n\n // INVARIANT 3: 500ms same-path dedup state\n let lastPath: string | null = null\n let lastAt = 0\n\n // autoTrack state — save originals for destroy()\n const autoTrack = config.autoTrack !== false\n const originalPushState = history.pushState\n const originalReplaceState = history.replaceState\n let destroyed = false\n\n // -------------------------------------------------------------------------\n // Core send logic\n // -------------------------------------------------------------------------\n\n // Generate a unique event ID (crypto.randomUUID when available, Date+Math.random fallback)\n function newEventId(): string {\n return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\n ? crypto.randomUUID()\n : String(Date.now()) + String(Math.random())\n }\n\n // INVARIANT 4: sendBeacon → fetch keepalive fallback\n // INVARIANT 5: publishableKey in body only\n function sendBeaconOrFetch(body: string): void {\n try {\n if (typeof navigator.sendBeacon === 'function') {\n const blob = new Blob([body], { type: 'text/plain' })\n const sent = navigator.sendBeacon(endpoint, blob)\n if (sent) return\n // sent === false → fall through to fetch\n }\n // Fetch fallback\n fetch(endpoint, {\n method: 'POST',\n keepalive: true,\n headers: { 'Content-Type': 'application/json' },\n body,\n }).catch(() => {})\n } catch {\n // INVARIANT 7: swallow all errors\n }\n }\n\n function sendPageview(pathname: string): void {\n // INVARIANT 1: DNT/GPC\n if (isDntActive()) return\n\n // INVARIANT 2: prerender skip\n const doc = document as Document & { prerendering?: boolean }\n // visibilityState cast to string to accommodate non-standard 'prerender' value\n if (doc.prerendering === true || (document.visibilityState as string) === 'prerender') return\n\n // INVARIANT 3: 500ms same-path dedup\n const now = Date.now()\n if (pathname === lastPath && now - lastAt < 500) return\n lastPath = pathname\n lastAt = now\n\n const body = JSON.stringify({\n publishableKey: config.publishableKey,\n pathname,\n referrer: document.referrer || '',\n eventId: newEventId(),\n eventTs: Date.now(),\n })\n\n sendBeaconOrFetch(body)\n }\n\n // -------------------------------------------------------------------------\n // autoTrack: patch history methods + listen to popstate\n // -------------------------------------------------------------------------\n function trackCurrentPath(): void {\n if (destroyed) return\n sendPageview(location.pathname)\n }\n\n function patchedPushState(\n this: History,\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalPushState.apply(this, [data, unused, url] as Parameters<typeof history.pushState>)\n if (!destroyed) setTimeout(trackCurrentPath, 0)\n }\n\n function patchedReplaceState(\n this: History,\n data: unknown,\n unused: string,\n url?: string | URL | null,\n ): void {\n originalReplaceState.apply(this, [data, unused, url] as Parameters<typeof history.replaceState>)\n if (!destroyed) setTimeout(trackCurrentPath, 0)\n }\n\n if (autoTrack) {\n history.pushState = patchedPushState\n history.replaceState = patchedReplaceState\n window.addEventListener('popstate', trackCurrentPath)\n\n // Initial pageview\n if (document.readyState === 'complete') {\n trackCurrentPath()\n } else {\n window.addEventListener('load', trackCurrentPath, { once: true })\n }\n }\n\n // -------------------------------------------------------------------------\n // track() — client-side validation + send\n // -------------------------------------------------------------------------\n\n // Dev-mode detection: warn in dev, silent in production.\n // process.env.NODE_ENV is unreliable in browser bundles (tsup does not replace it\n // by default). Instead we detect production at runtime via hostname heuristics.\n // SSR (window undefined) is caught at the top of createAnalytics and returns a\n // stub, so window is always defined here.\n const isProduction: boolean = (() => {\n try {\n const hostname = location.hostname\n return (\n hostname !== 'localhost' &&\n hostname !== '127.0.0.1' &&\n !hostname.endsWith('.local')\n )\n } catch {\n // hostname access failed (non-browser) — default to silent\n return true\n }\n })()\n\n // One-shot warn dedup per reason per page load (keyed by reason only)\n const warnedReasons = new Set<string>()\n\n function devWarn(name: string, reason: string): void {\n if (isProduction) return\n if (warnedReasons.has(reason)) return\n warnedReasons.add(reason)\n console.warn(`[01 analytics] dropped event ${name}: ${reason}`)\n }\n\n const EVENT_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_:-]{0,49}$/\n const RESERVED_PREFIXES = ['__', '_pv_']\n\n function validateEventName(name: string): string | null {\n if (!name || typeof name !== 'string') return 'name-empty'\n for (const prefix of RESERVED_PREFIXES) {\n if (name.startsWith(prefix)) return 'name-reserved'\n }\n if (!EVENT_NAME_RE.test(name)) return 'name-regex'\n return null\n }\n\n const PROP_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,31}$/\n\n function validateEventProps(\n props: Record<string, string | number | boolean> | undefined,\n ): string | null {\n if (props === undefined || props === null) return null\n if (typeof props !== 'object' || Array.isArray(props)) return 'props-value-type'\n const keys = Object.keys(props)\n if (keys.length > 10) return 'props-too-many-keys'\n for (const k of keys) {\n const v = props[k]\n if (!PROP_KEY_RE.test(k)) return 'props-key-regex'\n if (typeof v === 'string') {\n if (v.length > 80) return 'props-value-too-long'\n } else if (typeof v === 'number') {\n if (!isFinite(v)) return 'props-value-not-finite'\n } else if (typeof v === 'boolean') {\n // ok\n } else {\n return 'props-value-type'\n }\n }\n return null\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n return {\n pageview(path?: string): void {\n if (destroyed) return\n sendPageview(path ?? location.pathname)\n },\n\n track(name: string, props?: Record<string, string | number | boolean>): void {\n if (destroyed) return\n\n // INVARIANT 1: DNT/GPC (same as pageview)\n if (isDntActive()) return\n\n // INVARIANT 2: prerender skip\n const doc = document as Document & { prerendering?: boolean }\n if (doc.prerendering === true || (document.visibilityState as string) === 'prerender') return\n\n // Client-side validation\n const nameErr = validateEventName(name)\n if (nameErr) {\n devWarn(name, nameErr)\n return\n }\n\n if (props !== undefined) {\n const propsErr = validateEventProps(props)\n if (propsErr) {\n devWarn(name, propsErr)\n return\n }\n }\n\n // Build body — no dedup for track() events\n const body = JSON.stringify({\n publishableKey: config.publishableKey,\n pathname: location.pathname,\n referrer: document.referrer || '',\n eventId: newEventId(),\n eventName: name,\n eventProps: props,\n eventTs: Date.now(),\n })\n\n sendBeaconOrFetch(body)\n },\n\n destroy(): void {\n if (destroyed) return\n destroyed = true\n\n if (autoTrack) {\n // Restore original history methods\n history.pushState = originalPushState\n history.replaceState = originalReplaceState\n window.removeEventListener('popstate', trackCurrentPath)\n }\n\n // Null out dedup state\n lastPath = null\n lastAt = 0\n },\n }\n}\n","export type AnalyticsRuntimeEnv = {\n nextPublicKey?: string\n vitePublicKey?: string\n}\n\nexport function readAnalyticsRuntimeEnv(): AnalyticsRuntimeEnv {\n const nextPublicKey =\n typeof process !== 'undefined'\n ? process.env?.NEXT_PUBLIC_SOFTWARE_PUBLISHABLE_KEY\n : undefined\n\n const viteEnv = (\n import.meta as ImportMeta & {\n env?: Record<string, string | undefined>\n }\n ).env\n\n return {\n nextPublicKey,\n vitePublicKey: viteEnv?.VITE_SOFTWARE_PUBLISHABLE_KEY,\n }\n}\n\nexport function resolveAnalyticsPublishableKey(\n explicit?: string,\n env: AnalyticsRuntimeEnv = readAnalyticsRuntimeEnv(),\n): string | undefined {\n if (explicit !== undefined) return explicit || undefined\n\n return env.nextPublicKey || env.vitePublicKey\n}\n"],"mappings":"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","names":[]}
|