@revturbine/sdk 0.11.6 → 0.11.8
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 +133 -0
- package/dist/headless.d.ts +421 -13
- package/dist/headless.js +2 -2
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +421 -13
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/types/web-sdk/account-identity.d.ts +87 -0
- package/dist/types/web-sdk/account-identity.d.ts.map +1 -0
- package/dist/types/web-sdk/customer-side.d.ts +61 -6
- package/dist/types/web-sdk/customer-side.d.ts.map +1 -1
- package/dist/types/web-sdk/react/useEntitlement.d.ts.map +1 -1
- package/dist/types/web-sdk/react/usePlacement.d.ts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -47,6 +47,139 @@ also require a changelog entry.
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 0.11.8
|
|
51
|
+
|
|
52
|
+
### `account_id` falls back to a PREFIXED user key instead of a bare user id (BL-0117)
|
|
53
|
+
|
|
54
|
+
**What changed.** Every clickstream event the SDK sends to `/api/track` used to
|
|
55
|
+
carry `account_id: userContext.account_id || userId`. `TrackEvent.account_id`
|
|
56
|
+
was a required, min-length-1 field, so an app that identified no account had to
|
|
57
|
+
send *something* — and what it sent was the **user** id, bare and
|
|
58
|
+
indistinguishable from a real account. `monetization_funnel` and `cohort_rollup`
|
|
59
|
+
build their account map out of `events_clickstream.account_id`, and experiment
|
|
60
|
+
analysis reads that column whenever `analysis_unit = 'account'`: every such row
|
|
61
|
+
contributed a bogus `user_id → user_id` account, so an account-grain readout
|
|
62
|
+
returned a user-grain n while looking perfectly valid. The treatment-interaction
|
|
63
|
+
lane had the same hole from the other side — the SDK sent no `account_id` at all
|
|
64
|
+
and the ingest route's own `account_id ?? user_id` fallback stamped a user id
|
|
65
|
+
into `placement_presentations.account_id`.
|
|
66
|
+
|
|
67
|
+
Kent ruled on it, 2026-09-25 (**D-13**): *"Keep the user-id fallback but
|
|
68
|
+
explicitly prefix it to make it obvious its a fallback key."*
|
|
69
|
+
|
|
70
|
+
So the fallback stays — a row with no identified account still needs an
|
|
71
|
+
attribution handle, and a hole in a join key is not an improvement — but it is
|
|
72
|
+
now **self-describing**:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
identified account → account_id: "acct_acme"
|
|
76
|
+
no account → account_id: "user-fallback:user_123"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both wire lanes (`/api/track` → `events_clickstream.account_id`, and
|
|
80
|
+
`/api/events/interactions` → `placement_presentations.account_id`) resolve the
|
|
81
|
+
value through one private helper, so they cannot drift apart: the funnel joins
|
|
82
|
+
one against the other, and a divergence in trimming, hashing or fallback
|
|
83
|
+
derivation joins nothing. The fallback is derived from the same
|
|
84
|
+
`userContext.id || anonymousId` the row's `user_id` comes from and redacted the
|
|
85
|
+
same way, so an email-shaped user id is the SAME hash behind the marker on both
|
|
86
|
+
lanes. A blank or whitespace-only `account_id` is not an account and falls
|
|
87
|
+
through to the fallback.
|
|
88
|
+
|
|
89
|
+
Two new `@public` exports carry the contract to integrators and to anything
|
|
90
|
+
reading the warehouse:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { FALLBACK_ACCOUNT_ID_PREFIX, isFallbackAccountId } from '@revturbine/sdk';
|
|
94
|
+
|
|
95
|
+
FALLBACK_ACCOUNT_ID_PREFIX; // 'user-fallback:'
|
|
96
|
+
isFallbackAccountId('user-fallback:user_123'); // true — fabricated
|
|
97
|
+
isFallbackAccountId('acct_acme'); // false — a real account
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`isFallbackAccountId` is position- and case-sensitive by contract, matching the
|
|
101
|
+
read-time SQL guards: an account id that merely *contains* the marker is a real
|
|
102
|
+
account, and the bare prefix with nothing after it is not a key at all. The same
|
|
103
|
+
constant and predicate ship in `server-python`
|
|
104
|
+
(`revturbine.core.account_identity`) and `server-rust`
|
|
105
|
+
(`revturbine::account_identity`, re-exported at the crate root), and the
|
|
106
|
+
cross-language parity corpus drives all three
|
|
107
|
+
(`tests/parity/fixtures/account_id_fallback_prefix.json`) so the literal cannot
|
|
108
|
+
drift between ports.
|
|
109
|
+
|
|
110
|
+
**Absence is still valid on the wire.** `@revt-eng/schema` 0.1.325 (scaffold
|
|
111
|
+
[#375](https://github.com/revt-eng/revturbine-scaffold/pull/375)) made
|
|
112
|
+
`TrackEvent.account_id` optional, and web migration 014 made
|
|
113
|
+
`events_clickstream.account_id` nullable — absence means "no account
|
|
114
|
+
identified", and a producer with no user identity to derive from may still omit
|
|
115
|
+
the field. The browser SDK always has an identity (an un-identified visitor
|
|
116
|
+
still has the anonymous id), so its default emit always sends the prefixed
|
|
117
|
+
fallback rather than a hole.
|
|
118
|
+
|
|
119
|
+
No public API change to an existing signature: nothing moved, and nothing new is
|
|
120
|
+
asked of the caller. `identify(userId, { account_id })` is still the only way to
|
|
121
|
+
supply an account. Integrations that identify one see no wire change at all.
|
|
122
|
+
|
|
123
|
+
**Read-time consequence, stated plainly.** Account-grain analytics exclude
|
|
124
|
+
fallback-prefixed ids from account denominators, so an app that never identifies
|
|
125
|
+
an account now reports **zero** accounts rather than one-per-user. That is a
|
|
126
|
+
correction, not a regression — those rows were never accounts — but a dashboard
|
|
127
|
+
that silently read them as account-grain will show a drop.
|
|
128
|
+
|
|
129
|
+
**Landed in** `0.11.8`. **Fail-closed in** `0.11.8` — the bare fallback is gone
|
|
130
|
+
in the same version that introduced the prefixed one; there is no window where
|
|
131
|
+
both shapes are emitted.
|
|
132
|
+
|
|
133
|
+
**Proving test:** `web-sdk/account-identity.test.ts` (the literal, the
|
|
134
|
+
classifier's position/case sensitivity, and the bare-prefix and absence cases)
|
|
135
|
+
plus `web-sdk/interaction-wire-contract.test.ts` — "the account identity on the
|
|
136
|
+
/api/track wire" and "the account identity the analytics joins key on": an
|
|
137
|
+
identified user with no account produces `user-fallback:<user_id>` on both
|
|
138
|
+
lanes, the prefix survives `/api/track` JSON serialization byte-for-byte, an
|
|
139
|
+
identified account reaches the wire byte-identically to the value supplied, a
|
|
140
|
+
blank account id is treated as absence, an un-identified visitor derives from
|
|
141
|
+
the anonymous id, and every body parses against the canonical
|
|
142
|
+
`TrackEventSchema` / `TreatmentInteractionInputSchema`. Also
|
|
143
|
+
`web-sdk/customer-side-ingest.test.ts` on the plain `capture()` path,
|
|
144
|
+
`server-python/tests/test_account_identity.py`, the unit tests in
|
|
145
|
+
`server-rust/src/account_identity.rs`, and the three-way parity fixture.
|
|
146
|
+
|
|
147
|
+
## 0.11.7
|
|
148
|
+
|
|
149
|
+
### A rebuilt gate/slot re-runs its check, so a page inside `<Gate>` no longer renders blank (BL-0251)
|
|
150
|
+
|
|
151
|
+
**What changed.** `RevTurbineProvider` re-initializes and publishes a **new**
|
|
152
|
+
`sdk` instance whenever its `options` prop changes identity, with `isReady`
|
|
153
|
+
staying `true`. Hosts do this routinely: options derived from an async session
|
|
154
|
+
change once, shortly after first paint.
|
|
155
|
+
|
|
156
|
+
`useEntitlement` rebuilt its `EntitlementGate` on that change, but the effect
|
|
157
|
+
that *runs* the check keyed on `[autoCheck, isReady, handle, contextKey]` — none
|
|
158
|
+
of which move when only `sdk` does. The replacement gate was therefore never
|
|
159
|
+
checked, and an unchecked gate reports `result: null` with `isLoading: false`,
|
|
160
|
+
which `<Gate>` reads as *unresolved* and renders as `null`. A page whose whole
|
|
161
|
+
body sits inside a `<Gate>` rendered **nothing**, permanently, until the gate was
|
|
162
|
+
remounted — which is why navigating away and back "fixed" it and a hard refresh
|
|
163
|
+
hit it every time (the session settles after the gate mounts).
|
|
164
|
+
|
|
165
|
+
`usePlacement` had the same defect: `loadDecision` was memoized on
|
|
166
|
+
`[isReady, resolvedUserId]`, so a controller rebuilt for a new `sdk` **or** a
|
|
167
|
+
changed slot config was never loaded and the slot stayed empty for the life of
|
|
168
|
+
the mount.
|
|
169
|
+
|
|
170
|
+
Both auto-run dependency lists are now supersets of the lists that rebuild the
|
|
171
|
+
object they act on (`sdk` added to both; `placementKey` added to
|
|
172
|
+
`loadDecision`). No retry, no polling — the trigger simply covers every reason
|
|
173
|
+
the underlying object was replaced.
|
|
174
|
+
|
|
175
|
+
**Landed in.** `0.11.7`.
|
|
176
|
+
|
|
177
|
+
**Fail-closed in.** n/a — this was a fail-**blank**, not a tolerated old shape.
|
|
178
|
+
Nothing a host passes changes.
|
|
179
|
+
|
|
180
|
+
**Proving test.** `web-sdk/placements/AccessGateSurfaceSlot.sdk-rebuild.test.tsx`
|
|
181
|
+
and `web-sdk/react/usePlacement.sdk-rebuild.test.tsx`.
|
|
182
|
+
|
|
50
183
|
## 0.11.6
|
|
51
184
|
|
|
52
185
|
### `sdk.convert()` reloads the UserContext, so a mounted slot re-decides (BL-0004)
|