@traffical/react 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,32 +1,30 @@
1
1
  {
2
2
  "name": "@traffical/react",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Traffical SDK for React - Provider and hooks for parameter resolution",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
7
- "module": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
12
  "bun": "./src/index.ts",
13
- "import": "./src/index.ts"
13
+ "default": "./dist/index.js"
14
14
  }
15
15
  },
16
16
  "files": [
17
- "dist",
18
- "src"
17
+ "dist"
19
18
  ],
20
19
  "scripts": {
21
20
  "build": "tsc",
22
21
  "dev": "tsc --watch",
23
22
  "test": "echo 'No tests yet'",
24
- "typecheck": "tsc --noEmit",
25
- "release": "bun scripts/release.ts"
23
+ "typecheck": "tsc --noEmit"
26
24
  },
27
25
  "dependencies": {
28
- "@traffical/core": "workspace:^",
29
- "@traffical/js-client": "workspace:^"
26
+ "@traffical/core": "^0.1.4",
27
+ "@traffical/js-client": "^0.1.4"
30
28
  },
31
29
  "devDependencies": {
32
30
  "@types/bun": "latest",
package/src/context.ts DELETED
@@ -1,144 +0,0 @@
1
- /**
2
- * Traffical React Context
3
- *
4
- * Provides the Traffical client instance to React components.
5
- * Uses the browser-optimized JS Client for full feature support.
6
- */
7
-
8
- import { createContext, useContext } from "react";
9
- import type { TrafficalClient, TrafficalPlugin } from "@traffical/js-client";
10
- import type { ConfigBundle, Context } from "@traffical/core";
11
-
12
- /**
13
- * Configuration for the Traffical provider.
14
- */
15
- export interface TrafficalProviderConfig {
16
- // ==========================================================================
17
- // Required
18
- // ==========================================================================
19
-
20
- /** Organization ID */
21
- orgId: string;
22
- /** Project ID */
23
- projectId: string;
24
- /** Environment (e.g., "production", "staging") */
25
- env: string;
26
- /** API key for authentication */
27
- apiKey: string;
28
-
29
- // ==========================================================================
30
- // Optional - Connection
31
- // ==========================================================================
32
-
33
- /** Base URL for the control plane API (optional) */
34
- baseUrl?: string;
35
- /** Local config bundle for offline fallback */
36
- localConfig?: ConfigBundle;
37
- /** Refresh interval in milliseconds (default: 60000) */
38
- refreshIntervalMs?: number;
39
-
40
- // ==========================================================================
41
- // Optional - Identity
42
- // ==========================================================================
43
-
44
- /**
45
- * Function to get the unit key value.
46
- * If not provided, the SDK will use automatic stable ID generation.
47
- */
48
- unitKeyFn?: () => string;
49
- /** Function to get additional context (optional) */
50
- contextFn?: () => Context;
51
-
52
- // ==========================================================================
53
- // Optional - Decision Tracking
54
- // ==========================================================================
55
-
56
- /**
57
- * Whether to automatically track decision events (default: true).
58
- * When enabled, every call to decide() automatically sends a DecisionEvent
59
- * to the control plane, enabling intent-to-treat analysis.
60
- */
61
- trackDecisions?: boolean;
62
- /**
63
- * Decision deduplication TTL in milliseconds (default: 1 hour).
64
- * Same user+assignment combination won't be tracked again within this window.
65
- */
66
- decisionDeduplicationTtlMs?: number;
67
-
68
- // ==========================================================================
69
- // Optional - Exposure Tracking
70
- // ==========================================================================
71
-
72
- /**
73
- * Exposure deduplication session TTL in milliseconds (default: 30 minutes).
74
- * Same user seeing same variant won't trigger multiple exposure events.
75
- */
76
- exposureSessionTtlMs?: number;
77
-
78
- // ==========================================================================
79
- // Optional - Plugins
80
- // ==========================================================================
81
-
82
- /**
83
- * Plugins to register with the client.
84
- * The DecisionTrackingPlugin is included by default unless trackDecisions is false.
85
- */
86
- plugins?: TrafficalPlugin[];
87
-
88
- // ==========================================================================
89
- // Optional - Event Batching
90
- // ==========================================================================
91
-
92
- /** Max events before auto-flush (default: 10) */
93
- eventBatchSize?: number;
94
- /** Auto-flush interval in ms (default: 30000) */
95
- eventFlushIntervalMs?: number;
96
-
97
- // ==========================================================================
98
- // Optional - SSR
99
- // ==========================================================================
100
-
101
- /** Initial params from SSR (optional) */
102
- initialParams?: Record<string, unknown>;
103
- }
104
-
105
- /**
106
- * Internal context value.
107
- */
108
- export interface TrafficalContextValue {
109
- /** The Traffical client instance */
110
- client: TrafficalClient | null;
111
- /** Whether the client is ready (config loaded) */
112
- ready: boolean;
113
- /** Any initialization error */
114
- error: Error | null;
115
- /** Function to get the unit key */
116
- getUnitKey: () => string;
117
- /** Function to get the full context */
118
- getContext: () => Context;
119
- /** Initial params from SSR */
120
- initialParams?: Record<string, unknown>;
121
- /** Local config bundle for synchronous resolution during initial render */
122
- localConfig?: ConfigBundle;
123
- }
124
-
125
- /**
126
- * React context for Traffical.
127
- */
128
- export const TrafficalContext = createContext<TrafficalContextValue | null>(
129
- null
130
- );
131
-
132
- /**
133
- * Hook to access the Traffical context.
134
- * Throws if used outside of TrafficalProvider.
135
- */
136
- export function useTrafficalContext(): TrafficalContextValue {
137
- const context = useContext(TrafficalContext);
138
- if (!context) {
139
- throw new Error(
140
- "useTrafficalContext must be used within a TrafficalProvider"
141
- );
142
- }
143
- return context;
144
- }
package/src/hooks.ts DELETED
@@ -1,656 +0,0 @@
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
- }
package/src/index.ts DELETED
@@ -1,93 +0,0 @@
1
- /**
2
- * @traffical/react
3
- *
4
- * Traffical SDK for React applications.
5
- * Provides Provider and hooks for parameter resolution and decision tracking.
6
- *
7
- * Features:
8
- * - Browser-optimized with sendBeacon, localStorage persistence
9
- * - Automatic stable ID for anonymous users
10
- * - Plugin system support (DecisionTrackingPlugin enabled by default)
11
- * - Decision and exposure deduplication
12
- *
13
- * @example
14
- * ```tsx
15
- * import { TrafficalProvider, useTraffical } from '@traffical/react';
16
- *
17
- * function App() {
18
- * return (
19
- * <TrafficalProvider
20
- * config={{
21
- * orgId: 'org_123',
22
- * projectId: 'proj_456',
23
- * env: 'production',
24
- * apiKey: 'pk_...',
25
- * }}
26
- * >
27
- * <MyComponent />
28
- * </TrafficalProvider>
29
- * );
30
- * }
31
- *
32
- * function MyComponent() {
33
- * const { params, ready } = useTraffical({
34
- * defaults: { 'ui.hero.title': 'Welcome' },
35
- * });
36
- *
37
- * if (!ready) return <div>Loading...</div>;
38
- * return <h1>{params['ui.hero.title']}</h1>;
39
- * }
40
- * ```
41
- */
42
-
43
- // Re-export everything from core
44
- export * from "@traffical/core";
45
-
46
- // Re-export client and utilities from JS Client
47
- export {
48
- TrafficalClient,
49
- createTrafficalClient,
50
- createTrafficalClientSync,
51
- type TrafficalClientOptions,
52
- } from "@traffical/js-client";
53
-
54
- // Re-export plugin utilities from JS Client
55
- export {
56
- type TrafficalPlugin,
57
- type PluginOptions,
58
- createDOMBindingPlugin,
59
- type DOMBindingPlugin,
60
- type DOMBindingPluginOptions,
61
- } from "@traffical/js-client";
62
-
63
- // Export React-specific components and hooks
64
- export { TrafficalProvider, type TrafficalProviderProps } from "./provider.js";
65
-
66
- export {
67
- TrafficalContext,
68
- useTrafficalContext,
69
- type TrafficalProviderConfig,
70
- type TrafficalContextValue,
71
- } from "./context.js";
72
-
73
- export {
74
- // Primary hook
75
- useTraffical,
76
- type UseTrafficalOptions,
77
- type UseTrafficalResult,
78
- type BoundTrackOptions,
79
- // Track hook
80
- useTrafficalTrack,
81
- // Other hooks
82
- useTrafficalPlugin,
83
- useTrafficalClient,
84
- // Deprecated (kept for backward compatibility)
85
- type BoundTrackRewardOptions,
86
- useTrafficalReward,
87
- useTrafficalParams,
88
- useTrafficalDecision,
89
- type UseTrafficalParamsOptions,
90
- type UseTrafficalParamsResult,
91
- type UseTrafficalDecisionOptions,
92
- type UseTrafficalDecisionResult,
93
- } from "./hooks.js";
package/src/provider.tsx DELETED
@@ -1,194 +0,0 @@
1
- /**
2
- * Traffical React Provider
3
- *
4
- * Initializes the Traffical client (browser-optimized) and provides it to child components.
5
- * Supports plugins, automatic stable ID, and decision tracking out of the box.
6
- */
7
-
8
- import React, {
9
- useEffect,
10
- useState,
11
- useCallback,
12
- useMemo,
13
- useRef,
14
- type ReactNode,
15
- } from "react";
16
- import {
17
- TrafficalClient,
18
- createTrafficalClientSync,
19
- } from "@traffical/js-client";
20
- import type { Context } from "@traffical/core";
21
- import {
22
- TrafficalContext,
23
- type TrafficalProviderConfig,
24
- type TrafficalContextValue,
25
- } from "./context.js";
26
-
27
- /**
28
- * Props for the TrafficalProvider component.
29
- */
30
- export interface TrafficalProviderProps {
31
- /** Configuration for the Traffical client */
32
- config: TrafficalProviderConfig;
33
- /** Child components */
34
- children: ReactNode;
35
- }
36
-
37
- /**
38
- * TrafficalProvider - initializes and provides the Traffical client to React components.
39
- *
40
- * Features:
41
- * - Browser-optimized with sendBeacon, localStorage persistence
42
- * - Automatic stable ID for anonymous users (unless unitKeyFn provided)
43
- * - Plugin system support (DecisionTrackingPlugin enabled by default)
44
- * - Decision and exposure deduplication
45
- *
46
- * @example
47
- * ```tsx
48
- * <TrafficalProvider
49
- * config={{
50
- * orgId: "org_123",
51
- * projectId: "proj_456",
52
- * env: "production",
53
- * apiKey: "pk_...",
54
- * // Optional: provide unitKeyFn for logged-in users
55
- * unitKeyFn: () => getUserId(),
56
- * // Optional: add context
57
- * contextFn: () => ({ locale: "en-US" }),
58
- * // Optional: add custom plugins
59
- * plugins: [createDOMBindingPlugin()],
60
- * }}
61
- * >
62
- * <App />
63
- * </TrafficalProvider>
64
- * ```
65
- */
66
- export function TrafficalProvider({
67
- config,
68
- children,
69
- }: TrafficalProviderProps): React.ReactElement {
70
- const [client, setClient] = useState<TrafficalClient | null>(null);
71
- const [ready, setReady] = useState(false);
72
- const [error, setError] = useState<Error | null>(null);
73
-
74
- // Keep a ref to the client for cleanup
75
- const clientRef = useRef<TrafficalClient | null>(null);
76
-
77
- // Memoize the unit key function
78
- // If no unitKeyFn is provided, use the client's stable ID
79
- const getUnitKey = useCallback(() => {
80
- if (config.unitKeyFn) {
81
- return config.unitKeyFn();
82
- }
83
- // Fall back to the client's auto-generated stable ID
84
- return clientRef.current?.getStableId() ?? "";
85
- }, [config.unitKeyFn]);
86
-
87
- const getContext = useCallback((): Context => {
88
- const unitKey = getUnitKey();
89
- const additionalContext = config.contextFn?.() ?? {};
90
-
91
- // The unit key field name comes from the bundle's hashing config
92
- // For now, we use common conventions and let the SDK handle it
93
- return {
94
- ...additionalContext,
95
- // Include common unit key field names
96
- userId: unitKey,
97
- deviceId: unitKey,
98
- anonymousId: unitKey,
99
- };
100
- }, [getUnitKey, config.contextFn]);
101
-
102
- // Initialize client on mount
103
- useEffect(() => {
104
- let mounted = true;
105
-
106
- const initClient = async () => {
107
- try {
108
- // Create client synchronously so it's available immediately
109
- const newClient = createTrafficalClientSync({
110
- orgId: config.orgId,
111
- projectId: config.projectId,
112
- env: config.env,
113
- apiKey: config.apiKey,
114
- baseUrl: config.baseUrl,
115
- localConfig: config.localConfig,
116
- refreshIntervalMs: config.refreshIntervalMs,
117
- trackDecisions: config.trackDecisions,
118
- decisionDeduplicationTtlMs: config.decisionDeduplicationTtlMs,
119
- exposureSessionTtlMs: config.exposureSessionTtlMs,
120
- eventBatchSize: config.eventBatchSize,
121
- eventFlushIntervalMs: config.eventFlushIntervalMs,
122
- plugins: config.plugins,
123
- });
124
-
125
- clientRef.current = newClient;
126
-
127
- if (mounted) {
128
- setClient(newClient);
129
- // If localConfig was provided, mark as ready immediately
130
- if (config.localConfig) {
131
- setReady(true);
132
- }
133
- }
134
-
135
- // Initialize asynchronously (fetches/refreshes config bundle)
136
- await newClient.initialize();
137
-
138
- if (mounted) {
139
- setReady(true);
140
- }
141
- } catch (err) {
142
- if (mounted) {
143
- setError(
144
- err instanceof Error ? err : new Error(String(err))
145
- );
146
- // Still mark as ready - we'll use defaults
147
- setReady(true);
148
- }
149
- }
150
- };
151
-
152
- initClient();
153
-
154
- return () => {
155
- mounted = false;
156
- clientRef.current?.destroy();
157
- clientRef.current = null;
158
- };
159
- }, [
160
- config.orgId,
161
- config.projectId,
162
- config.env,
163
- config.apiKey,
164
- config.baseUrl,
165
- config.localConfig,
166
- config.refreshIntervalMs,
167
- config.trackDecisions,
168
- config.decisionDeduplicationTtlMs,
169
- config.exposureSessionTtlMs,
170
- config.eventBatchSize,
171
- config.eventFlushIntervalMs,
172
- config.plugins,
173
- ]);
174
-
175
- // Memoize context value
176
- const contextValue = useMemo<TrafficalContextValue>(
177
- () => ({
178
- client,
179
- ready,
180
- error,
181
- getUnitKey,
182
- getContext,
183
- initialParams: config.initialParams,
184
- localConfig: config.localConfig,
185
- }),
186
- [client, ready, error, getUnitKey, getContext, config.initialParams, config.localConfig]
187
- );
188
-
189
- return (
190
- <TrafficalContext.Provider value={contextValue}>
191
- {children}
192
- </TrafficalContext.Provider>
193
- );
194
- }