@revturbine/sdk 0.11.7 → 0.11.9
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 +162 -0
- package/dist/growthbook.d.ts +1 -1
- package/dist/headless.d.ts +751 -62
- package/dist/headless.js +2 -2
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +751 -62
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/optimizely.d.ts +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 +128 -6
- package/dist/types/web-sdk/customer-side.d.ts.map +1 -1
- package/dist/types/web-sdk/trial-revision.d.ts +157 -0
- package/dist/types/web-sdk/trial-revision.d.ts.map +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -47,6 +47,168 @@ also require a changelog entry.
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 0.11.9
|
|
51
|
+
|
|
52
|
+
### The SDK can record app-owned trial revisions and evidenced account creation (BL-0237, plan 276 TASK-13)
|
|
53
|
+
|
|
54
|
+
**What changed.** Three new `@public` methods on `RevTurbineCustomerSdk` and the
|
|
55
|
+
matching capability on the Python and Rust ports:
|
|
56
|
+
|
|
57
|
+
| Method | Does |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `recordTrialRevision(episode, labels?)` | Classifies one trial episode's facts and emits one `trial_revision` if — and only if — the evidence supports a revision |
|
|
60
|
+
| `syncTrialRevisions(labels?)` | Does the same for every episode the registered `'trial'` domain provider states |
|
|
61
|
+
| `recordAccountCreated(payload)` | Emits `account_created` with plan 276 REQ-3 evidence; refuses a user-grain source |
|
|
62
|
+
|
|
63
|
+
**Trial execution and ownership stay with your app.** RevTurbine runs no trial,
|
|
64
|
+
enrols nobody, and decides nothing about when a trial ended. There is no
|
|
65
|
+
trial-enrollment service and no server-side evaluation. These methods *record*
|
|
66
|
+
the fact your app states, after running the same pure classifier every
|
|
67
|
+
RevTurbine port runs — so the fact that reaches the warehouse agrees with the
|
|
68
|
+
fact your app believes.
|
|
69
|
+
|
|
70
|
+
**The classifier reads no clock.** An episode whose scheduled end has merely
|
|
71
|
+
passed emits **nothing** and comes back `pending_unknown`. An elapsed deadline
|
|
72
|
+
proves only that the time passed, never that your app ended anything — supply
|
|
73
|
+
`actual_end_at` plus an `end_evidence` when it did. A conversion links only its
|
|
74
|
+
own episode's commitment, at or after that evidenced end, so card collection at
|
|
75
|
+
enrolment and unrelated paid activity cannot convert a trial. A usage-metered
|
|
76
|
+
episode needs exhaustion evidence before it can expire.
|
|
77
|
+
|
|
78
|
+
**The gentle nudge.** Registering no `'trial'` provider is a perfectly valid
|
|
79
|
+
integration: nothing breaks, no decision changes, every trial keeps working. But
|
|
80
|
+
trial revisions then go unrecorded, so the SDK logs **one** info-level console
|
|
81
|
+
line per runtime the first time trials exist and nothing will record them:
|
|
82
|
+
|
|
83
|
+
> `[RevTurbine] No trial provider supplied; trial revisions will not be recorded — see https://docs.revturbine.com/sdk/trials. Register a domain provider with domain: 'trial', or call recordTrialRevision(episode) yourself. Trials keep working; only the lifecycle facts are missing.`
|
|
84
|
+
|
|
85
|
+
It fires from `setTrialInstances()` (the moment your app hands the SDK trials)
|
|
86
|
+
and from `syncTrialRevisions()`. It is never a warning and never an error.
|
|
87
|
+
|
|
88
|
+
**Local mode.** In `local_only` the SDK makes no server calls at all, so a
|
|
89
|
+
recorded revision reaches any registered event consumer and never RevTurbine.
|
|
90
|
+
The result says `recorded_locally` rather than `recorded`, so an integrator can
|
|
91
|
+
tell the difference instead of assuming the warehouse has it.
|
|
92
|
+
|
|
93
|
+
**The ports.** `revturbine.core.trials.record_trial_revision` (Python) and
|
|
94
|
+
`revturbine::trial_revision::record_trial_revision` (Rust) classify an episode
|
|
95
|
+
and return the **validated payload for the host to ship** through its existing
|
|
96
|
+
ingest path. They emit nothing themselves: the server ports have never held an
|
|
97
|
+
event transport, and adding one would make each a second, unversioned ingest
|
|
98
|
+
client. What they guarantee is that the payload is byte-identical to the browser
|
|
99
|
+
SDK's for the same facts, asserted by the
|
|
100
|
+
`trial_revision_classification` cross-language parity fixture.
|
|
101
|
+
|
|
102
|
+
| field | value |
|
|
103
|
+
|---|---|
|
|
104
|
+
| Lands in | `0.11.9` |
|
|
105
|
+
| Fails closed in | n/a — additive. No existing call site changes behaviour. |
|
|
106
|
+
| Action needed | None to keep working. To record trial lifecycle facts, register a `'trial'` domain provider or call `recordTrialRevision` where your app changes a trial. |
|
|
107
|
+
| Proven by | `web-sdk/trial-revision.test.ts`, `tests/parity/fixtures/trial_revision_classification.json` (ts/py/rs), scaffold `src/trials/models/trial-revision.test.ts` |
|
|
108
|
+
|
|
109
|
+
Requires `@revt-eng/*` **0.1.352**, which adds the `trial_revision` taxonomy
|
|
110
|
+
name, the `TrialRevisionPayload` / `AccountCreatedPayload` contracts and the
|
|
111
|
+
`classifyTrialRevision` classifier.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 0.11.8
|
|
116
|
+
|
|
117
|
+
### `account_id` falls back to a PREFIXED user key instead of a bare user id (BL-0117)
|
|
118
|
+
|
|
119
|
+
**What changed.** Every clickstream event the SDK sends to `/api/track` used to
|
|
120
|
+
carry `account_id: userContext.account_id || userId`. `TrackEvent.account_id`
|
|
121
|
+
was a required, min-length-1 field, so an app that identified no account had to
|
|
122
|
+
send *something* — and what it sent was the **user** id, bare and
|
|
123
|
+
indistinguishable from a real account. `monetization_funnel` and `cohort_rollup`
|
|
124
|
+
build their account map out of `events_clickstream.account_id`, and experiment
|
|
125
|
+
analysis reads that column whenever `analysis_unit = 'account'`: every such row
|
|
126
|
+
contributed a bogus `user_id → user_id` account, so an account-grain readout
|
|
127
|
+
returned a user-grain n while looking perfectly valid. The treatment-interaction
|
|
128
|
+
lane had the same hole from the other side — the SDK sent no `account_id` at all
|
|
129
|
+
and the ingest route's own `account_id ?? user_id` fallback stamped a user id
|
|
130
|
+
into `placement_presentations.account_id`.
|
|
131
|
+
|
|
132
|
+
Kent ruled on it, 2026-09-25 (**D-13**): *"Keep the user-id fallback but
|
|
133
|
+
explicitly prefix it to make it obvious its a fallback key."*
|
|
134
|
+
|
|
135
|
+
So the fallback stays — a row with no identified account still needs an
|
|
136
|
+
attribution handle, and a hole in a join key is not an improvement — but it is
|
|
137
|
+
now **self-describing**:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
identified account → account_id: "acct_acme"
|
|
141
|
+
no account → account_id: "user-fallback:user_123"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Both wire lanes (`/api/track` → `events_clickstream.account_id`, and
|
|
145
|
+
`/api/events/interactions` → `placement_presentations.account_id`) resolve the
|
|
146
|
+
value through one private helper, so they cannot drift apart: the funnel joins
|
|
147
|
+
one against the other, and a divergence in trimming, hashing or fallback
|
|
148
|
+
derivation joins nothing. The fallback is derived from the same
|
|
149
|
+
`userContext.id || anonymousId` the row's `user_id` comes from and redacted the
|
|
150
|
+
same way, so an email-shaped user id is the SAME hash behind the marker on both
|
|
151
|
+
lanes. A blank or whitespace-only `account_id` is not an account and falls
|
|
152
|
+
through to the fallback.
|
|
153
|
+
|
|
154
|
+
Two new `@public` exports carry the contract to integrators and to anything
|
|
155
|
+
reading the warehouse:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { FALLBACK_ACCOUNT_ID_PREFIX, isFallbackAccountId } from '@revturbine/sdk';
|
|
159
|
+
|
|
160
|
+
FALLBACK_ACCOUNT_ID_PREFIX; // 'user-fallback:'
|
|
161
|
+
isFallbackAccountId('user-fallback:user_123'); // true — fabricated
|
|
162
|
+
isFallbackAccountId('acct_acme'); // false — a real account
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`isFallbackAccountId` is position- and case-sensitive by contract, matching the
|
|
166
|
+
read-time SQL guards: an account id that merely *contains* the marker is a real
|
|
167
|
+
account, and the bare prefix with nothing after it is not a key at all. The same
|
|
168
|
+
constant and predicate ship in `server-python`
|
|
169
|
+
(`revturbine.core.account_identity`) and `server-rust`
|
|
170
|
+
(`revturbine::account_identity`, re-exported at the crate root), and the
|
|
171
|
+
cross-language parity corpus drives all three
|
|
172
|
+
(`tests/parity/fixtures/account_id_fallback_prefix.json`) so the literal cannot
|
|
173
|
+
drift between ports.
|
|
174
|
+
|
|
175
|
+
**Absence is still valid on the wire.** `@revt-eng/schema` 0.1.325 (scaffold
|
|
176
|
+
[#375](https://github.com/revt-eng/revturbine-scaffold/pull/375)) made
|
|
177
|
+
`TrackEvent.account_id` optional, and web migration 014 made
|
|
178
|
+
`events_clickstream.account_id` nullable — absence means "no account
|
|
179
|
+
identified", and a producer with no user identity to derive from may still omit
|
|
180
|
+
the field. The browser SDK always has an identity (an un-identified visitor
|
|
181
|
+
still has the anonymous id), so its default emit always sends the prefixed
|
|
182
|
+
fallback rather than a hole.
|
|
183
|
+
|
|
184
|
+
No public API change to an existing signature: nothing moved, and nothing new is
|
|
185
|
+
asked of the caller. `identify(userId, { account_id })` is still the only way to
|
|
186
|
+
supply an account. Integrations that identify one see no wire change at all.
|
|
187
|
+
|
|
188
|
+
**Read-time consequence, stated plainly.** Account-grain analytics exclude
|
|
189
|
+
fallback-prefixed ids from account denominators, so an app that never identifies
|
|
190
|
+
an account now reports **zero** accounts rather than one-per-user. That is a
|
|
191
|
+
correction, not a regression — those rows were never accounts — but a dashboard
|
|
192
|
+
that silently read them as account-grain will show a drop.
|
|
193
|
+
|
|
194
|
+
**Landed in** `0.11.8`. **Fail-closed in** `0.11.8` — the bare fallback is gone
|
|
195
|
+
in the same version that introduced the prefixed one; there is no window where
|
|
196
|
+
both shapes are emitted.
|
|
197
|
+
|
|
198
|
+
**Proving test:** `web-sdk/account-identity.test.ts` (the literal, the
|
|
199
|
+
classifier's position/case sensitivity, and the bare-prefix and absence cases)
|
|
200
|
+
plus `web-sdk/interaction-wire-contract.test.ts` — "the account identity on the
|
|
201
|
+
/api/track wire" and "the account identity the analytics joins key on": an
|
|
202
|
+
identified user with no account produces `user-fallback:<user_id>` on both
|
|
203
|
+
lanes, the prefix survives `/api/track` JSON serialization byte-for-byte, an
|
|
204
|
+
identified account reaches the wire byte-identically to the value supplied, a
|
|
205
|
+
blank account id is treated as absence, an un-identified visitor derives from
|
|
206
|
+
the anonymous id, and every body parses against the canonical
|
|
207
|
+
`TrackEventSchema` / `TreatmentInteractionInputSchema`. Also
|
|
208
|
+
`web-sdk/customer-side-ingest.test.ts` on the plain `capture()` path,
|
|
209
|
+
`server-python/tests/test_account_identity.py`, the unit tests in
|
|
210
|
+
`server-rust/src/account_identity.rs`, and the three-way parity fixture.
|
|
211
|
+
|
|
50
212
|
## 0.11.7
|
|
51
213
|
|
|
52
214
|
### A rebuilt gate/slot re-runs its check, so a page inside `<Gate>` no longer renders blank (BL-0251)
|
package/dist/growthbook.d.ts
CHANGED
|
@@ -275,7 +275,7 @@ type TraitsNamespace = `traits:${string}`;
|
|
|
275
275
|
* The `traits` domain and any `traits:<namespace>` domain are merged
|
|
276
276
|
* into a single `traits` slot in `ResolvedProviderContext`.
|
|
277
277
|
*/
|
|
278
|
-
type DomainProviderName = 'plan' | 'entitlements' | 'segments' | 'experiments' | 'content' | 'rules' | 'traits' | 'theme' | 'events' | 'cta' | TraitsNamespace;
|
|
278
|
+
type DomainProviderName = 'plan' | 'entitlements' | 'segments' | 'experiments' | 'content' | 'rules' | 'traits' | 'theme' | 'events' | 'cta' | 'trial' | TraitsNamespace;
|
|
279
279
|
/** Immutable inputs available while resolving a domain provider. */
|
|
280
280
|
interface DomainProviderResolutionInput {
|
|
281
281
|
/** Current caller-supplied and trusted server-enriched context snapshot. */
|