@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/src/hooks.ts
ADDED
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traffical React Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for parameter resolution and decision tracking.
|
|
5
|
+
* Uses the browser-optimized JS Client for full feature support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
9
|
+
import type {
|
|
10
|
+
ParameterValue,
|
|
11
|
+
DecisionResult,
|
|
12
|
+
Context,
|
|
13
|
+
} from "@traffical/core";
|
|
14
|
+
import { resolveParameters } from "@traffical/core";
|
|
15
|
+
import type { TrafficalPlugin } from "@traffical/js-client";
|
|
16
|
+
import { useTrafficalContext } from "./context.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Internal Utilities
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a stable string key from an object for use in dependency arrays.
|
|
24
|
+
* This prevents infinite re-renders when users pass inline objects to hooks.
|
|
25
|
+
*
|
|
26
|
+
* Uses JSON.stringify with sorted keys to ensure consistent ordering.
|
|
27
|
+
*/
|
|
28
|
+
function createStableKey(obj: unknown): string {
|
|
29
|
+
if (obj === null || obj === undefined) {
|
|
30
|
+
return String(obj);
|
|
31
|
+
}
|
|
32
|
+
if (typeof obj !== "object") {
|
|
33
|
+
return String(obj);
|
|
34
|
+
}
|
|
35
|
+
// Sort keys for consistent ordering
|
|
36
|
+
return JSON.stringify(obj, Object.keys(obj as object).sort());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom hook that returns a stable reference to an object.
|
|
41
|
+
* Only updates the reference when the object's serialized value changes.
|
|
42
|
+
*
|
|
43
|
+
* This allows users to pass inline objects without causing infinite re-renders:
|
|
44
|
+
* ```tsx
|
|
45
|
+
* // This now works without memoization!
|
|
46
|
+
* const { params } = useTraffical({
|
|
47
|
+
* defaults: { 'ui.color': '#000' }, // inline object is fine
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function useStableObject<T>(obj: T): T {
|
|
52
|
+
const stableKey = createStableKey(obj);
|
|
53
|
+
const ref = useRef(obj);
|
|
54
|
+
|
|
55
|
+
// Only update the ref when the serialized value actually changes
|
|
56
|
+
// This is safe because we're comparing by value, not reference
|
|
57
|
+
if (createStableKey(ref.current) !== stableKey) {
|
|
58
|
+
ref.current = obj;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ref.current;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// useTraffical - Primary Hook
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Options for the useTraffical hook.
|
|
70
|
+
*/
|
|
71
|
+
export interface UseTrafficalOptions<T> {
|
|
72
|
+
/** Default parameter values */
|
|
73
|
+
defaults: T;
|
|
74
|
+
|
|
75
|
+
/** Additional context (optional) */
|
|
76
|
+
context?: Context;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracking mode (default: "full")
|
|
80
|
+
* - "full": Track decision + exposure (default, recommended for UI)
|
|
81
|
+
* - "decision": Track decision only, manual exposure control
|
|
82
|
+
* - "none": No tracking (SSR, internal logic, tests)
|
|
83
|
+
*/
|
|
84
|
+
tracking?: "full" | "decision" | "none";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Options for the bound track function returned by useTraffical.
|
|
89
|
+
*/
|
|
90
|
+
export interface BoundTrackOptions {
|
|
91
|
+
/** Additional event properties */
|
|
92
|
+
properties?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @deprecated Use BoundTrackOptions instead.
|
|
97
|
+
*/
|
|
98
|
+
export interface BoundTrackRewardOptions {
|
|
99
|
+
/** The reward value (e.g., revenue amount, conversion count) */
|
|
100
|
+
reward: number;
|
|
101
|
+
/** Type of reward (e.g., "revenue", "conversion", "engagement") */
|
|
102
|
+
rewardType?: string;
|
|
103
|
+
/** Multiple reward values keyed by type */
|
|
104
|
+
rewards?: Record<string, number>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Return value from the useTraffical hook.
|
|
109
|
+
*/
|
|
110
|
+
export interface UseTrafficalResult<T> {
|
|
111
|
+
/** Resolved parameter values */
|
|
112
|
+
params: T;
|
|
113
|
+
/** The full decision result (null when tracking="none") */
|
|
114
|
+
decision: DecisionResult | null;
|
|
115
|
+
/** Whether the client is ready (config loaded) */
|
|
116
|
+
ready: boolean;
|
|
117
|
+
/** Any error that occurred */
|
|
118
|
+
error: Error | null;
|
|
119
|
+
/** Function to manually track exposure (no-op when tracking="none") */
|
|
120
|
+
trackExposure: () => void;
|
|
121
|
+
/**
|
|
122
|
+
* Track a user event. The decisionId is automatically bound.
|
|
123
|
+
* No-op if tracking="none" or no decision is available.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* track('purchase', { value: 99.99, orderId: 'ord_123' });
|
|
127
|
+
* track('add_to_cart', { itemId: 'sku_456' });
|
|
128
|
+
*/
|
|
129
|
+
track: (event: string, properties?: Record<string, unknown>) => void;
|
|
130
|
+
/**
|
|
131
|
+
* @deprecated Use track() instead.
|
|
132
|
+
* Track a reward for this decision. The decisionId is automatically bound.
|
|
133
|
+
* No-op if tracking="none" or no decision is available.
|
|
134
|
+
*/
|
|
135
|
+
trackReward: (options: BoundTrackRewardOptions) => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Primary hook for Traffical parameter resolution and decision tracking.
|
|
140
|
+
*
|
|
141
|
+
* On first render, returns defaults immediately (no blocking).
|
|
142
|
+
* When the config bundle loads, recomputes and returns resolved values.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```tsx
|
|
146
|
+
* // Full tracking (default) - decision + exposure events
|
|
147
|
+
* const { params, decision, ready } = useTraffical({
|
|
148
|
+
* defaults: { "checkout.ctaText": "Buy Now" },
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // Decision tracking only - manual exposure control
|
|
152
|
+
* const { params, decision, trackExposure } = useTraffical({
|
|
153
|
+
* defaults: { "checkout.ctaText": "Buy Now" },
|
|
154
|
+
* tracking: "decision",
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* // No tracking - for SSR, tests, or internal logic
|
|
158
|
+
* const { params, ready } = useTraffical({
|
|
159
|
+
* defaults: { "ui.hero.title": "Welcome" },
|
|
160
|
+
* tracking: "none",
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function useTraffical<T extends Record<string, ParameterValue>>(
|
|
165
|
+
options: UseTrafficalOptions<T>
|
|
166
|
+
): UseTrafficalResult<T> {
|
|
167
|
+
const { client, ready, error, getContext, getUnitKey, initialParams, localConfig } =
|
|
168
|
+
useTrafficalContext();
|
|
169
|
+
|
|
170
|
+
const trackingMode = options.tracking ?? "full";
|
|
171
|
+
const shouldTrackDecision = trackingMode !== "none";
|
|
172
|
+
const shouldAutoTrackExposure = trackingMode === "full";
|
|
173
|
+
|
|
174
|
+
// Create stable references for objects to prevent infinite re-renders
|
|
175
|
+
// when users pass inline objects like: useTraffical({ defaults: { ... } })
|
|
176
|
+
const stableDefaults = useStableObject(options.defaults);
|
|
177
|
+
const stableContext = useStableObject(options.context);
|
|
178
|
+
|
|
179
|
+
// Track if we resolved synchronously (to avoid duplicate resolution in useEffect)
|
|
180
|
+
const resolvedSyncRef = useRef(false);
|
|
181
|
+
const syncDecisionRef = useRef<DecisionResult | null>(null);
|
|
182
|
+
|
|
183
|
+
// State - resolve synchronously if possible to prevent flicker
|
|
184
|
+
const [params, setParams] = useState<T>(() => {
|
|
185
|
+
// If client is already ready (e.g., subsequent page navigation), resolve synchronously
|
|
186
|
+
// This prevents the default -> resolved flicker (classic A/B testing problem)
|
|
187
|
+
if (client && ready) {
|
|
188
|
+
resolvedSyncRef.current = true;
|
|
189
|
+
|
|
190
|
+
const context: Context = {
|
|
191
|
+
...getContext(),
|
|
192
|
+
...(options.context ?? {}),
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (shouldTrackDecision) {
|
|
196
|
+
// Use decide() for tracked decisions
|
|
197
|
+
const result = client.decide({
|
|
198
|
+
context,
|
|
199
|
+
defaults: options.defaults,
|
|
200
|
+
});
|
|
201
|
+
syncDecisionRef.current = result;
|
|
202
|
+
return result.assignments as T;
|
|
203
|
+
} else {
|
|
204
|
+
// Use getParams() for untracked
|
|
205
|
+
return client.getParams({
|
|
206
|
+
context,
|
|
207
|
+
defaults: options.defaults,
|
|
208
|
+
}) as T;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// NEW: If we have localConfig bundle, resolve synchronously even before client is ready
|
|
213
|
+
// This is the key to flicker-free SSR: server and client both resolve from the same bundle
|
|
214
|
+
if (localConfig) {
|
|
215
|
+
try {
|
|
216
|
+
const context = getContext();
|
|
217
|
+
// Only resolve if we have a userId (set by server via cookie/header)
|
|
218
|
+
if (context.userId) {
|
|
219
|
+
resolvedSyncRef.current = true;
|
|
220
|
+
const fullContext: Context = {
|
|
221
|
+
...context,
|
|
222
|
+
...(options.context ?? {}),
|
|
223
|
+
};
|
|
224
|
+
// Use pure resolution function from core - no tracking on initial render
|
|
225
|
+
const resolved = resolveParameters(localConfig, fullContext, options.defaults);
|
|
226
|
+
return resolved as T;
|
|
227
|
+
}
|
|
228
|
+
} catch {
|
|
229
|
+
// Context function not ready, fall through to defaults
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Fallback to defaults (no localConfig or no userId)
|
|
234
|
+
if (initialParams) {
|
|
235
|
+
return { ...stableDefaults, ...initialParams } as T;
|
|
236
|
+
}
|
|
237
|
+
return stableDefaults;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const [decision, setDecision] = useState<DecisionResult | null>(
|
|
241
|
+
() => syncDecisionRef.current
|
|
242
|
+
);
|
|
243
|
+
const [hasTrackedExposure, setHasTrackedExposure] = useState(false);
|
|
244
|
+
|
|
245
|
+
// Manual exposure tracking (synchronous - batched internally)
|
|
246
|
+
const trackExposure = useCallback(() => {
|
|
247
|
+
// No-op when tracking is "none"
|
|
248
|
+
if (trackingMode === "none") {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (!client || !decision || hasTrackedExposure) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
client.trackExposure(decision);
|
|
255
|
+
setHasTrackedExposure(true);
|
|
256
|
+
}, [client, decision, hasTrackedExposure, trackingMode]);
|
|
257
|
+
|
|
258
|
+
// Resolve params or make decision when client is ready
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (!client || !ready) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Build context using stable references
|
|
265
|
+
const context: Context = {
|
|
266
|
+
...getContext(),
|
|
267
|
+
...stableContext,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (shouldTrackDecision) {
|
|
271
|
+
// Use decide() - tracks decision event via DecisionTrackingPlugin
|
|
272
|
+
const result = client.decide({
|
|
273
|
+
context,
|
|
274
|
+
defaults: stableDefaults,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Only update state if we didn't already resolve synchronously
|
|
278
|
+
// This prevents the params from flickering (default -> resolved)
|
|
279
|
+
// But we ALWAYS call decide() to ensure tracking happens
|
|
280
|
+
if (!resolvedSyncRef.current) {
|
|
281
|
+
setParams(result.assignments as T);
|
|
282
|
+
}
|
|
283
|
+
setDecision(result);
|
|
284
|
+
setHasTrackedExposure(false);
|
|
285
|
+
} else {
|
|
286
|
+
// Use getParams() - no tracking
|
|
287
|
+
if (!resolvedSyncRef.current) {
|
|
288
|
+
const resolved = client.getParams({
|
|
289
|
+
context,
|
|
290
|
+
defaults: stableDefaults,
|
|
291
|
+
});
|
|
292
|
+
setParams(resolved as T);
|
|
293
|
+
}
|
|
294
|
+
setDecision(null);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Clear the sync flag after first effect run
|
|
298
|
+
resolvedSyncRef.current = false;
|
|
299
|
+
}, [client, ready, getContext, stableContext, stableDefaults, shouldTrackDecision]);
|
|
300
|
+
|
|
301
|
+
// Auto-track exposure when tracking is "full"
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (shouldAutoTrackExposure && decision && !hasTrackedExposure) {
|
|
304
|
+
trackExposure();
|
|
305
|
+
}
|
|
306
|
+
}, [shouldAutoTrackExposure, decision, hasTrackedExposure, trackExposure]);
|
|
307
|
+
|
|
308
|
+
// Ref to store current decision for stable track function reference
|
|
309
|
+
const decisionRef = useRef<DecisionResult | null>(null);
|
|
310
|
+
decisionRef.current = decision;
|
|
311
|
+
|
|
312
|
+
// Buffer for track events that arrive before decision is ready
|
|
313
|
+
// This prevents race conditions where track() is called in a useEffect
|
|
314
|
+
// that runs before the decision has been set
|
|
315
|
+
const pendingTracksRef = useRef<Array<{ event: string; properties?: Record<string, unknown> }>>([]);
|
|
316
|
+
|
|
317
|
+
// Flush pending track events when decision becomes available
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (decision && client && pendingTracksRef.current.length > 0) {
|
|
320
|
+
const pending = pendingTracksRef.current;
|
|
321
|
+
pendingTracksRef.current = [];
|
|
322
|
+
|
|
323
|
+
for (const { event, properties } of pending) {
|
|
324
|
+
client.track(event, properties, {
|
|
325
|
+
decisionId: decision.decisionId,
|
|
326
|
+
unitKey: getUnitKey(),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}, [decision, client, getUnitKey]);
|
|
331
|
+
|
|
332
|
+
// Track user events - decisionId is automatically included
|
|
333
|
+
// If decision isn't ready yet, events are queued and flushed when it becomes available
|
|
334
|
+
const track = useCallback(
|
|
335
|
+
(event: string, properties?: Record<string, unknown>) => {
|
|
336
|
+
if (!client) {
|
|
337
|
+
console.warn("[Traffical] Client not initialized, cannot track event");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const currentDecision = decisionRef.current;
|
|
342
|
+
if (!currentDecision) {
|
|
343
|
+
// Queue the event instead of dropping it - will be flushed when decision is ready
|
|
344
|
+
// This handles the race condition where track() is called before decision is set
|
|
345
|
+
if (trackingMode === "none") {
|
|
346
|
+
console.warn(
|
|
347
|
+
"[Traffical] Cannot track event with tracking: 'none'. Use tracking: 'full' or 'decision'."
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
pendingTracksRef.current.push({ event, properties });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
client.track(event, properties, {
|
|
356
|
+
decisionId: currentDecision.decisionId,
|
|
357
|
+
unitKey: getUnitKey(),
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
[client, getUnitKey, trackingMode]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Deprecated: Bound reward tracking - decisionId is automatically included
|
|
364
|
+
const trackReward = useCallback(
|
|
365
|
+
(options: BoundTrackRewardOptions) => {
|
|
366
|
+
if (!client) {
|
|
367
|
+
console.warn("[Traffical] Client not initialized, cannot track reward");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const currentDecision = decisionRef.current;
|
|
371
|
+
if (!currentDecision) {
|
|
372
|
+
console.warn(
|
|
373
|
+
"[Traffical] No decision available, cannot track reward. Did you use tracking: 'none'?"
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
// Map old API to new track() API
|
|
378
|
+
track(options.rewardType || "reward", {
|
|
379
|
+
value: options.reward,
|
|
380
|
+
...(options.rewards ? { rewards: options.rewards } : {}),
|
|
381
|
+
});
|
|
382
|
+
},
|
|
383
|
+
[client, track]
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
return { params, decision, ready, error, trackExposure, track, trackReward };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// Deprecated Hooks (for backward compatibility)
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Options for useTrafficalParams hook.
|
|
395
|
+
* @deprecated Use UseTrafficalOptions instead.
|
|
396
|
+
*/
|
|
397
|
+
export interface UseTrafficalParamsOptions<T extends Record<string, ParameterValue>> {
|
|
398
|
+
/** Default values for parameters */
|
|
399
|
+
defaults: T;
|
|
400
|
+
/** Additional context to merge (optional) */
|
|
401
|
+
context?: Context;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Return value from useTrafficalParams hook.
|
|
406
|
+
* @deprecated Use UseTrafficalResult instead.
|
|
407
|
+
*/
|
|
408
|
+
export interface UseTrafficalParamsResult<T> {
|
|
409
|
+
/** Resolved parameter values */
|
|
410
|
+
params: T;
|
|
411
|
+
/** Whether the client is ready (config loaded) */
|
|
412
|
+
ready: boolean;
|
|
413
|
+
/** Any error that occurred */
|
|
414
|
+
error: Error | null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Hook to get resolved parameter values.
|
|
419
|
+
*
|
|
420
|
+
* @deprecated Use `useTraffical({ tracking: "none" })` instead.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```tsx
|
|
424
|
+
* // Old way (deprecated)
|
|
425
|
+
* const { params, ready } = useTrafficalParams({ defaults: { ... } });
|
|
426
|
+
*
|
|
427
|
+
* // New way
|
|
428
|
+
* const { params, ready } = useTraffical({ defaults: { ... }, tracking: "none" });
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
export function useTrafficalParams<T extends Record<string, ParameterValue>>(
|
|
432
|
+
options: UseTrafficalParamsOptions<T>
|
|
433
|
+
): UseTrafficalParamsResult<T> {
|
|
434
|
+
// Show deprecation warning in development only
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (process.env.NODE_ENV === "development") {
|
|
437
|
+
console.warn(
|
|
438
|
+
'[Traffical] useTrafficalParams is deprecated. Use useTraffical({ tracking: "none" }) instead.'
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}, []);
|
|
442
|
+
|
|
443
|
+
const result = useTraffical({ ...options, tracking: "none" });
|
|
444
|
+
return { params: result.params, ready: result.ready, error: result.error };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Options for useTrafficalDecision hook.
|
|
449
|
+
* @deprecated Use UseTrafficalOptions instead.
|
|
450
|
+
*/
|
|
451
|
+
export interface UseTrafficalDecisionOptions<T extends Record<string, ParameterValue>> {
|
|
452
|
+
/** Default values for parameters */
|
|
453
|
+
defaults: T;
|
|
454
|
+
/** Additional context to merge (optional) */
|
|
455
|
+
context?: Context;
|
|
456
|
+
/**
|
|
457
|
+
* Whether to automatically track exposure (default: true).
|
|
458
|
+
* Set to false if you want to manually control when exposure is tracked
|
|
459
|
+
* (e.g., when an element scrolls into view).
|
|
460
|
+
*
|
|
461
|
+
* Note: Decision tracking happens automatically via DecisionTrackingPlugin
|
|
462
|
+
* and is separate from exposure tracking.
|
|
463
|
+
*/
|
|
464
|
+
trackExposure?: boolean;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Return value from useTrafficalDecision hook.
|
|
469
|
+
* @deprecated Use UseTrafficalResult instead.
|
|
470
|
+
*/
|
|
471
|
+
export interface UseTrafficalDecisionResult<T> {
|
|
472
|
+
/** Resolved parameter values */
|
|
473
|
+
params: T;
|
|
474
|
+
/** The full decision result (for tracking) */
|
|
475
|
+
decision: DecisionResult | null;
|
|
476
|
+
/** Whether the client is ready (config loaded) */
|
|
477
|
+
ready: boolean;
|
|
478
|
+
/** Any error that occurred */
|
|
479
|
+
error: Error | null;
|
|
480
|
+
/**
|
|
481
|
+
* Function to manually track exposure.
|
|
482
|
+
* Note: This is synchronous - events are batched internally.
|
|
483
|
+
*/
|
|
484
|
+
trackExposure: () => void;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Hook to get a decision with full metadata for tracking.
|
|
489
|
+
*
|
|
490
|
+
* @deprecated Use `useTraffical()` instead.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```tsx
|
|
494
|
+
* // Old way (deprecated)
|
|
495
|
+
* const { params, decision } = useTrafficalDecision({ defaults: { ... } });
|
|
496
|
+
*
|
|
497
|
+
* // New way
|
|
498
|
+
* const { params, decision } = useTraffical({ defaults: { ... } });
|
|
499
|
+
*
|
|
500
|
+
* // Old way with manual exposure (deprecated)
|
|
501
|
+
* const { params, trackExposure } = useTrafficalDecision({ defaults: { ... }, trackExposure: false });
|
|
502
|
+
*
|
|
503
|
+
* // New way with manual exposure
|
|
504
|
+
* const { params, trackExposure } = useTraffical({ defaults: { ... }, tracking: "decision" });
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
export function useTrafficalDecision<T extends Record<string, ParameterValue>>(
|
|
508
|
+
options: UseTrafficalDecisionOptions<T>
|
|
509
|
+
): UseTrafficalDecisionResult<T> {
|
|
510
|
+
// Show deprecation warning in development only
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
if (process.env.NODE_ENV === "development") {
|
|
513
|
+
console.warn(
|
|
514
|
+
"[Traffical] useTrafficalDecision is deprecated. Use useTraffical() instead."
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}, []);
|
|
518
|
+
|
|
519
|
+
// Map old trackExposure option to new tracking mode
|
|
520
|
+
const tracking = options.trackExposure === false ? "decision" : "full";
|
|
521
|
+
return useTraffical({ defaults: options.defaults, context: options.context, tracking });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Hook to track user events.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```tsx
|
|
529
|
+
* const track = useTrafficalTrack();
|
|
530
|
+
*
|
|
531
|
+
* const handlePurchase = (amount: number) => {
|
|
532
|
+
* track('purchase', { value: amount, orderId: 'ord_123' });
|
|
533
|
+
* };
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
export function useTrafficalTrack() {
|
|
537
|
+
const { client, getUnitKey } = useTrafficalContext();
|
|
538
|
+
|
|
539
|
+
const track = useCallback(
|
|
540
|
+
(
|
|
541
|
+
event: string,
|
|
542
|
+
properties?: Record<string, unknown>,
|
|
543
|
+
options?: { decisionId?: string }
|
|
544
|
+
) => {
|
|
545
|
+
if (!client) {
|
|
546
|
+
console.warn("[Traffical] Client not initialized, cannot track event");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
client.track(event, properties, {
|
|
551
|
+
decisionId: options?.decisionId,
|
|
552
|
+
unitKey: getUnitKey(),
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
[client, getUnitKey]
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
return track;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @deprecated Use useTrafficalTrack() instead.
|
|
563
|
+
*
|
|
564
|
+
* Hook to track a reward.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```tsx
|
|
568
|
+
* const trackReward = useTrafficalReward();
|
|
569
|
+
*
|
|
570
|
+
* const handlePurchase = (amount: number) => {
|
|
571
|
+
* trackReward({
|
|
572
|
+
* decisionId: decision.decisionId,
|
|
573
|
+
* reward: amount,
|
|
574
|
+
* rewardType: "revenue",
|
|
575
|
+
* });
|
|
576
|
+
* };
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
export function useTrafficalReward() {
|
|
580
|
+
const { client, getUnitKey, getContext } = useTrafficalContext();
|
|
581
|
+
|
|
582
|
+
const trackReward = useCallback(
|
|
583
|
+
(options: {
|
|
584
|
+
decisionId: string;
|
|
585
|
+
reward: number;
|
|
586
|
+
rewardType?: string;
|
|
587
|
+
rewards?: Record<string, number>;
|
|
588
|
+
}) => {
|
|
589
|
+
if (!client) {
|
|
590
|
+
console.warn("[Traffical] Client not initialized, cannot track reward");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Map old API to new track() API
|
|
595
|
+
client.track(options.rewardType || "reward", {
|
|
596
|
+
value: options.reward,
|
|
597
|
+
...(options.rewards ? { rewards: options.rewards } : {}),
|
|
598
|
+
}, {
|
|
599
|
+
decisionId: options.decisionId,
|
|
600
|
+
unitKey: getUnitKey(),
|
|
601
|
+
});
|
|
602
|
+
},
|
|
603
|
+
[client, getUnitKey, getContext]
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
return trackReward;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Hook to access a registered plugin by name.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```tsx
|
|
614
|
+
* import { createDOMBindingPlugin, DOMBindingPlugin } from '@traffical/js-client';
|
|
615
|
+
*
|
|
616
|
+
* // In your provider config:
|
|
617
|
+
* plugins: [createDOMBindingPlugin()]
|
|
618
|
+
*
|
|
619
|
+
* // In a component:
|
|
620
|
+
* const domPlugin = useTrafficalPlugin<DOMBindingPlugin>('dom-binding');
|
|
621
|
+
*
|
|
622
|
+
* // Re-apply bindings after dynamic content changes
|
|
623
|
+
* useEffect(() => {
|
|
624
|
+
* domPlugin?.applyBindings();
|
|
625
|
+
* }, [contentLoaded, domPlugin]);
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
export function useTrafficalPlugin<T extends TrafficalPlugin = TrafficalPlugin>(
|
|
629
|
+
name: string
|
|
630
|
+
): T | undefined {
|
|
631
|
+
const { client, ready } = useTrafficalContext();
|
|
632
|
+
|
|
633
|
+
if (!client || !ready) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return client.getPlugin(name) as T | undefined;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Hook to access the Traffical client directly.
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* ```tsx
|
|
645
|
+
* const { client, ready } = useTrafficalClient();
|
|
646
|
+
*
|
|
647
|
+
* if (ready && client) {
|
|
648
|
+
* const version = client.getConfigVersion();
|
|
649
|
+
* const stableId = client.getStableId();
|
|
650
|
+
* }
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
export function useTrafficalClient() {
|
|
654
|
+
const { client, ready, error } = useTrafficalContext();
|
|
655
|
+
return { client, ready, error };
|
|
656
|
+
}
|