@revturbine/sdk 0.10.0 → 0.10.2

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/CHANGELOG.md CHANGED
@@ -22,12 +22,19 @@ A change whose "Fail-closed in" is later than its "Landed in" had a window where
22
22
  the old shape was accepted and did nothing. Those windows are the expensive ones.
23
23
 
24
24
  > **What this file is not yet.** A CI guard fails any PR that changes a
25
- > `@public`-tagged export signature without touching this file. That tagging is
26
- > thin: as of `0.8.0` it covers **4** methods on `RevTurbineCustomerSdk`, which
27
- > declares roughly **73**. `can()`, `getPlacementDecision()`, `identify()` and
28
- > `dismiss()` are among the untagged, so a breaking change to one of them will
29
- > not be caught automatically — it depends on whoever writes the PR. Entries here
30
- > are reliable; the absence of an entry is not yet proof that nothing changed.
25
+ > `@public`-tagged export signature without touching this file. That tagging
26
+ > was thin (4 of ~73 methods) until BL-0005 (2026-09-22, no release — TSDoc
27
+ > and snapshot only) widened it to **34** of `RevTurbineCustomerSdk`'s **67**
28
+ > non-private methods, plus 5 provider/hook entry points
29
+ > (`initRevTurbine`, `RevTurbineProvider`, `useCan`, `useEntitlement`,
30
+ > `useGatedAction`) — every method a customer-facing guide, skill, the
31
+ > starter, or a demo app calls directly. The remaining ~33 methods are
32
+ > internal-only paths (called only by the SDK's own placement components,
33
+ > React bindings, or other internal packages) and stay untagged, so a
34
+ > breaking change to one of *those* still will not be caught automatically —
35
+ > it depends on whoever writes the PR. Entries here are reliable; the absence
36
+ > of an entry for a currently-untagged method is not yet proof that nothing
37
+ > changed.
31
38
 
32
39
  > **Note on 0.x.** These packages are pre-1.0, so breaking changes ship in the
33
40
  > **minor** position (`0.6.0` → `0.7.0`), not the major. `npm`'s caret on a `0.x`
@@ -40,6 +47,93 @@ also require a changelog entry.
40
47
 
41
48
  ---
42
49
 
50
+ ## 0.10.2
51
+
52
+ ### Local mode resolves placements by slot id (BL-0119)
53
+
54
+ **What changed.** In `local_only` mode the browser SDK now resolves a placement
55
+ request against the Playbook's authored `placement_slots[]` registry.
56
+ `getPlacement({ slotId })` — with or without a `componentType` — previously
57
+ returned `null` for a slot the Playbook declared, and a controller/`usePlacement`
58
+ mount of that slot resolved to `placement_not_found`. Looking the same placement
59
+ up by **name** worked, which is what made this look like a config problem rather
60
+ than an SDK one.
61
+
62
+ Two things were missing, and both are now in place:
63
+
64
+ - **`registerSurfaceSlot()` adopts the declared template.** `surface_template_ids`
65
+ on the registered record is what puts the shared resolver on its slot branch,
66
+ where `trigger.slot_id` is matched. Without it the resolver fell back to direct
67
+ lookup, which is keyed by placement **name**/id and therefore can never match a
68
+ slot id — hence `placement_not_found`. An id passed explicitly at the call site
69
+ still wins; the mounting code knows what it renders.
70
+ - **`getPlacement()` falls through to the slot registry.** The local placement
71
+ cache only holds slots a decision has already run through, so a cold lookup
72
+ found nothing and stopped. It now derives the record from `placement_slots`,
73
+ matching on `id` and falling back to `surface_type` when only a component type
74
+ is given — the same derivation scaffold's headless
75
+ `LocalRuntime.slotRecordForConfig` has always performed. Registration on this
76
+ path is local only: reading a placement never writes a surface slot back to the
77
+ control plane.
78
+
79
+ This closes a browser-versus-headless divergence: the same Playbook decided
80
+ differently depending on which runtime read it. No public API change — no
81
+ signature moved, and nothing new is asked of the caller. A slot id that no
82
+ `placement_slots` entry declares still resolves to `null`, unchanged.
83
+
84
+ **Landed in** `0.10.2`. **Fail-closed in** n/a — a previously-`null` lookup now
85
+ returns the placement the Playbook authored.
86
+
87
+ **Proving test:** `web-sdk/local-slot-id-lookup.test.ts` — by-slot-id and
88
+ by-slot-id-plus-component-type both resolve, the controller path no longer
89
+ reports `placement_not_found`, an undeclared slot id still returns `null` and
90
+ gains no invented template, an explicit `surfaceTemplateIds` still wins, and the
91
+ by-name lookup that always worked keeps working (the positive control).
92
+
93
+ ---
94
+
95
+ ## 0.10.1
96
+
97
+ ### Treatment interactions carry the identified `account_id`
98
+
99
+ **What changed.** Every treatment interaction the SDK sends to
100
+ `/api/events/interactions` — the impression that writes a
101
+ `placement_presentations` row, plus dismiss / click / conversion — now carries
102
+ `account_id`, taken from the account the integration identified
103
+ (`identify(userId, { account_id })` / `setUserContext`). It was never sent at
104
+ all, so the ingest route's `account_id ?? user_id` fallback stamped a **user**
105
+ id into an **account** column. `placement_presentations.account_id` is a join
106
+ key, not a label: `monetization_funnel` matches it against account ids from
107
+ `events_clickstream` / `events_billing`, and every experiment summary pipe
108
+ reads it when `analysis_unit='account'`. Those joins therefore matched only
109
+ where a user id happened to equal an account id — and an account-grain
110
+ experiment readout returned the user-grain n while looking perfectly valid.
111
+
112
+ Two details make it a usable key. The value is PII-redacted **identically to
113
+ `/api/track`**, so an email-shaped account id becomes the same hash in
114
+ `placement_presentations` and in `events_clickstream` and the funnel actually
115
+ joins. And when no account was identified the field is **omitted**, never set
116
+ to the user id — the route keeps its own fallback for SDKs that have not
117
+ upgraded, but a wrong join key is worse than a missing one.
118
+
119
+ No public API change: nothing new is asked of the caller, and
120
+ `RevTurbineTreatmentInteractionInput` is unchanged. Integrations that already
121
+ pass `account_id` to `identify()` get correct account-grain analytics with no
122
+ code change; integrations that never identify an account are unaffected on the
123
+ wire and see the same route-side fallback as before.
124
+
125
+ **Landed in** `0.10.1`. **Fail-closed in** n/a — additive; nothing old is
126
+ rejected.
127
+
128
+ **Proving test:** `web-sdk/interaction-wire-contract.test.ts` — "the account
129
+ identity the analytics joins key on": the identified account reaches the wire
130
+ distinct from `user_id`, an unidentified one is omitted rather than copied from
131
+ `user_id`, an email-shaped account id matches the `/api/track` lane
132
+ byte-for-byte, and a re-queued batch keeps the account that was acting when
133
+ each interaction happened.
134
+
135
+ ---
136
+
43
137
  ## 0.10.0
44
138
 
45
139
  ### `publicKey` is the browser credential; `apiKey` is the server key