@revturbine/sdk 0.11.16 → 0.11.17

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
@@ -47,6 +47,188 @@ also require a changelog entry.
47
47
 
48
48
  ---
49
49
 
50
+ ## 0.11.17
51
+
52
+ ### Checkout metadata helper, `checkout_started`, and decision context on interactions (plan 282 TASK-9, BL-0432)
53
+
54
+ **What changed.** Three additive changes to the browser SDK (`@revturbine/sdk`);
55
+ no existing signature narrows.
56
+
57
+ - **`checkoutMetadata(outputId)`** (new, `@public`) returns the Stripe Checkout
58
+ `revturbine_*` metadata bag for an output this SDK decided — user, account,
59
+ placement, decision, output, rule and target plan — plus
60
+ `client_reference_id` (the user id). Pass `metadata` as both the session's
61
+ `metadata` and `subscription_data.metadata`; the mirror is what reaches each
62
+ invoice's `parent.subscription_details.metadata`. A key the SDK does not hold
63
+ is absent, never empty; `null` for an output this SDK did not decide. New
64
+ types `RevTurbineCheckoutMetadata` and `RevTurbineCheckoutMetadataBag`.
65
+ - **`checkout_started`** (taxonomy v6, `@revt-eng/schema` 0.1.381) is emitted on
66
+ a `cta_clicked` whose CTA opens a plan picker or pricing page
67
+ (`CHECKOUT_STARTED_CTA_ACTION_TYPES`: `navigate_to_plans`, `view_plans`) with
68
+ `{ placement_id, payload_id, decision_id, plan_handle, rule_handle }`. Never
69
+ for `open_checkout_modal` — that CTA opens checkout directly ("on click"), so
70
+ its started count is its clicked count.
71
+ - **`trackTreatmentInteraction`** accepts optional `decisionId`,
72
+ `segmentHandles` and `segmentIds` (`RevTurbineTreatmentInteractionDecisionContext`;
73
+ the parameter type is now `RevTurbineTreatmentInteractionRequest`, a
74
+ superset of the previous `RevTurbineTreatmentInteractionInput`) and sends
75
+ them as `decision_id` / `segment_handles` / `segment_ids`. Segments are
76
+ handles: `segment_handles` is the effective set scaffold's `evaluateSegments`
77
+ built plus any hosted-resolved slugs — the analytics join key — and
78
+ `segment_ids` carries only the minted ids a hosted context supplied
79
+ (telemetry; omitted entirely in local mode). When not given, they are filled
80
+ from the output this SDK indexed for `payloadId`; a caller that gives none
81
+ and names no indexed output sends exactly the row it sent before. The React
82
+ controller's clicks and the output-addressed `convert()` / `dismiss()` /
83
+ `snooze()` carry them.
84
+
85
+ **Landed in** 0.11.17. **Fail-closed in** n/a — additive. The headless ports
86
+ (Python, Rust, `server-node`) carry only the re-vendored generated types for
87
+ the 0.1.381 pin; the cross-language parity corpus is untouched.
88
+
89
+ **Docs.** `guides/billing.mdx` "Where checkout fits" now documents the bag, the
90
+ `subscription_data.metadata` mirror, `client_reference_id`, and that Customer
91
+ metadata cannot be set from Checkout.
92
+
93
+ **Proving tests.** `web-sdk/checkout-metadata.test.ts`,
94
+ `web-sdk/checkout-started-emit.test.ts`,
95
+ `web-sdk/interaction-decision-context.test.ts`.
96
+
97
+ ### `update({ usage })` now reaches usage and credit threshold placements and their usage tokens (BL-0402, BL-0409)
98
+
99
+ **What changed.** A behaviour fix in the browser SDK (`@revturbine/sdk`); no
100
+ signature changes. `update({ usage })` / `updateUsage()` wrote only the SDK's
101
+ private usage balances. `can()` and `getUsage()` read those balances, but the
102
+ placement resolver built its usage state from the user context's `usage`
103
+ entries alone, and took each entry's `limit` only from a per-entry field
104
+ (default `0`). The shared threshold gate fails closed on a missing or zero
105
+ limit. So `usage_threshold` and `credit_threshold` placements (the 80% usage
106
+ banner, the low-credits warning) never fired for app-reported usage, and
107
+ `{{usage_current}}` / `{{usage_limit}}` / `{{usage_remaining}}` /
108
+ `{{usage_percent}}` stayed stale or unresolved, while the gate and the meter
109
+ moved.
110
+
111
+ Placements now read one usage view, the same one `getUsage()` reports:
112
+
113
+ - **Used**: the balance the app reported through `update({ usage })` /
114
+ `updateUsage()` / `identify(…, { usage })` wins (the most recent write),
115
+ else the context entry's `amount`.
116
+ - **Limit**: the context entry's own `limit` wins, else the Playbook
117
+ allowance for the user's **current plan** (the plan-scoped `usage_limit`
118
+ rule's `limit_value` or `credits` rule's `allowance_value`, which is the
119
+ `limit` `getUsage()` already returned). An `unlimited` or absent allowance
120
+ means no threshold: the placement does not fire.
121
+ - **Tokens**: the SDK now fills the usage token family in decision content
122
+ from the same view. It fills `{{usage_current}}` whenever usage is known.
123
+ It fills `{{usage_limit}}`, `{{usage_remaining}}` and `{{usage_percent}}`
124
+ only when a positive limit is known, so an unlimited allowance leaves
125
+ those tokens untouched instead of rendering `0`. The `<unit>_usage_*`
126
+ forms and the aliases (`current_usage`, `current_limit`,
127
+ `remaining_usage`) are filled too. A registered entitlements provider
128
+ still supplies its own usage state, as before.
129
+
130
+ `can()` and `getUsage()` are unchanged. The headless server ports (Python,
131
+ Rust, `server-node`) take usage counters from the caller and are unaffected;
132
+ the cross-language parity corpus is untouched.
133
+
134
+ **Docs.** `tutorials/usage-quota-meter.mdx` and
135
+ `tutorials/low-credits-warning.mdx` now use the canonical flat Playbook rule
136
+ shape (`kind` + `limit_value` / `allowance_value`, handle-valued ids). The
137
+ old nested `type_fields` shape resolved no allowance, so neither tutorial's
138
+ banner could fire. The expected-behaviour table now matches what the SDK
139
+ does: the banner shows from 80%, and at 100% `can('api_calls')` denies.
140
+
141
+ **Proving test.** `web-sdk/usage-reaches-placements.test.ts`.
142
+
143
+ ### App-supplied trial context now reaches trial placements, trial tokens and reverse-trial grants (BL-0403)
144
+
145
+ **What changed.** A behaviour fix in the browser SDK (`@revturbine/sdk`); no
146
+ signature changes. `user.trial` at init, `update({ trial })`,
147
+ `setUserContext({ trial })` and a server action's `userContext` result all
148
+ merged into the user context's `trial`, but neither trial reader looked there.
149
+ The placement resolver's plan state (which gates `trial_started`,
150
+ `trial_progress`, `trial_ending`, `trial_ended` and `trial_converted`
151
+ placements) and the reverse-trial entitlement grants read only the SDK's own
152
+ trial status, written by `localRuntime.initialData.trialStatus`, `hydrate()`,
153
+ `getTrialStatus()` and `setTrialInstances()`. So an app that supplied its
154
+ trial saw no trial placement fire, no reverse-trial grant apply, and
155
+ `{{trial_days_remaining}}` stay raw.
156
+
157
+ Both readers now use one trial view:
158
+
159
+ - **Sources.** The user context's `trial` (what the app supplies, plus the
160
+ authenticated client-context delivery, which merges its trial into the same
161
+ field) and the SDK trial status (`initialData.trialStatus`, `hydrate()`,
162
+ `getTrialStatus()`, `setTrialInstances()`).
163
+ - **Precedence: the most recent change wins**, the same rule the usage view
164
+ applies to reported balances (BL-0402). A server-delivered or hydrated
165
+ status wins when it arrives after the app's trial (an
166
+ `initialData.trialStatus` passed alongside init `user.trial` counts as
167
+ later). An app `update({ trial })` made after it wins over it. Re-sending
168
+ an unchanged trial is not a newer write, so passing the same trial on every
169
+ render never displaces a fresher server status. A source that was never
170
+ supplied does not compete.
171
+ - **Tokens.** The SDK now fills `{{trial_days_remaining}}` and
172
+ `{{trial_days_total}}` in decision content from the same plan state the
173
+ trial gate read. A token whose value is unknown is left untouched.
174
+ - **`getTrialStatus()`** in `local_only` mode with no `getTrialStatus`
175
+ resolver returns this trial view, the app-supplied trial included.
176
+
177
+ **React.** `<RevTurbineProvider>` no longer reports `trial`, `tiers`,
178
+ `payment_failed`, `payment_at_risk`, `instances`, `experiments`, `email_type`,
179
+ `derived_config_version`, `context_hash` or `derived_computed_at` in
180
+ `options.user` as "dropped unrecognized user-context key(s)" on mount. The SDK
181
+ constructor already merges them from `options.user`; the provider now forwards
182
+ only the identity fields `identify()` merges. Unknown keys and server-assigned
183
+ keys are still reported.
184
+
185
+ The headless server ports (Python, Rust, `server-node`) take an
186
+ already-derived trial status and are unaffected; the cross-language parity
187
+ corpus is untouched.
188
+
189
+ **Proving tests.** `web-sdk/trial-reaches-placements.test.ts`,
190
+ `web-sdk/react/RevTurbineProvider.identify-context.test.tsx`.
191
+
192
+ ### Trial state no longer leaks across users; app-supplied trials fire lifecycle triggers; `getUserContext()` reports trial and billing fields (BL-0417)
193
+
194
+ **What changed.** Behaviour fixes in the browser SDK (`@revturbine/sdk`)
195
+ that follow up BL-0403. No signature changes.
196
+
197
+ - **Reset and user switch.** `resetIdentity()` / `resetUserContext()`
198
+ cleared the user context but kept the SDK trial status (from
199
+ `initialData.trialStatus`, `hydrate()`, `getTrialStatus()` or
200
+ `setTrialInstances()`). They also kept which trial source was newest and
201
+ the last lifecycle stage. So the next user saw the previous user's trial
202
+ placements, reverse-trial grants and `{{trial_days_*}}` tokens, and a
203
+ trial-less next user could emit `trial_expired`. A reset now clears all of
204
+ that, and the next user starts with no trial. Identifying a **different**
205
+ user without a reset (`identify()`, `setUserContext({ id })`, or a
206
+ `hydrate()` payload for another user) now drops the previous user's trial
207
+ (both sources) and usage (reported balances and context `usage`) before
208
+ merging the new user's values. The new user's own values still apply. Going
209
+ from anonymous to identified, or re-identifying the same user, keeps what
210
+ is held. Reported usage was already cleared on reset; only the
211
+ switch-without-reset path leaked it.
212
+ - **Lifecycle triggers.** `trial_midpoint`, `trial_expiring` and
213
+ `trial_expired` fired only from `getTrialStatus()` and
214
+ `setTrialInstances()`. The SDK now also runs the lifecycle evaluation
215
+ against the resolved trial view after every change that can move it:
216
+ `user.trial` at init, `update({ trial })`, `setUserContext`, a server
217
+ action's context, the client-context delivery, `hydrate()`, a reset, and a
218
+ Playbook load that turns the triggers on. The dedupe has not changed: a
219
+ stage fires only when it differs from the last stage seen. So an unchanged
220
+ trial, or both sources reporting the same threshold, fires once.
221
+ `enableTrialAutoTriggers: false` still silences them.
222
+ - **`getUserContext()`** now includes `trial` (the resolved trial view, the
223
+ one placements and grants decide with), `tiers`, `payment_failed`,
224
+ `payment_at_risk` and `instances` when they are held. The snapshot is a
225
+ copy, so changing it does not change SDK state.
226
+
227
+ The headless server ports are unaffected, and the cross-language parity
228
+ corpus is untouched.
229
+
230
+ **Proving test.** `web-sdk/trial-reset-lifecycle.test.ts`.
231
+
50
232
  ## 0.11.16
51
233
 
52
234
  ### Build fix, part two: the npm declaration build resolves `openapi-fetch` too (no API change)