@sentientui/react 0.3.1 → 0.4.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 +242 -64
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/next/adaptive-root-client.d.ts +7 -0
- package/dist/next/adaptive-root-client.js +3 -2
- package/dist/next/adaptive-root-client.js.map +1 -1
- package/dist/next/adaptive-root.d.ts +12 -0
- package/dist/next/adaptive-root.js +10 -4
- package/dist/next/adaptive-root.js.map +1 -1
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +4 -2
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
# @sentientui/react
|
|
2
2
|
|
|
3
|
-
React SDK for [SentientUI](https://
|
|
3
|
+
React SDK for [SentientUI](https://sentient-ui.com) — drop-in components and hooks that let a Thompson Sampling bandit, persona clusters, and visitor portraits automatically serve the best-performing UI variant (and section order) for each visitor.
|
|
4
|
+
|
|
5
|
+
Learning, assignment, portraits, clustering, and graph storage all run on the SentientUI hosted API. You only install the SDK and add API keys from the dashboard.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install @sentientui/react
|
|
10
|
+
npm install @sentientui/react
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @sentientui/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`@sentientui/core` is a transitive dependency — you do not need to install it directly. Only import from `@sentientui/react`.
|
|
16
|
+
|
|
17
|
+
## API key
|
|
18
|
+
|
|
19
|
+
Create a project at [app.sentient-ui.com](https://app.sentient-ui.com) and copy the API key shown once at project creation. It looks like `pk_xxxxxxxx…`. The key is safe to ship in browser bundles — the API enforces an allowed-origins allowlist on every request. Add your production domain in Project → Settings → Allowed origins.
|
|
20
|
+
|
|
21
|
+
For Next.js, set the same key in two env vars (one for the server, one for the browser):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# .env.local
|
|
25
|
+
SENTIENT_API_KEY=pk_your_key
|
|
26
|
+
NEXT_PUBLIC_SENTIENT_API_KEY=pk_your_key
|
|
9
27
|
```
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
The SDK points at `https://sentient-api.fly.dev` (the hosted API) automatically — no ingest URL configuration required.
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
## Quick start — Next.js App Router (recommended)
|
|
32
|
+
|
|
33
|
+
Wrap your root layout with `<AdaptiveRoot>`. It is a Server Component: variant assignments and persona-specific layout order are resolved server-side before HTML is sent, so the first paint has the right content with no layout shift.
|
|
14
34
|
|
|
15
35
|
```tsx
|
|
16
|
-
|
|
36
|
+
// app/layout.tsx
|
|
37
|
+
import { AdaptiveRoot } from '@sentientui/react/next';
|
|
17
38
|
|
|
18
|
-
export default function
|
|
39
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
19
40
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<body>
|
|
43
|
+
<AdaptiveRoot
|
|
44
|
+
components={[
|
|
45
|
+
{ id: 'hero_cta', variantIds: ['control', 'variant_a'] },
|
|
46
|
+
{ id: 'pricing', variantIds: ['monthly', 'annual_first'] },
|
|
47
|
+
]}
|
|
48
|
+
sections={['hero', 'pricing', 'features', 'social_proof']}
|
|
49
|
+
serverApiKey={process.env.SENTIENT_API_KEY!}
|
|
50
|
+
apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
|
|
51
|
+
appOrigin={process.env.NEXT_PUBLIC_APP_URL!}
|
|
52
|
+
context="saas"
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</AdaptiveRoot>
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
23
58
|
);
|
|
24
59
|
}
|
|
25
60
|
```
|
|
26
61
|
|
|
27
|
-
|
|
62
|
+
Then drop `<Adaptive>` anywhere you want a tested component:
|
|
28
63
|
|
|
29
64
|
```tsx
|
|
30
65
|
import { Adaptive } from '@sentientui/react';
|
|
@@ -32,118 +67,261 @@ import { Adaptive } from '@sentientui/react';
|
|
|
32
67
|
export default function Hero() {
|
|
33
68
|
return (
|
|
34
69
|
<Adaptive
|
|
35
|
-
id="
|
|
36
|
-
goal="
|
|
70
|
+
id="hero_cta"
|
|
71
|
+
goal="signup_click"
|
|
37
72
|
variants={{
|
|
38
|
-
control:
|
|
39
|
-
|
|
73
|
+
control: <button className="btn-dark">Start free trial</button>,
|
|
74
|
+
variant_a: <button className="btn-blue">Get instant access →</button>,
|
|
40
75
|
}}
|
|
41
76
|
/>
|
|
42
77
|
);
|
|
43
78
|
}
|
|
44
79
|
```
|
|
45
80
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Next.js (App Router)
|
|
81
|
+
## Quick start — other React apps (Vite, CRA, Remix, Pages Router)
|
|
49
82
|
|
|
50
|
-
Use
|
|
83
|
+
Use `<AdaptiveProvider>` directly. Assignments are made on the client after mount — no SSR preloading, but the bandit still learns normally.
|
|
51
84
|
|
|
52
85
|
```tsx
|
|
53
|
-
|
|
54
|
-
import { AdaptiveRoot } from '@sentientui/react/next';
|
|
86
|
+
import { AdaptiveProvider } from '@sentientui/react';
|
|
55
87
|
|
|
56
|
-
export default function
|
|
88
|
+
export default function App({ children }: { children: React.ReactNode }) {
|
|
57
89
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
{ id: 'hero-headline', variantIds: ['control', 'variant_b'] },
|
|
61
|
-
{ id: 'pricing-cta', variantIds: ['control', 'variant_b'] },
|
|
62
|
-
]}
|
|
63
|
-
serverApiKey={process.env.SENTIENT_API_KEY!}
|
|
64
|
-
apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
|
|
90
|
+
<AdaptiveProvider
|
|
91
|
+
apiKey={import.meta.env.VITE_SENTIENT_API_KEY}
|
|
65
92
|
context="saas"
|
|
66
93
|
>
|
|
67
94
|
{children}
|
|
68
|
-
</
|
|
95
|
+
</AdaptiveProvider>
|
|
69
96
|
);
|
|
70
97
|
}
|
|
71
98
|
```
|
|
72
99
|
|
|
73
|
-
`AdaptiveRoot` is a Server Component that fetches assignments before HTML is sent, then passes them to `AdaptiveProvider` as `initialAssignments` so crawlers and hydration see real content.
|
|
74
|
-
|
|
75
100
|
## API
|
|
76
101
|
|
|
77
|
-
### `<
|
|
102
|
+
### `<AdaptiveRoot>` (Next.js App Router — server component)
|
|
103
|
+
|
|
104
|
+
Imported from `@sentientui/react/next`.
|
|
78
105
|
|
|
79
106
|
| Prop | Type | Description |
|
|
80
107
|
|------|------|-------------|
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
108
|
+
| `components` | `Array<{ id: string; variantIds: string[] }>` | Components to preload server-side. `id` must match `<Adaptive id="…">`. |
|
|
109
|
+
| `sections` | `string[]` *(optional)* | Page section IDs in default order. When provided, a single `POST /v1/decide` returns both layout order and assignments; `useLayoutOrder()` becomes available. |
|
|
110
|
+
| `serverApiKey` | `string` | Same `pk_…` key as `apiKey`, but read from the non-public env var so it never reaches the client bundle. |
|
|
111
|
+
| `apiKey` | `string` | `pk_…` key used by the browser SDK for event tracking and cache hydration. |
|
|
112
|
+
| `appOrigin` | `string` *(default `http://localhost:3001`)* | Your app origin (e.g. `https://yourapp.com`). Must be on the project's allowed-origins list. Always set in production. |
|
|
113
|
+
| `context` | `'landing' \| 'ecommerce' \| 'saas' \| 'marketplace'` | Type of product. Used for segment weighting and analytics grouping. |
|
|
114
|
+
| `consent` | `boolean` *(default `true`)* | Set `false` to skip SDK init (no cookies, no events). Flip to `true` after the visitor accepts. |
|
|
115
|
+
| `ssrFallback` | `'first' \| 'none'` *(default `'first'`)* | What to render in SSR HTML for components not in `components`. `'first'` is safe for SEO. |
|
|
116
|
+
| `onAssignment` | `(componentId, variantId) => void` | Called once per component the first time a variant resolves. Forward to Mixpanel/PostHog/Segment/GA4. |
|
|
117
|
+
| `timeoutMs` | `number` *(default `1500`)* | Server-side fetch timeout before falling back to the first variant. |
|
|
118
|
+
| `debug` | `boolean` | Log assignment and event activity to the console. |
|
|
119
|
+
|
|
120
|
+
### `<AdaptiveProvider>` (any React app)
|
|
121
|
+
|
|
122
|
+
Accepts the same `apiKey`, `context`, `consent`, `ssrFallback`, `onAssignment`, `debug` props as `<AdaptiveRoot>`, plus:
|
|
123
|
+
|
|
124
|
+
| Prop | Type | Description |
|
|
125
|
+
|------|------|-------------|
|
|
126
|
+
| `initialAssignments` | `Record<string, string>` | SSR-preloaded assignments (e.g. from `loadAdaptiveAssignments` in `getServerSideProps`). |
|
|
127
|
+
| `sessionSegment` | `string` | Segment from SSR (`device:source`). Must match the value used in `loadAdaptiveAssignments`. |
|
|
128
|
+
| `initialLayoutOrder` | `string[] \| null` | Preloaded section order from `loadAdaptiveDecision` (for Pages-Router-style SSR with sections). |
|
|
89
129
|
|
|
90
130
|
### `<Adaptive>`
|
|
91
131
|
|
|
92
132
|
| Prop | Type | Description |
|
|
93
133
|
|------|------|-------------|
|
|
94
|
-
| `id` | `string` | Unique component identifier |
|
|
95
|
-
| `variants` | `Record<string, ReactNode>` | Map of variant ID → content |
|
|
96
|
-
| `goal` | `string \| GoalConfig` | Conversion goal.
|
|
97
|
-
| `clientOnly` | `boolean` |
|
|
134
|
+
| `id` | `string` | Unique component identifier within your project. |
|
|
135
|
+
| `variants` | `Record<string, ReactNode>` | Map of variant ID → content. Any two or more keys; the bandit explores them all. |
|
|
136
|
+
| `goal` | `string \| GoalConfig` | Conversion goal. A string is a click-goal label; an object is an explicit `GoalConfig`. |
|
|
137
|
+
| `clientOnly` | `boolean` | Render nothing on the server; resolve on the client only. Use for cookie-dependent slots. |
|
|
98
138
|
|
|
99
139
|
#### Goal types
|
|
100
140
|
|
|
101
141
|
```ts
|
|
102
|
-
//
|
|
103
|
-
goal="
|
|
142
|
+
// Any click inside the variant (button, a, role=button). The string is the analytics label.
|
|
143
|
+
goal="signup_click"
|
|
104
144
|
|
|
105
|
-
// Click on a
|
|
145
|
+
// Click only on elements matching a CSS selector inside the variant.
|
|
106
146
|
goal={{ type: 'click', selector: 'button.cta' }}
|
|
107
147
|
|
|
108
|
-
//
|
|
109
|
-
goal={{ type: 'scroll_depth', threshold:
|
|
148
|
+
// 80% of the component visible in the viewport (IntersectionObserver, 0–1 scale).
|
|
149
|
+
goal={{ type: 'scroll_depth', threshold: 0.8 }}
|
|
110
150
|
|
|
111
|
-
// A form inside the variant
|
|
151
|
+
// A <form> inside the variant fires submit.
|
|
112
152
|
goal={{ type: 'form_submit' }}
|
|
113
153
|
|
|
114
|
-
//
|
|
115
|
-
goal={{
|
|
154
|
+
// Composite — all sub-goals must fire (in any order) before the reward is recorded.
|
|
155
|
+
goal={{
|
|
156
|
+
type: 'composite',
|
|
157
|
+
all: [
|
|
158
|
+
{ type: 'scroll_depth', threshold: 0.8 },
|
|
159
|
+
{ type: 'click' },
|
|
160
|
+
],
|
|
161
|
+
}}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Each goal fires at most once per variant mount.
|
|
165
|
+
|
|
166
|
+
### `<AdaptiveText>`
|
|
167
|
+
|
|
168
|
+
Lightweight text-only variant (no wrapper element, no automatic goal wiring). Useful when you publish text variants from the dashboard WYSIWYG.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import { AdaptiveText } from '@sentientui/react';
|
|
172
|
+
|
|
173
|
+
<h1>
|
|
174
|
+
<AdaptiveText id="hero_headline" default="Ship faster with SentientUI" />
|
|
175
|
+
</h1>
|
|
116
176
|
```
|
|
117
177
|
|
|
118
|
-
### `useAssignment(componentId, variantIds)`
|
|
178
|
+
### `useAssignment(componentId, variantIds?)`
|
|
119
179
|
|
|
120
|
-
Lower-level hook
|
|
180
|
+
Lower-level hook when you need the variant ID inside your own render logic (e.g. full-page layout tests where `<Adaptive>`'s wrapper `<div>` would break a flex/grid layout).
|
|
121
181
|
|
|
122
182
|
```tsx
|
|
123
|
-
import { useAssignment } from '@sentientui/react';
|
|
183
|
+
import { useAssignment, useSentient } from '@sentientui/react';
|
|
124
184
|
|
|
125
|
-
function
|
|
126
|
-
const
|
|
185
|
+
function Hero() {
|
|
186
|
+
const client = useSentient();
|
|
187
|
+
const { variantId, isLoading } = useAssignment('hero_cta', ['control', 'variant_a']);
|
|
127
188
|
|
|
128
189
|
if (isLoading) return <Skeleton />;
|
|
129
|
-
return variantId === '
|
|
190
|
+
return variantId === 'variant_a' ? <AccentHero /> : <DefaultHero />;
|
|
130
191
|
}
|
|
131
192
|
```
|
|
132
193
|
|
|
133
|
-
|
|
194
|
+
When using `useAssignment` you are responsible for firing the goal — `<Adaptive>`'s automatic impression and goal tracking does not apply. Use `client?.track(…)` or `client?.goal(name)` from `useSentient()`.
|
|
195
|
+
|
|
196
|
+
### `useLayoutOrder()`
|
|
197
|
+
|
|
198
|
+
Returns the resolved persona-specific section order, or `null` when not configured / below confidence threshold. Always fall back to your default order.
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { useLayoutOrder } from '@sentientui/react';
|
|
202
|
+
|
|
203
|
+
function Page() {
|
|
204
|
+
const order = useLayoutOrder();
|
|
205
|
+
const defaultOrder = ['hero', 'pricing', 'features', 'social_proof'];
|
|
206
|
+
const sections: Record<string, React.ReactNode> = {
|
|
207
|
+
hero: <Hero />, pricing: <Pricing />, features: <Features />, social_proof: <SocialProof />,
|
|
208
|
+
};
|
|
209
|
+
return <main>{(order ?? defaultOrder).map((id) => <Fragment key={id}>{sections[id]}</Fragment>)}</main>;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### SSR for Pages Router / custom SSR
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { loadAdaptiveAssignments } from '@sentientui/react/server';
|
|
217
|
+
|
|
218
|
+
export async function getServerSideProps({ req }) {
|
|
219
|
+
const assignments = await loadAdaptiveAssignments(
|
|
220
|
+
[{ id: 'hero_cta', variantIds: ['control', 'variant_a'] }],
|
|
221
|
+
{
|
|
222
|
+
cookies: req.cookies,
|
|
223
|
+
apiKey: process.env.SENTIENT_API_KEY!,
|
|
224
|
+
baseUrl: 'https://sentient-api.fly.dev/v1',
|
|
225
|
+
origin: process.env.NEXT_PUBLIC_APP_URL,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
return { props: { initialAssignments: assignments } };
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Pass the result as `initialAssignments` on `<AdaptiveProvider>` in `_app.tsx`. For pages with a `sections` layout, use `loadAdaptiveDecision` (same options shape, plus `sections: string[]`) — its return value carries both `assignments` and `layoutOrder`.
|
|
233
|
+
|
|
234
|
+
### Forwarding to your own analytics
|
|
134
235
|
|
|
135
236
|
```tsx
|
|
136
237
|
<AdaptiveProvider
|
|
137
|
-
apiKey=
|
|
238
|
+
apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
|
|
138
239
|
context="saas"
|
|
139
240
|
onAssignment={(componentId, variantId) => {
|
|
140
|
-
posthog.capture('$
|
|
141
|
-
|
|
142
|
-
variant: variantId,
|
|
143
|
-
});
|
|
241
|
+
posthog.capture('$feature_flag_called', { $feature_flag: componentId, $feature_flag_response: variantId });
|
|
242
|
+
mixpanel.register({ [`variant_${componentId}`]: variantId });
|
|
144
243
|
}}
|
|
145
244
|
>
|
|
245
|
+
{children}
|
|
246
|
+
</AdaptiveProvider>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Fires at most once per component ID per page load. Full Segment / GA4 / Mixpanel recipes: see [sentient-ui.com/docs/integrations](https://sentient-ui.com/docs/integrations).
|
|
250
|
+
|
|
251
|
+
### Local overrides (development)
|
|
252
|
+
|
|
253
|
+
Force a variant without touching the bandit:
|
|
254
|
+
|
|
146
255
|
```
|
|
256
|
+
# URL parameter (stackable)
|
|
257
|
+
https://yourapp.com?sentient_variant=hero_cta:variant_a&sentient_variant=pricing:annual_first
|
|
258
|
+
|
|
259
|
+
# Or, before SDK init:
|
|
260
|
+
window.__sentient_overrides = { hero_cta: 'variant_a' };
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Overrides bypass the bandit entirely — no events recorded, weights unchanged.
|
|
264
|
+
|
|
265
|
+
## Consent management (GDPR)
|
|
266
|
+
|
|
267
|
+
SentientUI does not track visitors until consent is granted. To serve the best-performing variant while the cookie banner is pending (rather than freezing the UI), use `preConsentBehavior: 'statistical_winner'`:
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
<AdaptiveProvider
|
|
271
|
+
apiKey={process.env.NEXT_PUBLIC_SENTIENT_API_KEY!}
|
|
272
|
+
context="saas"
|
|
273
|
+
consent={hasConsent}
|
|
274
|
+
preConsentBehavior="statistical_winner"
|
|
275
|
+
>
|
|
276
|
+
{children}
|
|
277
|
+
</AdaptiveProvider>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Set `hasConsent` to `false` until the user accepts your cookie banner, then flip it to `true`. The provider re-initialises automatically and begins full tracking.
|
|
281
|
+
|
|
282
|
+
### Without React
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { init, grantConsent } from '@sentientui/core';
|
|
286
|
+
|
|
287
|
+
const client = init({
|
|
288
|
+
apiKey: 'pk_...',
|
|
289
|
+
context: 'saas',
|
|
290
|
+
consent: false,
|
|
291
|
+
preConsentBehavior: 'statistical_winner',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// After the user accepts the cookie banner:
|
|
295
|
+
grantConsent(); // upgrades client in place — no need to reassign
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### OneTrust integration
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
window.addEventListener('OneTrustGroupsUpdated', () => {
|
|
302
|
+
// C0002 = Analytics/Performance category in OneTrust
|
|
303
|
+
if (window.OnetrustActiveGroups?.includes('C0002')) {
|
|
304
|
+
grantConsent();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Cookiebot integration
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
window.addEventListener('CookiebotOnAccept', () => {
|
|
313
|
+
if (window.Cookiebot?.consent?.statistics) {
|
|
314
|
+
grantConsent();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
When `consent: false` without `preConsentBehavior`, the SDK is a complete no-op — no API calls, no cookies, nothing. The `preConsentBehavior: 'statistical_winner'` mode calls only `GET /v1/winner`, a read-only endpoint that returns the best variant without storing any visitor data.
|
|
320
|
+
|
|
321
|
+
## Docs
|
|
322
|
+
|
|
323
|
+
Full SDK reference: [sentient-ui.com/docs](https://sentient-ui.com/docs).
|
|
324
|
+
Integrations: [sentient-ui.com/docs/integrations](https://sentient-ui.com/docs/integrations).
|
|
147
325
|
|
|
148
326
|
## License
|
|
149
327
|
|
package/dist/index.d.cts
CHANGED
|
@@ -34,6 +34,13 @@ type AdaptiveProviderProps = {
|
|
|
34
34
|
* initialise and begin tracking.
|
|
35
35
|
*/
|
|
36
36
|
consent?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Behavior before consent is granted. Pass `'statistical_winner'` to serve the
|
|
39
|
+
* best-performing variant via `GET /v1/winner` with zero tracking while the
|
|
40
|
+
* consent banner is showing. Requires `consent: false`.
|
|
41
|
+
* @see SentientConfig.preConsentBehavior
|
|
42
|
+
*/
|
|
43
|
+
preConsentBehavior?: 'statistical_winner' | 'control';
|
|
37
44
|
/**
|
|
38
45
|
* Called once per component the first time a variant is resolved for that
|
|
39
46
|
* component in this session. Use to forward assignments to your own analytics
|
|
@@ -92,6 +99,8 @@ type AdaptiveProps = {
|
|
|
92
99
|
* a hydration mismatch. Tradeoff: minor CLS on first paint.
|
|
93
100
|
*/
|
|
94
101
|
clientOnly?: boolean;
|
|
102
|
+
/** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
|
|
103
|
+
agentData?: unknown;
|
|
95
104
|
};
|
|
96
105
|
declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
|
|
97
106
|
/** Re-renders only when the chosen variant changes. */
|
|
@@ -125,7 +134,7 @@ type AssignmentState = {
|
|
|
125
134
|
* on the next render. Subsequent paints read synchronously from cache —
|
|
126
135
|
* no flicker, no loading state after first paint.
|
|
127
136
|
*/
|
|
128
|
-
declare function useAssignment(componentId: string, variantIds: string[]): AssignmentState;
|
|
137
|
+
declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
|
|
129
138
|
|
|
130
139
|
/** Per-component weights store with isolated subscriptions. */
|
|
131
140
|
type VariantWeight = {
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ type AdaptiveProviderProps = {
|
|
|
34
34
|
* initialise and begin tracking.
|
|
35
35
|
*/
|
|
36
36
|
consent?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Behavior before consent is granted. Pass `'statistical_winner'` to serve the
|
|
39
|
+
* best-performing variant via `GET /v1/winner` with zero tracking while the
|
|
40
|
+
* consent banner is showing. Requires `consent: false`.
|
|
41
|
+
* @see SentientConfig.preConsentBehavior
|
|
42
|
+
*/
|
|
43
|
+
preConsentBehavior?: 'statistical_winner' | 'control';
|
|
37
44
|
/**
|
|
38
45
|
* Called once per component the first time a variant is resolved for that
|
|
39
46
|
* component in this session. Use to forward assignments to your own analytics
|
|
@@ -92,6 +99,8 @@ type AdaptiveProps = {
|
|
|
92
99
|
* a hydration mismatch. Tradeoff: minor CLS on first paint.
|
|
93
100
|
*/
|
|
94
101
|
clientOnly?: boolean;
|
|
102
|
+
/** Structured data for AI agent consumption via GET /v1/agent/layout. Stored per assigned variant. */
|
|
103
|
+
agentData?: unknown;
|
|
95
104
|
};
|
|
96
105
|
declare function AdaptiveImpl(props: AdaptiveProps): JSX.Element | null;
|
|
97
106
|
/** Re-renders only when the chosen variant changes. */
|
|
@@ -125,7 +134,7 @@ type AssignmentState = {
|
|
|
125
134
|
* on the next render. Subsequent paints read synchronously from cache —
|
|
126
135
|
* no flicker, no loading state after first paint.
|
|
127
136
|
*/
|
|
128
|
-
declare function useAssignment(componentId: string, variantIds: string[]): AssignmentState;
|
|
137
|
+
declare function useAssignment(componentId: string, variantIds: string[], agentData?: unknown): AssignmentState;
|
|
129
138
|
|
|
130
139
|
/** Per-component weights store with isolated subscriptions. */
|
|
131
140
|
type VariantWeight = {
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ function AdaptiveProvider(props) {
|
|
|
52
52
|
var _a, _b, _c, _d;
|
|
53
53
|
const [client, setClient] = (0, import_react.useState)(null);
|
|
54
54
|
(0, import_react.useEffect)(() => {
|
|
55
|
-
if (props.consent === false) {
|
|
55
|
+
if (props.consent === false && !props.preConsentBehavior) {
|
|
56
56
|
setClient((prev) => {
|
|
57
57
|
prev == null ? void 0 : prev.destroy();
|
|
58
58
|
return null;
|
|
@@ -65,7 +65,8 @@ function AdaptiveProvider(props) {
|
|
|
65
65
|
debug: props.debug,
|
|
66
66
|
initialAssignments: props.initialAssignments,
|
|
67
67
|
sessionSegment: props.sessionSegment,
|
|
68
|
-
consent: props.consent
|
|
68
|
+
consent: props.consent,
|
|
69
|
+
preConsentBehavior: props.preConsentBehavior
|
|
69
70
|
});
|
|
70
71
|
setClient(c);
|
|
71
72
|
return () => {
|
|
@@ -176,7 +177,7 @@ function pickFromWeights(weights, variantIds) {
|
|
|
176
177
|
}
|
|
177
178
|
return (_a = best == null ? void 0 : best.variantId) != null ? _a : null;
|
|
178
179
|
}
|
|
179
|
-
function useAssignment(componentId, variantIds) {
|
|
180
|
+
function useAssignment(componentId, variantIds, agentData) {
|
|
180
181
|
const initialAssignments = useInitialAssignments();
|
|
181
182
|
const ssrFallback = useSsrFallback();
|
|
182
183
|
const client = useSentient();
|
|
@@ -242,7 +243,7 @@ function useAssignment(componentId, variantIds) {
|
|
|
242
243
|
const cached = client.getAssignment(componentId, segment);
|
|
243
244
|
if (cached && variantIds.includes(cached.variantId)) return;
|
|
244
245
|
let cancelled = false;
|
|
245
|
-
void client.assign(componentId, variantIds).then((result) => {
|
|
246
|
+
void client.assign(componentId, variantIds, agentData).then((result) => {
|
|
246
247
|
var _a;
|
|
247
248
|
if (cancelled) return;
|
|
248
249
|
if (!result) return;
|
|
@@ -300,7 +301,7 @@ function AdaptiveImpl(props) {
|
|
|
300
301
|
const client = useSentient();
|
|
301
302
|
const apiKey = useAdaptiveApiKey();
|
|
302
303
|
const variantIds = (0, import_react3.useMemo)(() => Object.keys(props.variants), [props.variants]);
|
|
303
|
-
const { variantId, content } = useAssignment(props.id, variantIds);
|
|
304
|
+
const { variantId, content } = useAssignment(props.id, variantIds, props.agentData);
|
|
304
305
|
const containerRef = (0, import_react3.useRef)(null);
|
|
305
306
|
const [mounted, setMounted] = (0, import_react3.useState)(false);
|
|
306
307
|
(0, import_react3.useEffect)(() => {
|