@traffical/react 0.1.1
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 +1011 -0
- package/dist/context.d.ts +90 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +23 -0
- package/dist/context.js.map +1 -0
- package/dist/hooks.d.ts +273 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +424 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +48 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +144 -0
- package/dist/provider.js.map +1 -0
- package/package.json +55 -0
- package/src/context.ts +144 -0
- package/src/hooks.ts +656 -0
- package/src/index.ts +93 -0
- package/src/provider.tsx +194 -0
package/README.md
ADDED
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
# @traffical/react
|
|
2
|
+
|
|
3
|
+
React SDK for Traffical - a unified parameter decisioning platform for feature flags, A/B testing, and contextual bandits.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @traffical/react
|
|
9
|
+
# or
|
|
10
|
+
npm install @traffical/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Wrap your app with TrafficalProvider
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { TrafficalProvider } from '@traffical/react';
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<TrafficalProvider
|
|
23
|
+
config={{
|
|
24
|
+
orgId: 'org_123',
|
|
25
|
+
projectId: 'proj_456',
|
|
26
|
+
env: 'production',
|
|
27
|
+
apiKey: 'pk_...',
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<MyComponent />
|
|
31
|
+
</TrafficalProvider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Use the `useTraffical` hook in your components
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useTraffical } from '@traffical/react';
|
|
40
|
+
|
|
41
|
+
function MyComponent() {
|
|
42
|
+
const { params, ready, track } = useTraffical({
|
|
43
|
+
defaults: {
|
|
44
|
+
'ui.hero.title': 'Welcome',
|
|
45
|
+
'ui.hero.color': '#007bff',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const handleCTAClick = () => {
|
|
50
|
+
// Track a user event (decisionId is automatically bound)
|
|
51
|
+
track('cta_click', { button: 'hero' });
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!ready) return <div>Loading...</div>;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<h1 style={{ color: params['ui.hero.color'] }} onClick={handleCTAClick}>
|
|
58
|
+
{params['ui.hero.title']}
|
|
59
|
+
</h1>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
### TrafficalProvider
|
|
67
|
+
|
|
68
|
+
Initializes the Traffical client and provides it to child components.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
<TrafficalProvider config={config}>
|
|
72
|
+
{children}
|
|
73
|
+
</TrafficalProvider>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Props
|
|
77
|
+
|
|
78
|
+
| Prop | Type | Required | Description |
|
|
79
|
+
|------|------|----------|-------------|
|
|
80
|
+
| `config.orgId` | `string` | Yes | Organization ID |
|
|
81
|
+
| `config.projectId` | `string` | Yes | Project ID |
|
|
82
|
+
| `config.env` | `string` | Yes | Environment (e.g., "production", "staging") |
|
|
83
|
+
| `config.apiKey` | `string` | Yes | API key for authentication |
|
|
84
|
+
| `config.baseUrl` | `string` | No | Base URL for the control plane API |
|
|
85
|
+
| `config.localConfig` | `ConfigBundle` | No | Local config bundle for offline fallback |
|
|
86
|
+
| `config.refreshIntervalMs` | `number` | No | Config refresh interval (default: 60000) |
|
|
87
|
+
| `config.unitKeyFn` | `() => string` | No | Function to get the unit key (user ID). If not provided, uses automatic stable ID |
|
|
88
|
+
| `config.contextFn` | `() => Context` | No | Function to get additional context |
|
|
89
|
+
| `config.trackDecisions` | `boolean` | No | Whether to track decision events (default: true) |
|
|
90
|
+
| `config.decisionDeduplicationTtlMs` | `number` | No | Decision dedup TTL (default: 1 hour) |
|
|
91
|
+
| `config.exposureSessionTtlMs` | `number` | No | Exposure dedup session TTL (default: 30 min) |
|
|
92
|
+
| `config.plugins` | `TrafficalPlugin[]` | No | Additional plugins to register |
|
|
93
|
+
| `config.eventBatchSize` | `number` | No | Max events before auto-flush (default: 10) |
|
|
94
|
+
| `config.eventFlushIntervalMs` | `number` | No | Auto-flush interval (default: 30000) |
|
|
95
|
+
| `config.initialParams` | `Record<string, unknown>` | No | Initial params from SSR |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### useTraffical
|
|
100
|
+
|
|
101
|
+
Primary hook for parameter resolution and decision tracking.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
const { params, decision, ready, error, trackExposure, track } = useTraffical(options);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Options
|
|
108
|
+
|
|
109
|
+
| Option | Type | Default | Description |
|
|
110
|
+
|--------|------|---------|-------------|
|
|
111
|
+
| `defaults` | `T` | Required | Default parameter values |
|
|
112
|
+
| `context` | `Context` | `undefined` | Additional context to merge |
|
|
113
|
+
| `tracking` | `"full" \| "decision" \| "none"` | `"full"` | Tracking mode |
|
|
114
|
+
|
|
115
|
+
#### Tracking Modes
|
|
116
|
+
|
|
117
|
+
| Mode | Decision Event | Exposure Event | Use Case |
|
|
118
|
+
|------|----------------|----------------|----------|
|
|
119
|
+
| `"full"` | Yes | Auto | Default. UI components that users see |
|
|
120
|
+
| `"decision"` | Yes | Manual | Manual exposure control (e.g., viewport tracking) |
|
|
121
|
+
| `"none"` | No | No | SSR, tests, internal logic |
|
|
122
|
+
|
|
123
|
+
#### Return Value
|
|
124
|
+
|
|
125
|
+
| Property | Type | Description |
|
|
126
|
+
|----------|------|-------------|
|
|
127
|
+
| `params` | `T` | Resolved parameter values |
|
|
128
|
+
| `decision` | `DecisionResult \| null` | Decision metadata (null when `tracking="none"`) |
|
|
129
|
+
| `ready` | `boolean` | Whether the client is ready |
|
|
130
|
+
| `error` | `Error \| null` | Any initialization error |
|
|
131
|
+
| `trackExposure` | `() => void` | Manually track exposure (no-op when `tracking="none"`) |
|
|
132
|
+
| `track` | `(event: string, properties?: object) => void` | Track event with bound decisionId (no-op when `tracking="none"`) |
|
|
133
|
+
|
|
134
|
+
#### Examples
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// Full tracking (default) - decision + exposure events
|
|
138
|
+
const { params, decision, ready } = useTraffical({
|
|
139
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Decision tracking only - manual exposure control
|
|
143
|
+
const { params, decision, trackExposure } = useTraffical({
|
|
144
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
145
|
+
tracking: 'decision',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Track exposure when element is visible
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (isElementVisible && decision) {
|
|
151
|
+
trackExposure();
|
|
152
|
+
}
|
|
153
|
+
}, [isElementVisible, decision, trackExposure]);
|
|
154
|
+
|
|
155
|
+
// No tracking - for SSR, tests, or internal logic
|
|
156
|
+
const { params, ready } = useTraffical({
|
|
157
|
+
defaults: { 'ui.hero.title': 'Welcome' },
|
|
158
|
+
tracking: 'none',
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### useTrafficalTrack
|
|
165
|
+
|
|
166
|
+
Hook to track user events for A/B testing and bandit optimization.
|
|
167
|
+
|
|
168
|
+
> **Tip:** For most use cases, use the bound `track` from `useTraffical()` instead. It automatically includes the `decisionId`. Use this standalone hook for advanced scenarios like cross-component event tracking or server-side tracking.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
// Recommended: use bound track from useTraffical
|
|
172
|
+
const { params, track } = useTraffical({
|
|
173
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const handlePurchase = (amount: number) => {
|
|
177
|
+
track('purchase', { value: amount, orderId: 'ord_123' });
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Advanced: standalone hook when you need to attribute to a specific decision
|
|
181
|
+
const standaloneTrack = useTrafficalTrack();
|
|
182
|
+
|
|
183
|
+
standaloneTrack('purchase', { value: amount }, { decisionId: someOtherDecision.decisionId });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### useTrafficalReward (deprecated)
|
|
187
|
+
|
|
188
|
+
> **Deprecated:** Use `useTrafficalTrack()` instead.
|
|
189
|
+
|
|
190
|
+
Hook to track rewards for A/B testing and bandit optimization.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### useTrafficalClient
|
|
195
|
+
|
|
196
|
+
Hook to access the Traffical client directly.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
const { client, ready, error } = useTrafficalClient();
|
|
200
|
+
|
|
201
|
+
if (ready && client) {
|
|
202
|
+
const version = client.getConfigVersion();
|
|
203
|
+
const stableId = client.getStableId();
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### useTrafficalPlugin
|
|
210
|
+
|
|
211
|
+
Hook to access a registered plugin by name.
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { createDOMBindingPlugin, DOMBindingPlugin } from '@traffical/react';
|
|
215
|
+
|
|
216
|
+
// In your provider config:
|
|
217
|
+
// plugins: [createDOMBindingPlugin()]
|
|
218
|
+
|
|
219
|
+
// In a component:
|
|
220
|
+
const domPlugin = useTrafficalPlugin<DOMBindingPlugin>('dom-binding');
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
domPlugin?.applyBindings();
|
|
224
|
+
}, [contentLoaded, domPlugin]);
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Best Practices
|
|
230
|
+
|
|
231
|
+
# Traffical React SDK — Usage Patterns
|
|
232
|
+
|
|
233
|
+
## Mental Model
|
|
234
|
+
|
|
235
|
+
Traffical is **parameter-first**. You define parameters with defaults, and Traffical handles the rest—whether that's a static value, an A/B test, or an adaptive optimization. Your code doesn't need to know which.
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
239
|
+
│ Your Code │
|
|
240
|
+
│ │
|
|
241
|
+
│ 1. Define parameters with defaults │
|
|
242
|
+
│ 2. Use the resolved values │
|
|
243
|
+
│ 3. Track rewards on conversion │
|
|
244
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
245
|
+
▲
|
|
246
|
+
│ (hidden from you)
|
|
247
|
+
▼
|
|
248
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
249
|
+
│ Traffical │
|
|
250
|
+
│ │
|
|
251
|
+
│ • Layers & policies for mutual exclusivity │
|
|
252
|
+
│ • Bucket assignment & deterministic hashing │
|
|
253
|
+
│ • Thompson Sampling & contextual bandits │
|
|
254
|
+
│ • Statistical analysis & optimization │
|
|
255
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Key insight:** Resolution is local and synchronous. The SDK fetches a config bundle once and caches it. Every `useTraffical()` call resolves instantly from cache—no network latency, no render flicker on page navigation.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Quick Start
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
import { useTraffical } from "@traffical/react";
|
|
266
|
+
|
|
267
|
+
function ProductPage() {
|
|
268
|
+
const { params, track } = useTraffical({
|
|
269
|
+
defaults: {
|
|
270
|
+
"ui.cta.text": "Buy Now",
|
|
271
|
+
"ui.cta.color": "#2563eb",
|
|
272
|
+
"pricing.showDiscount": true,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const handlePurchase = (amount: number) => {
|
|
277
|
+
// track has the decisionId already bound!
|
|
278
|
+
track("purchase", { value: amount, itemId: "prod_123" });
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<button
|
|
283
|
+
style={{ backgroundColor: params["ui.cta.color"] }}
|
|
284
|
+
onClick={() => handlePurchase(99.99)}
|
|
285
|
+
>
|
|
286
|
+
{params["ui.cta.text"]}
|
|
287
|
+
</button>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
That's it. Default tracking is enabled automatically, and `track` knows which decision to attribute conversions to.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## API Reference
|
|
297
|
+
|
|
298
|
+
### `useTraffical(options)`
|
|
299
|
+
|
|
300
|
+
The primary hook for parameter resolution and experiment tracking.
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
const { params, decision, ready, error, trackExposure, track } = useTraffical({
|
|
304
|
+
defaults: { /* parameter defaults */ },
|
|
305
|
+
context: { /* optional additional context */ },
|
|
306
|
+
tracking: "full" | "decision" | "none", // default: "full"
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
| Option | Type | Default | Description |
|
|
311
|
+
|--------|------|---------|-------------|
|
|
312
|
+
| `defaults` | `Record<string, ParameterValue>` | *required* | Default values for each parameter |
|
|
313
|
+
| `context` | `Record<string, unknown>` | `{}` | Additional context for targeting |
|
|
314
|
+
| `tracking` | `"full"` \| `"decision"` \| `"none"` | `"full"` | Controls event tracking behavior |
|
|
315
|
+
|
|
316
|
+
**Tracking Modes:**
|
|
317
|
+
|
|
318
|
+
| Mode | Decision Event | Exposure Event | Use Case |
|
|
319
|
+
|------|---------------|----------------|----------|
|
|
320
|
+
| `"full"` | ✅ Auto | ✅ Auto | UI shown to users (default) |
|
|
321
|
+
| `"decision"` | ✅ Auto | 🔧 Manual | Below-the-fold, lazy-loaded content |
|
|
322
|
+
| `"none"` | ❌ No | ❌ No | SSR, internal logic, tests |
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Use Cases
|
|
327
|
+
|
|
328
|
+
### 1. Feature Flag
|
|
329
|
+
|
|
330
|
+
Control feature rollout without redeploying.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
function Dashboard() {
|
|
334
|
+
const { params } = useTraffical({
|
|
335
|
+
defaults: {
|
|
336
|
+
"feature.newAnalytics": false,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (params["feature.newAnalytics"]) {
|
|
341
|
+
return <NewAnalyticsDashboard />;
|
|
342
|
+
}
|
|
343
|
+
return <LegacyDashboard />;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 2. A/B Test with Conversion Tracking
|
|
348
|
+
|
|
349
|
+
Test different variants and measure which performs better.
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
function PricingPage() {
|
|
353
|
+
const { params, track } = useTraffical({
|
|
354
|
+
defaults: {
|
|
355
|
+
"pricing.headline": "Simple, transparent pricing",
|
|
356
|
+
"pricing.showAnnualToggle": false,
|
|
357
|
+
"pricing.highlightPlan": "pro",
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const handleSubscribe = (plan: string, amount: number) => {
|
|
362
|
+
// decisionId is automatically bound
|
|
363
|
+
track("subscription", { value: amount, plan });
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<div>
|
|
368
|
+
<h1>{params["pricing.headline"]}</h1>
|
|
369
|
+
<PricingCards
|
|
370
|
+
showAnnualToggle={params["pricing.showAnnualToggle"]}
|
|
371
|
+
highlightPlan={params["pricing.highlightPlan"]}
|
|
372
|
+
onSubscribe={handleSubscribe}
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 3. Dynamic UI Configuration
|
|
380
|
+
|
|
381
|
+
Adjust colors, copy, and layout without code changes.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
function HeroBanner() {
|
|
385
|
+
const { params } = useTraffical({
|
|
386
|
+
defaults: {
|
|
387
|
+
"ui.hero.title": "Welcome to Our Platform",
|
|
388
|
+
"ui.hero.subtitle": "The best solution for your needs",
|
|
389
|
+
"ui.hero.ctaText": "Get Started",
|
|
390
|
+
"ui.hero.ctaColor": "#3b82f6",
|
|
391
|
+
"ui.hero.layout": "centered",
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<section className={`hero-${params["ui.hero.layout"]}`}>
|
|
397
|
+
<h1>{params["ui.hero.title"]}</h1>
|
|
398
|
+
<p>{params["ui.hero.subtitle"]}</p>
|
|
399
|
+
<button style={{ backgroundColor: params["ui.hero.ctaColor"] }}>
|
|
400
|
+
{params["ui.hero.ctaText"]}
|
|
401
|
+
</button>
|
|
402
|
+
</section>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 4. Below-the-Fold Content (Manual Exposure)
|
|
408
|
+
|
|
409
|
+
Track exposure only when content is actually viewed.
|
|
410
|
+
|
|
411
|
+
```tsx
|
|
412
|
+
function ProductRecommendations() {
|
|
413
|
+
const { params, trackExposure } = useTraffical({
|
|
414
|
+
defaults: {
|
|
415
|
+
"recommendations.algorithm": "collaborative",
|
|
416
|
+
"recommendations.count": 4,
|
|
417
|
+
},
|
|
418
|
+
tracking: "decision", // Decision tracked, exposure manual
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
422
|
+
|
|
423
|
+
// Track exposure when section scrolls into view
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
const observer = new IntersectionObserver(
|
|
426
|
+
([entry]) => {
|
|
427
|
+
if (entry.isIntersecting) {
|
|
428
|
+
trackExposure();
|
|
429
|
+
observer.disconnect();
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
{ threshold: 0.5 }
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
if (ref.current) observer.observe(ref.current);
|
|
436
|
+
return () => observer.disconnect();
|
|
437
|
+
}, [trackExposure]);
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<section ref={ref}>
|
|
441
|
+
<RecommendationGrid
|
|
442
|
+
algorithm={params["recommendations.algorithm"]}
|
|
443
|
+
count={params["recommendations.count"]}
|
|
444
|
+
/>
|
|
445
|
+
</section>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### 5. Server-Side Rendering (No Tracking)
|
|
451
|
+
|
|
452
|
+
Use defaults during SSR, hydrate on client.
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
// Server Component (Next.js App Router)
|
|
456
|
+
async function ProductPage({ productId }: { productId: string }) {
|
|
457
|
+
// Server: use defaults directly (no SDK call)
|
|
458
|
+
const defaultPrice = 299.99;
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<TrafficalProvider>
|
|
462
|
+
<ProductDetails productId={productId} defaultPrice={defaultPrice} />
|
|
463
|
+
</TrafficalProvider>
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Client Component
|
|
468
|
+
"use client";
|
|
469
|
+
function ProductDetails({ productId, defaultPrice }: Props) {
|
|
470
|
+
const { params, ready } = useTraffical({
|
|
471
|
+
defaults: {
|
|
472
|
+
"pricing.basePrice": defaultPrice,
|
|
473
|
+
"pricing.discount": 0,
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Shows defaultPrice immediately, updates when SDK ready
|
|
478
|
+
const price = params["pricing.basePrice"] * (1 - params["pricing.discount"] / 100);
|
|
479
|
+
|
|
480
|
+
return <Price value={price} loading={!ready} />;
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 6. Component with Self-Contained Parameters
|
|
485
|
+
|
|
486
|
+
Reusable component that owns its experiment surface.
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
function CheckoutButton({ onCheckout }: { onCheckout: () => void }) {
|
|
490
|
+
const { params } = useTraffical({
|
|
491
|
+
defaults: {
|
|
492
|
+
"checkout.button.text": "Complete Purchase",
|
|
493
|
+
"checkout.button.color": "#22c55e",
|
|
494
|
+
"checkout.button.showIcon": true,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<button
|
|
500
|
+
onClick={onCheckout}
|
|
501
|
+
style={{ backgroundColor: params["checkout.button.color"] }}
|
|
502
|
+
>
|
|
503
|
+
{params["checkout.button.showIcon"] && <ShoppingCartIcon />}
|
|
504
|
+
{params["checkout.button.text"]}
|
|
505
|
+
</button>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 7. Multiple Event Types
|
|
511
|
+
|
|
512
|
+
Track different conversion events for the same decision.
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
function CheckoutFlow() {
|
|
516
|
+
const { params, track } = useTraffical({
|
|
517
|
+
defaults: {
|
|
518
|
+
"checkout.showExpressOption": true,
|
|
519
|
+
"checkout.showUpsells": false,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const handleAddUpsell = () => {
|
|
524
|
+
track("upsell_accept", { upsellId: "premium" });
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const handleComplete = (orderValue: number) => {
|
|
528
|
+
track("checkout_complete", { value: orderValue });
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<div>
|
|
533
|
+
{params["checkout.showExpressOption"] && <ExpressCheckout />}
|
|
534
|
+
{params["checkout.showUpsells"] && (
|
|
535
|
+
<UpsellSection onAccept={handleAddUpsell} />
|
|
536
|
+
)}
|
|
537
|
+
<CheckoutForm onComplete={handleComplete} />
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## Architecture Patterns
|
|
546
|
+
|
|
547
|
+
### Pattern A: Page-Level Parameters (Recommended for Simple Pages)
|
|
548
|
+
|
|
549
|
+
All parameters defined at page level, passed as props to children.
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
function ProductPage() {
|
|
553
|
+
const { params, decision } = useTraffical({
|
|
554
|
+
defaults: {
|
|
555
|
+
"product.showReviews": true,
|
|
556
|
+
"product.showRelated": true,
|
|
557
|
+
"pricing.discount": 0,
|
|
558
|
+
"ui.ctaColor": "#2563eb",
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<>
|
|
564
|
+
<ProductDetails
|
|
565
|
+
showReviews={params["product.showReviews"]}
|
|
566
|
+
ctaColor={params["ui.ctaColor"]}
|
|
567
|
+
/>
|
|
568
|
+
<PricingSection discount={params["pricing.discount"]} />
|
|
569
|
+
{params["product.showRelated"] && <RelatedProducts />}
|
|
570
|
+
</>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Pros:** Single decision for attribution, clear data flow, testable components
|
|
576
|
+
**Cons:** Prop drilling, parent knows about all params
|
|
577
|
+
|
|
578
|
+
### Pattern B: Component-Level Parameters (Recommended for Reusable Components)
|
|
579
|
+
|
|
580
|
+
Each component owns its parameters.
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
583
|
+
// ProductDetails owns its params
|
|
584
|
+
function ProductDetails() {
|
|
585
|
+
const { params } = useTraffical({
|
|
586
|
+
defaults: {
|
|
587
|
+
"product.showReviews": true,
|
|
588
|
+
"product.imageSize": "large",
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
// ...
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// PricingSection owns its params
|
|
595
|
+
function PricingSection() {
|
|
596
|
+
const { params } = useTraffical({
|
|
597
|
+
defaults: {
|
|
598
|
+
"pricing.discount": 0,
|
|
599
|
+
"pricing.showOriginal": true,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
// ...
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
**Pros:** Encapsulated, portable, self-documenting
|
|
607
|
+
**Cons:** Multiple decisions (handled via deduplication)
|
|
608
|
+
|
|
609
|
+
### Pattern C: Context + Pure Components (Recommended for Complex Pages)
|
|
610
|
+
|
|
611
|
+
Single decision distributed via context, pure components for rendering.
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
// Context provider with all params
|
|
615
|
+
function ProductPageProvider({ children }) {
|
|
616
|
+
const traffical = useTraffical({
|
|
617
|
+
defaults: {
|
|
618
|
+
"product.showReviews": true,
|
|
619
|
+
"pricing.discount": 0,
|
|
620
|
+
"ui.ctaColor": "#2563eb",
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
return (
|
|
625
|
+
<ProductPageContext.Provider value={traffical}>
|
|
626
|
+
{children}
|
|
627
|
+
</ProductPageContext.Provider>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Pure component, testable without Traffical
|
|
632
|
+
function PricingSection({ discount, showOriginal }: Props) {
|
|
633
|
+
// Pure rendering logic
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Wrapper that connects to Traffical
|
|
637
|
+
function ConnectedPricingSection() {
|
|
638
|
+
const { params } = useProductPageContext();
|
|
639
|
+
return (
|
|
640
|
+
<PricingSection
|
|
641
|
+
discount={params["pricing.discount"]}
|
|
642
|
+
showOriginal={params["pricing.showOriginal"]}
|
|
643
|
+
/>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**Pros:** Single decision, no prop drilling, testable leaf components
|
|
649
|
+
**Cons:** More boilerplate
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
## Best Practices
|
|
654
|
+
|
|
655
|
+
### 1. Always Provide Sensible Defaults
|
|
656
|
+
|
|
657
|
+
Defaults are used when:
|
|
658
|
+
- No experiment is running
|
|
659
|
+
- User doesn't match targeting conditions
|
|
660
|
+
- SDK is still loading
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
// ✅ Good: Works without any experiment
|
|
664
|
+
const { params } = useTraffical({
|
|
665
|
+
defaults: {
|
|
666
|
+
"pricing.discount": 0,
|
|
667
|
+
"ui.buttonColor": "#3b82f6",
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// ❌ Bad: Undefined behavior without experiment
|
|
672
|
+
const { params } = useTraffical({
|
|
673
|
+
defaults: {
|
|
674
|
+
"pricing.discount": undefined, // What does this mean?
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### 2. Group Related Parameters
|
|
680
|
+
|
|
681
|
+
Parameters that should vary together belong in the same `useTraffical()` call.
|
|
682
|
+
|
|
683
|
+
```tsx
|
|
684
|
+
// ✅ Good: Related params together
|
|
685
|
+
const { params } = useTraffical({
|
|
686
|
+
defaults: {
|
|
687
|
+
"pricing.basePrice": 299,
|
|
688
|
+
"pricing.discount": 0,
|
|
689
|
+
"pricing.showOriginal": true,
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// ⚠️ Caution: Separate calls = separate decisions
|
|
694
|
+
const pricing = useTraffical({ defaults: { "pricing.basePrice": 299 } });
|
|
695
|
+
const discount = useTraffical({ defaults: { "pricing.discount": 0 } });
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### 3. Track Events at Conversion Points
|
|
699
|
+
|
|
700
|
+
Events enable Traffical to learn which variants perform best. Use the bound `track` from `useTraffical()` — it automatically includes the `decisionId`.
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
const { params, track } = useTraffical({
|
|
704
|
+
defaults: { "checkout.showUpsells": false },
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// ✅ Track meaningful conversions
|
|
708
|
+
const handlePurchase = (amount: number) => {
|
|
709
|
+
track("purchase", { value: amount, orderId: "ord_123" });
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// ✅ Track micro-conversions too
|
|
713
|
+
const handleAddToCart = () => {
|
|
714
|
+
track("add_to_cart", { itemId: "sku_456" });
|
|
715
|
+
};
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### 4. Use Consistent Naming Conventions
|
|
719
|
+
|
|
720
|
+
```
|
|
721
|
+
category.subcategory.name
|
|
722
|
+
|
|
723
|
+
feature.* → Feature flags (boolean)
|
|
724
|
+
ui.* → Visual variations (string, number)
|
|
725
|
+
pricing.* → Pricing experiments (number)
|
|
726
|
+
copy.* → Copywriting tests (string)
|
|
727
|
+
experiment.* → Explicit variants (string)
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### 5. Handle Loading State
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
const { params, ready } = useTraffical({
|
|
734
|
+
defaults: { "ui.heroVariant": "default" },
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Option A: Show defaults immediately (recommended)
|
|
738
|
+
// On page navigation, resolved values render immediately (no flicker)
|
|
739
|
+
return <Hero variant={params["ui.heroVariant"]} />;
|
|
740
|
+
|
|
741
|
+
// Option B: Show loading state (only for initial page load if needed)
|
|
742
|
+
if (!ready) return <HeroSkeleton />;
|
|
743
|
+
return <Hero variant={params["ui.heroVariant"]} />;
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
> **Note:** On client-side navigation (e.g., Next.js Link), params resolve synchronously—no loading state or flicker. Loading states are only relevant during the initial bundle fetch.
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Flicker-Free SSR (Next.js App Router)
|
|
751
|
+
|
|
752
|
+
The classic A/B testing problem: users briefly see the default content before it switches to their assigned variant. This section shows how to eliminate that flicker entirely.
|
|
753
|
+
|
|
754
|
+
### The Problem
|
|
755
|
+
|
|
756
|
+
Without special handling, here's what happens:
|
|
757
|
+
1. Server renders with defaults (no userId during SSR)
|
|
758
|
+
2. Client hydrates with defaults
|
|
759
|
+
3. SDK fetches config bundle
|
|
760
|
+
4. SDK resolves with userId → content changes (FLICKER!)
|
|
761
|
+
|
|
762
|
+
### The Solution: Cookie-Based SSR + LocalConfig
|
|
763
|
+
|
|
764
|
+
By passing the userId from server to client via cookies AND embedding the config bundle at build time, resolution can happen synchronously on both server and client.
|
|
765
|
+
|
|
766
|
+
#### Step 1: Middleware to Set UserId Cookie
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
// middleware.ts
|
|
770
|
+
import { NextResponse } from 'next/server';
|
|
771
|
+
import type { NextRequest } from 'next/server';
|
|
772
|
+
|
|
773
|
+
const COOKIE_NAME = 'traffical-userId';
|
|
774
|
+
const HEADER_NAME = 'x-traffical-userId';
|
|
775
|
+
|
|
776
|
+
function generateUserId(): string {
|
|
777
|
+
const array = new Uint8Array(6);
|
|
778
|
+
crypto.getRandomValues(array);
|
|
779
|
+
return `user_${Array.from(array, b => b.toString(16).padStart(2, '0')).join('')}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export function middleware(request: NextRequest) {
|
|
783
|
+
const existingUserId = request.cookies.get(COOKIE_NAME)?.value;
|
|
784
|
+
const userId = existingUserId || generateUserId();
|
|
785
|
+
|
|
786
|
+
// Pass userId via header for THIS request (cookie isn't available yet on first request)
|
|
787
|
+
const requestHeaders = new Headers(request.headers);
|
|
788
|
+
requestHeaders.set(HEADER_NAME, userId);
|
|
789
|
+
|
|
790
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
791
|
+
|
|
792
|
+
// Set cookie for NEXT request
|
|
793
|
+
if (!existingUserId) {
|
|
794
|
+
response.cookies.set(COOKIE_NAME, userId, {
|
|
795
|
+
httpOnly: false,
|
|
796
|
+
secure: process.env.NODE_ENV === 'production',
|
|
797
|
+
sameSite: 'lax',
|
|
798
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
799
|
+
path: '/',
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return response;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export const config = {
|
|
807
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|.*\\..*$).*)'],
|
|
808
|
+
};
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
#### Step 2: Server Layout Reads UserId
|
|
812
|
+
|
|
813
|
+
```tsx
|
|
814
|
+
// app/layout.tsx
|
|
815
|
+
import { cookies, headers } from 'next/headers';
|
|
816
|
+
|
|
817
|
+
export default async function RootLayout({ children }) {
|
|
818
|
+
const headerStore = await headers();
|
|
819
|
+
const cookieStore = await cookies();
|
|
820
|
+
|
|
821
|
+
// Header for first request, cookie for subsequent
|
|
822
|
+
const userId = headerStore.get('x-traffical-userId') ||
|
|
823
|
+
cookieStore.get('traffical-userId')?.value || '';
|
|
824
|
+
|
|
825
|
+
return (
|
|
826
|
+
<html>
|
|
827
|
+
<body>
|
|
828
|
+
<Providers initialUserId={userId}>
|
|
829
|
+
{children}
|
|
830
|
+
</Providers>
|
|
831
|
+
</body>
|
|
832
|
+
</html>
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
#### Step 3: Pass UserId Through Context
|
|
838
|
+
|
|
839
|
+
```tsx
|
|
840
|
+
// context/Providers.tsx
|
|
841
|
+
'use client';
|
|
842
|
+
|
|
843
|
+
export function Providers({ children, initialUserId }) {
|
|
844
|
+
return (
|
|
845
|
+
<DemoProvider initialUserId={initialUserId}>
|
|
846
|
+
<TrafficalWrapper>
|
|
847
|
+
{children}
|
|
848
|
+
</TrafficalWrapper>
|
|
849
|
+
</DemoProvider>
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// context/DemoContext.tsx
|
|
854
|
+
export function DemoProvider({ children, initialUserId }) {
|
|
855
|
+
const [userId] = useState(initialUserId || '');
|
|
856
|
+
|
|
857
|
+
// Use userId as initial state - NOT in useEffect
|
|
858
|
+
// ...
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
#### Step 4: Provide LocalConfig to SDK
|
|
863
|
+
|
|
864
|
+
Fetch the config bundle at build time and pass it to the provider:
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
// lib/traffical.ts
|
|
868
|
+
import configBundle from '@/data/config-bundle.json';
|
|
869
|
+
|
|
870
|
+
export const trafficalConfig = {
|
|
871
|
+
orgId: process.env.NEXT_PUBLIC_TRAFFICAL_ORG_ID,
|
|
872
|
+
projectId: process.env.NEXT_PUBLIC_TRAFFICAL_PROJECT_ID,
|
|
873
|
+
apiKey: process.env.NEXT_PUBLIC_TRAFFICAL_API_KEY,
|
|
874
|
+
// This is the key to flicker-free SSR!
|
|
875
|
+
localConfig: configBundle as ConfigBundle,
|
|
876
|
+
};
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
#### Step 5: TrafficalWrapper Uses UserId
|
|
880
|
+
|
|
881
|
+
```tsx
|
|
882
|
+
// context/TrafficalWrapper.tsx
|
|
883
|
+
export function TrafficalWrapper({ children }) {
|
|
884
|
+
const { userId } = useDemoContext();
|
|
885
|
+
|
|
886
|
+
const config = useMemo(() => ({
|
|
887
|
+
...trafficalConfig,
|
|
888
|
+
unitKeyFn: () => userId, // Returns the server-provided userId
|
|
889
|
+
}), [userId]);
|
|
890
|
+
|
|
891
|
+
return (
|
|
892
|
+
<TrafficalProvider config={config}>
|
|
893
|
+
{children}
|
|
894
|
+
</TrafficalProvider>
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### How It Works
|
|
900
|
+
|
|
901
|
+
```
|
|
902
|
+
Request Flow (First Visit):
|
|
903
|
+
─────────────────────────────────────────────────────────────────
|
|
904
|
+
1. Request arrives (no cookie)
|
|
905
|
+
2. Middleware generates userId → sets HEADER + COOKIE
|
|
906
|
+
3. Server layout reads userId from HEADER
|
|
907
|
+
4. Server passes userId to React via props
|
|
908
|
+
5. useTraffical's useState resolves from localConfig + userId
|
|
909
|
+
6. Server renders HTML with CORRECT variant
|
|
910
|
+
7. Response sent with Set-Cookie header
|
|
911
|
+
8. Client hydrates with SAME userId → NO FLICKER ✅
|
|
912
|
+
─────────────────────────────────────────────────────────────────
|
|
913
|
+
|
|
914
|
+
Subsequent Requests:
|
|
915
|
+
─────────────────────────────────────────────────────────────────
|
|
916
|
+
1. Request arrives WITH cookie
|
|
917
|
+
2. Middleware passes existing userId via header
|
|
918
|
+
3. Same flow as above → consistent experience
|
|
919
|
+
─────────────────────────────────────────────────────────────────
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
### Requirements
|
|
923
|
+
|
|
924
|
+
| Requirement | Why |
|
|
925
|
+
|-------------|-----|
|
|
926
|
+
| `localConfig` | Enables synchronous resolution without waiting for network |
|
|
927
|
+
| UserId in cookies | Server can read it during SSR |
|
|
928
|
+
| UserId via header on first request | Cookie isn't in request until second request |
|
|
929
|
+
| UserId as initial state (not useEffect) | Prevents hydration mismatch |
|
|
930
|
+
|
|
931
|
+
### What This Solves
|
|
932
|
+
|
|
933
|
+
- ✅ **First page load** - No flicker, correct variant from the start
|
|
934
|
+
- ✅ **Client-side navigation** - Already worked (bundle cached)
|
|
935
|
+
- ✅ **Page refresh** - UserId persisted in cookie
|
|
936
|
+
- ✅ **New users** - UserId generated on first request
|
|
937
|
+
|
|
938
|
+
---
|
|
939
|
+
|
|
940
|
+
## FAQ
|
|
941
|
+
|
|
942
|
+
**Q: Do multiple `useTraffical()` calls cause multiple network requests?**
|
|
943
|
+
|
|
944
|
+
No. The SDK fetches the config bundle once and caches it. All resolution happens locally.
|
|
945
|
+
|
|
946
|
+
**Q: What happens if the SDK fails to load?**
|
|
947
|
+
|
|
948
|
+
Defaults are returned. Your app works normally, just without experiment variations.
|
|
949
|
+
|
|
950
|
+
**Q: Should I use `tracking: "none"` for SSR?**
|
|
951
|
+
|
|
952
|
+
Yes, if you're calling `useTraffical` in a server context. On the client, use the default `"full"` tracking.
|
|
953
|
+
|
|
954
|
+
**Q: Can I change parameter values from the dashboard without deploying?**
|
|
955
|
+
|
|
956
|
+
Yes! That's the point. Parameters are resolved from Traffical's config bundle, which updates independently of your code.
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
## Migration from Deprecated Hooks
|
|
962
|
+
|
|
963
|
+
The `useTrafficalParams` and `useTrafficalDecision` hooks are deprecated but still available for backward compatibility.
|
|
964
|
+
|
|
965
|
+
### useTrafficalParams → useTraffical
|
|
966
|
+
|
|
967
|
+
```tsx
|
|
968
|
+
// Before (deprecated)
|
|
969
|
+
const { params, ready } = useTrafficalParams({
|
|
970
|
+
defaults: { 'ui.hero.title': 'Welcome' },
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// After
|
|
974
|
+
const { params, ready } = useTraffical({
|
|
975
|
+
defaults: { 'ui.hero.title': 'Welcome' },
|
|
976
|
+
tracking: 'none',
|
|
977
|
+
});
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
### useTrafficalDecision → useTraffical
|
|
981
|
+
|
|
982
|
+
```tsx
|
|
983
|
+
// Before (deprecated) - auto exposure
|
|
984
|
+
const { params, decision } = useTrafficalDecision({
|
|
985
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// After
|
|
989
|
+
const { params, decision } = useTraffical({
|
|
990
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Before (deprecated) - manual exposure
|
|
994
|
+
const { params, trackExposure } = useTrafficalDecision({
|
|
995
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
996
|
+
trackExposure: false,
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// After
|
|
1000
|
+
const { params, trackExposure } = useTraffical({
|
|
1001
|
+
defaults: { 'checkout.ctaText': 'Buy Now' },
|
|
1002
|
+
tracking: 'decision',
|
|
1003
|
+
});
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
## License
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
|