@vio-live/web-sdk 0.24.0 → 0.26.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
@@ -179,10 +179,12 @@ template that wants slots shared across articles simply sets no scope.
179
179
 
180
180
  ### Declaration
181
181
 
182
- Every mounted element registers its id (and label); after the first successful
183
- `Vio.bootstrap()` the SDK sends **one** manifest for the whole page
184
- (`POST /v2/mobile/components/manifest`, `mode: 'merge'`, so one page never
185
- deprecates another page's slots) and again whenever a new id or label appears.
182
+ Every mounted element registers its id, its label and its **kind** (`card` |
183
+ `carousel` | `detail` — each manifest location is `{ id, displayName, kind }`; the
184
+ campaigns micro stores the kind from 2026-10 on and ignored the key before); after
185
+ the first successful `Vio.bootstrap()` the SDK sends **one** manifest for the whole
186
+ page (`POST /v2/mobile/components/manifest`, `mode: 'merge'`, so one page never
187
+ deprecates another page's slots) and again whenever a new id, label or kind appears.
186
188
  The browser remembers **each id — under its label and the campaign it was
187
189
  declared with — for 24 h** in `localStorage` (under the platform and a hash of
188
190
  the key — never the key itself; one state per id), so a multi-page site posts
@@ -194,10 +196,16 @@ once). Best-effort, never throws. By hand, e.g. from an editor (`force` re-sends
194
196
  everything and ignores the per-load cap):
195
197
 
196
198
  ```ts
197
- await Vio.declareSlots(['hero', { id: 'sidebar', displayName: 'Sidebar' }], { force: true })
199
+ await Vio.declareSlots(['hero', { id: 'sidebar', displayName: 'Sidebar', kind: 'carousel' }], { force: true })
198
200
  // → ManifestResult | null (null = skipped as already declared, or failed — see the console)
201
+
202
+ // What an element does on mount (a host block can too). `(id, name, campaignId)` still works.
203
+ const release = Vio.registerSlot('sidebar', 'Sidebar', { kind: 'carousel', campaignId: 12 })
199
204
  ```
200
205
 
206
+ The first kind an id gets stays — the cards a carousel renders share the carousel's
207
+ id, and the slot is the carousel's.
208
+
201
209
  ### Which campaign a slot shows
202
210
 
203
211
  Among the active campaigns of the bootstrap, the one whose assignment names the
@@ -266,7 +274,7 @@ and so do the Vev blocks and the Replit site:
266
274
 
267
275
  ```ts
268
276
  Vio.attribution('hero')
269
- // → { slot: 'hero', campaignId: 12, sponsorId: 7, contentId: 'article-42', contentTitle: 'Why the lamp matters' }
277
+ // → { slot: 'hero', campaignId: 12, sponsorId: 7, contentId: 'article-42', contentTitle: 'Why the lamp matters', contentUrl: 'https://host.example/article-42' } // contentUrl: origin + path
270
278
  ```
271
279
 
272
280
  `campaignId`/`sponsorId` are the slot's campaign and its primary sponsor (`{}` before
@@ -296,13 +304,20 @@ sponsor's campaign → the default. Two things changed from
296
304
  0.22 for carousels: a carousel's impressions and `view_item_list` report
297
305
  `location_id` as its slot id (`carousel-1`, or the `vio-slot`) instead of its
298
306
  `label`, and every event now carries `content_id` / `content_title` — including
299
- the single-campaign `begin_checkout` / `purchase`.
307
+ the single-campaign `begin_checkout` / `purchase`. Since 0.25 every element also
308
+ says **what it is**: `component_template_id` is `product-card` for a card,
309
+ `product-carousel` for a carousel — and for the cards it renders, so a carousel's
310
+ impressions and its clicks count under one component type — and `product-detail`
311
+ for the detail (its `view_item`, `add_to_cart` and `begin_checkout`). A host that
312
+ lays cards out itself can set `component-template-id` on them (`product-grid`).
300
313
 
301
314
  ### Cart origins and the purchase per campaign
302
315
 
303
316
  The cart remembers **where every unit came from**. The add paths take an optional
304
- `origin` (`{ campaignId?, slot?, contentId? }` — what `Vio.attribution(slot)`
305
- answers), and so does `adjustQuantity(lineItemId, sponsorId, delta, { origin })`:
317
+ `origin` (`{ campaignId?, slot?, contentId?, componentKind?, contentUrl?, contentTitle? }`
318
+ — what `Vio.attribution(slot)` answers, plus the kind of element; the elements pass
319
+ their `componentTemplateId` as it), and so does
320
+ `adjustQuantity(lineItemId, sponsorId, delta, { origin })`:
306
321
 
307
322
  ```ts
308
323
  Vio.cart.addProduct({ product, sponsorId: 1, origin: Vio.attribution('hero') })
@@ -310,10 +325,13 @@ Vio.cart.addProduct({ product, sponsorId: 1, origin: Vio.attribution('hero') })
310
325
  onProductAdd={(e) => Vio.cart.addProduct({ product, sponsorId, origin: e.detail.origin })}
311
326
  ```
312
327
 
313
- Each line keeps `origins: Array<{ campaignId?, slot?, contentId?, quantity }>`: an
314
- add with an origin increments that origin, without one the most recent (the
315
- `<vio-cart>` +/− buttons pass none), and −1/remove comes off the most recent first.
316
- They persist with the cart; carts from before origins read as one empty origin.
328
+ Each line keeps `origins: Array<{ campaignId?, slot?, contentId?, componentKind?,
329
+ contentUrl?, contentTitle?, quantity }>`: an add with an origin increments that
330
+ origin, without one the most recent (the `<vio-cart>` +/− buttons pass none), and
331
+ −1/remove comes off the most recent first. A place is campaign + slot + content +
332
+ kind of element (an add from a card and one from the detail of the same slot are two
333
+ origins); the page's URL and title are attributes of it. They persist with the cart;
334
+ carts from before origins read as one empty origin.
317
335
 
318
336
  At checkout and purchase the cart is **split per campaign**: `begin_checkout` and
319
337
  `purchase` go out **once per campaign in the cart**, each with that campaign's
@@ -334,6 +352,107 @@ Vio.cartSlices(undefined, { items }) // pure, over a snapshot of lines
334
352
  // → CartSlice[]: { campaignId?, sponsorId?, slot?, contentId?, items: VioItem[], value, currency?, slice, slices }
335
353
  ```
336
354
 
355
+ ### Attribution on the order — what Commerce receives (0.26)
356
+
357
+ Since 0.26 the origins do not stay in the browser: **Commerce receives them**
358
+ (ADR-0035 phase B), so the order is born knowing where it was sold from.
359
+
360
+ - **Per line.** Every `AddItem` carries the line's `origin` — `{ campaign_id,
361
+ location_id (the slot), component_kind, content_url, content_title }` — and a
362
+ quantity **increase** carries, on `UpdateItem`, the origin of the units it adds (the
363
+ given one, else the line's most recent; a burst of +1 from two places is two
364
+ updates, each with its own origin); a decrease and a removal carry none, and an add
365
+ without an origin sends none (the line inherits the order's campaign on the
366
+ server). `component_kind` is the kind of element as the collector names it too
367
+ (`card` | `carousel` | `detail`: the element's `componentTemplateId` without its
368
+ `product-` / `vev-` prefix, else the kind word in the slot id), so
369
+ `order_item.component_kind` and `breakdown?by=component_kind` agree. shopcart keeps
370
+ every origin of a line with its units and writes `order_item.campaign_id` /
371
+ `location_id` / `component_kind` from them.
372
+ - **Per checkout.** `CreateCheckout` and the `UpdateCheckout` right before paying
373
+ carry `attribution`: `surface_key` (the SDK key — the gateway sets it from
374
+ `X-API-Key` anyway, and shopcart checks it against the cart's channel, so a page
375
+ cannot credit a sale to another surface), `campaign_id` (the page's campaign,
376
+ `Vio.pageCampaign()` — else the campaign that explains most units of the cart under
377
+ the same rule `purchase` is split by), `content_url` / `content_title` (the page at
378
+ pay time) and `entry` (`{ referrer, utm_source, utm_medium, utm_campaign,
379
+ landing_url }`: the landing the session **remembers** — the same memory the
380
+ `client` block reads, which now keeps the landing page too — never the page the
381
+ shopper pays on taken for a landing; without a remembered landing there is no
382
+ `entry`). They become `order.surface_id` / `campaign_id` / `content_url` /
383
+ `content_title` / `entry_source`. `Vio.orderAttribution(sponsorId?)` answers what
384
+ would be sent — synchronously: nothing on the way to a payment waits for it.
385
+ - **`content_url` is origin + path**, no query string, no fragment — on the order,
386
+ on the lines and in the landing: the query is where a page carries e-mails, tokens
387
+ and tracking tails, and none of that belongs on an order row. A site whose article
388
+ lives in the query or the hash identifies it through `contentId` (`Vio.init({
389
+ contentId })`, `?vio_content=`, the `vio:content-id` meta), not through the URL.
390
+ The analytics events keep `location.href` as before.
391
+ - **Privacy.** On a **`minimal`** surface what says *which screen* a unit was sold
392
+ from and *why the shopper was there* stays off Commerce — the same promise the
393
+ collector keeps for the events (vio-analytics `privacy.ts`): no `content_url`, no
394
+ `content_title`, no `location_id` on the lines, no `entry`; `surface_key`,
395
+ `campaign_id` and `component_kind` do travel. The mode comes from
396
+ `Vio.init({ privacyMode: 'minimal' })` when the host knows it (always wins), else
397
+ the bootstrap's `surface.privacyMode`, else the surface itself: `GET
398
+ /v2/mobile/surface`, asked **once at `Vio.init()`** (and again by `bootstrap()` if
399
+ that failed), given 1.5 s, believed for the page load — a failure or a timeout is
400
+ believed for a minute, then asked again; a service that predates the endpoint (404)
401
+ means `standard`; a word the SDK does not know does not. The payment path reads
402
+ what is known at that moment (`Vio.privacyModeNow()`) and never waits; an unknown
403
+ mode counts as `minimal`. The cart's lines wait for the ask in flight, at most its
404
+ cap (`Vio.privacyMode()`).
405
+ - **A gateway that predates these arguments** (`origin` on `LineItemInput` /
406
+ `UpdateItem`, `attribution` on `CreateCheckout` / `UpdateCheckout`) refuses the
407
+ operation at validation, before anything runs: the SDK sends it again without them,
408
+ once per page load, says so in the console, and the cart works as in 0.25. Nothing
409
+ that ran — data in the answer, or an error with a `path` — is ever repeated.
410
+
411
+ Everything is capped to Commerce's columns in one place (`lineOriginWire`,
412
+ `checkoutAttributionOf`, exported from `/core`): slot 100, kind 32, URL 2 048, title
413
+ 255, referrer 2 048, utm 128; `campaign_id` only as a positive 32-bit `Int`.
414
+
415
+ ### The visit and the buyer — `client` and `buyer_id` (0.25)
416
+
417
+ Every event carries a **`client`** block, the standard context of the visit:
418
+ `language` (`navigator.language`), the **landing page's** `referrer` and
419
+ `utm_source` / `utm_medium` / `utm_campaign` / `utm_term` / `utm_content`,
420
+ `screen_w` / `screen_h` and `tz` (IANA). The landing is read once and remembered
421
+ for the session in `sessionStorage` (`vio.landing.v1`), so the third page of a visit
422
+ still says it came from the newsletter — a page's own internal referrer is never
423
+ sent. The referrer is kept as origin + path (no query string, no fragment) and
424
+ dropped when it is this same host (a self-referral says nothing about the entry). A
425
+ URL with utm parameters is a new landing that replaces the memory — unless it was
426
+ reached from this same host, i.e. an internal link that propagates utm, which keeps
427
+ the landing that was. With `requireConsent` nothing is written until
428
+ `setConsent(true)`, and `setConsent(false)` forgets it. Device type, OS, browser and
429
+ country are **not** sent: the collector derives them from the request itself. The
430
+ block is computed once per page load (capped to the collector's limits) and the
431
+ same object rides on every event. `resolveClientContext()` / `resolveLanding()` are
432
+ exported from `/core` for hosts with their own pipeline.
433
+
434
+ A `purchase` carries **`commerce.buyer_id`**: shopcart's `buyer_ref`, the keyed hash
435
+ Commerce computes from the buyer's email — one person, one ref across orders and
436
+ devices, nothing reversible — so a campaign can count distinct and repeat buyers.
437
+ The SDK never sends an email and never hashes one itself: it forwards the reference
438
+ the checkout answers (the order-number query, the Apple Pay and the Klarna native
439
+ confirmations select `buyer_ref`; a gateway that predates the field is asked again
440
+ without it, once per page load) and omits it otherwise. `Vio.checkout.getOrderReference(checkoutId, sponsorId?)`
441
+ answers `{ orderNumber, buyerRef }` (never throws; concurrent asks share one
442
+ request); `getOrderNumber` is its order-number half. The `vio:payment-success`
443
+ detail carries it as `buyerRef`; an emitter that has to ask for it asks at once (and
444
+ again at 1.2 s while the checkout has neither order nor reference — a webhook may
445
+ still be writing it), waits at most 2.5 s, fires the event at once without it when the
446
+ page hides or the element is removed, and never asks when it cannot help (the
447
+ gateway has no `buyer_ref` yet; a confirmation that already answered). The
448
+ confirmation screen never waits. A host with its own checkout passes `buyerRef` (or
449
+ `buyer_ref`) in its `vio:payment-success` / `vio:checkout-open` detail, or
450
+ `commerce.buyerId` to `Vio.analytics.track()`; anything that is not a reference
451
+ (an email, more than 128 chars, anything outside `[A-Za-z0-9._~-]`) is dropped
452
+ (`normalizeBuyerRef` / `buyerRefOf`, exported from `/core`). **Without consent**
453
+ (`requireConsent` until `setConsent(true)`) `buyer_id` is not sent at all: a visit
454
+ measured with ephemeral ids carries no stable identifier of a person.
455
+
337
456
  ## Theming
338
457
 
339
458
  Every component reads its colors and fonts from `--vio-*` CSS custom properties