@traffical/js-client 0.16.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/client.d.ts +39 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +90 -59
- package/dist/client.js.map +1 -1
- package/dist/exposure-dedup.d.ts +1 -1
- package/dist/exposure-dedup.js +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/debug.d.ts +24 -0
- package/dist/plugins/debug.d.ts.map +1 -1
- package/dist/plugins/debug.js +61 -2
- package/dist/plugins/debug.js.map +1 -1
- package/dist/traffical.min.js +2 -2
- package/dist/traffical.min.js.map +4 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ const traffical = await createTrafficalClient({
|
|
|
29
29
|
orgId: 'org_xxx',
|
|
30
30
|
projectId: 'proj_xxx',
|
|
31
31
|
env: 'production',
|
|
32
|
-
apiKey: '
|
|
32
|
+
apiKey: 'traffical_pk_…',
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
// Get parameters
|
|
@@ -67,7 +67,7 @@ traffical.track('checkout_complete', { value: 1 }, { decisionId: decision.decisi
|
|
|
67
67
|
orgId: 'org_xxx',
|
|
68
68
|
projectId: 'proj_xxx',
|
|
69
69
|
env: 'production',
|
|
70
|
-
apiKey: '
|
|
70
|
+
apiKey: 'traffical_pk_…',
|
|
71
71
|
}).then(function(traffical) {
|
|
72
72
|
var params = traffical.getParams({
|
|
73
73
|
context: { userId: 'user_123' },
|
|
@@ -167,7 +167,7 @@ The Traffical DevTools debug plugin uses these methods to let developers force p
|
|
|
167
167
|
## Features
|
|
168
168
|
|
|
169
169
|
- **Error Boundary** - SDK errors never crash your app
|
|
170
|
-
- **Exposure Deduplication** - Same user/
|
|
170
|
+
- **Exposure Deduplication** - Same user/allocation = 1 exposure event per session
|
|
171
171
|
- **Smart Batching** - Events batched and flushed efficiently
|
|
172
172
|
- **Beacon on Unload** - Events sent reliably on page close
|
|
173
173
|
- **Auto Stable ID** - Anonymous user identification via localStorage/cookie
|
|
@@ -187,7 +187,7 @@ const traffical = await createTrafficalClient({
|
|
|
187
187
|
orgId: 'org_xxx',
|
|
188
188
|
projectId: 'proj_xxx',
|
|
189
189
|
env: 'production',
|
|
190
|
-
apiKey: '
|
|
190
|
+
apiKey: 'traffical_pk_…',
|
|
191
191
|
plugins: [
|
|
192
192
|
createDOMBindingPlugin({
|
|
193
193
|
observeMutations: true, // Watch for DOM changes (SPA support)
|
|
@@ -218,7 +218,7 @@ The plugin:
|
|
|
218
218
|
|
|
219
219
|
### Redirect Plugin
|
|
220
220
|
|
|
221
|
-
Run URL split tests (redirect
|
|
221
|
+
Run URL split tests (redirect policies) where visitors are redirected to different landing page allocations. The redirect plugin automatically triggers a decision on init, performs the redirect, and sets an attribution cookie. The attribution plugin ensures conversions on the destination page are attributed back to the policy.
|
|
222
222
|
|
|
223
223
|
```typescript
|
|
224
224
|
import {
|
|
@@ -231,7 +231,7 @@ const traffical = await createTrafficalClient({
|
|
|
231
231
|
orgId: 'org_xxx',
|
|
232
232
|
projectId: 'proj_xxx',
|
|
233
233
|
env: 'production',
|
|
234
|
-
apiKey: '
|
|
234
|
+
apiKey: 'traffical_pk_…',
|
|
235
235
|
plugins: [
|
|
236
236
|
createRedirectPlugin(),
|
|
237
237
|
createRedirectAttributionPlugin(),
|
|
@@ -242,7 +242,7 @@ const traffical = await createTrafficalClient({
|
|
|
242
242
|
// On entry pages, it redirects. On other pages, it's a no-op.
|
|
243
243
|
|
|
244
244
|
// Track goals as usual — the attribution plugin injects
|
|
245
|
-
// redirect
|
|
245
|
+
// redirect policy metadata into every track() call.
|
|
246
246
|
traffical.track('add_to_cart', { value: 29.99 });
|
|
247
247
|
```
|
|
248
248
|
|
|
@@ -281,9 +281,9 @@ Track goal events from a separate GTM tag (e.g., triggered on "Add to Cart" clic
|
|
|
281
281
|
|
|
282
282
|
1. **Init** — The redirect plugin's `onInitialize` hook receives the client and calls `decide()` automatically.
|
|
283
283
|
|
|
284
|
-
2. **Entry page** — `onBeforeDecision` injects `url.pathname` into the context. The policy condition (e.g., `url.pathname startsWith /products/pillow`) matches, `redirect.url` resolves to the
|
|
284
|
+
2. **Entry page** — `onBeforeDecision` injects `url.pathname` into the context. The policy condition (e.g., `url.pathname startsWith /products/pillow`) matches, `redirect.url` resolves to the allocation's URL. `onDecision` writes an attribution cookie (`traffical_rdr`) and calls `window.location.replace()`.
|
|
285
285
|
|
|
286
|
-
3. **
|
|
286
|
+
3. **Destination page** — The SDK loads again, `decide()` runs, but the policy condition doesn't match the new URL, so `redirect.url` stays empty and no redirect happens. The redirect-attribution plugin reads the `traffical_rdr` cookie and injects the policy metadata into every `track()` call.
|
|
287
287
|
|
|
288
288
|
#### Configuration
|
|
289
289
|
|
package/dist/client.d.ts
CHANGED
|
@@ -9,12 +9,26 @@
|
|
|
9
9
|
* - Plugin system (P2)
|
|
10
10
|
* - Auto stable ID for anonymous users
|
|
11
11
|
*/
|
|
12
|
-
import { type ConfigBundle, type Context, type DecisionResult, type ParameterValue, type DecideOptions, type GetParamsOptions, type TrackEventOptions, type AssignmentLogger, type TrackableEventLogger, type TrackEventMap, type OnSchemaWarnings } from "@traffical/core";
|
|
12
|
+
import { type ConfigBundle, type Context, type DecisionResult, type ParameterValue, type DecideOptions, type GetParamsOptions, type TrackEventOptions, type AssignmentLogger, type TrackableEventLogger, type TrackEventMap, type OnSchemaWarnings, type OnResolutionError, type SdkDiagnostics } from "@traffical/core";
|
|
13
13
|
import { type ErrorBoundaryOptions } from "./error-boundary.js";
|
|
14
14
|
import { type StorageProvider } from "./storage.js";
|
|
15
15
|
import { type TrafficalPlugin } from "./plugins/index.js";
|
|
16
16
|
import { type LifecycleProvider } from "./lifecycle.js";
|
|
17
17
|
export interface TrafficalClientOptions {
|
|
18
|
+
/**
|
|
19
|
+
* What a resolution failure does (default: `"default"` — return the caller's
|
|
20
|
+
* defaults and stamp `metadata.reason: "error"`). `"throw"` rethrows, which
|
|
21
|
+
* is useful in CI and staging.
|
|
22
|
+
*
|
|
23
|
+
* Governs **resolution only**: `assignmentLogger`, `eventLogger`, and
|
|
24
|
+
* `onError` are always contained in either mode.
|
|
25
|
+
*/
|
|
26
|
+
onResolutionError?: OnResolutionError;
|
|
27
|
+
/**
|
|
28
|
+
* Called for every contained error, deduplicated per `tag:name:message`.
|
|
29
|
+
* Errors it throws are swallowed.
|
|
30
|
+
*/
|
|
31
|
+
onError?: (tag: string, error: Error) => void;
|
|
18
32
|
/** Organization ID */
|
|
19
33
|
orgId: string;
|
|
20
34
|
/** Project ID */
|
|
@@ -58,6 +72,10 @@ export interface TrafficalClientOptions {
|
|
|
58
72
|
*/
|
|
59
73
|
resolveTimeoutMs?: number;
|
|
60
74
|
/** Error boundary options */
|
|
75
|
+
/**
|
|
76
|
+
* @deprecated Use the top-level `onError` (and `onResolutionError`) instead.
|
|
77
|
+
* `errorBoundary.onError` is still honored.
|
|
78
|
+
*/
|
|
61
79
|
errorBoundary?: ErrorBoundaryOptions;
|
|
62
80
|
/** Event batching options */
|
|
63
81
|
/**
|
|
@@ -133,7 +151,7 @@ export interface TrafficalClientOptions {
|
|
|
133
151
|
disableCloudEvents?: boolean;
|
|
134
152
|
/**
|
|
135
153
|
* When true, assignment logger calls are deduplicated per session
|
|
136
|
-
* (same unit+policy+
|
|
154
|
+
* (same unit+policy+allocation won't fire again). Default: true.
|
|
137
155
|
*/
|
|
138
156
|
deduplicateAssignmentLogger?: boolean;
|
|
139
157
|
/**
|
|
@@ -160,7 +178,7 @@ export interface TrafficalClientOptions {
|
|
|
160
178
|
export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventMap> {
|
|
161
179
|
private readonly _options;
|
|
162
180
|
private _state;
|
|
163
|
-
private readonly
|
|
181
|
+
private readonly _errorPolicy;
|
|
164
182
|
private readonly _storage;
|
|
165
183
|
private readonly _eventLogger;
|
|
166
184
|
private readonly _exposureDedup;
|
|
@@ -237,6 +255,12 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
|
|
|
237
255
|
* parameter is unknown / the bundle is not yet loaded.
|
|
238
256
|
*/
|
|
239
257
|
getParameterLayerId(key: string): string | null;
|
|
258
|
+
/**
|
|
259
|
+
* Counters for degradation that is otherwise invisible: contained resolution
|
|
260
|
+
* errors, dropped assignment/event log rows, rejected bundles, and the most
|
|
261
|
+
* recent contained error. Monotonic for the life of the client.
|
|
262
|
+
*/
|
|
263
|
+
getDiagnostics(): SdkDiagnostics;
|
|
240
264
|
/**
|
|
241
265
|
* Resolves parameters with defaults as fallback.
|
|
242
266
|
*/
|
|
@@ -251,7 +275,7 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
|
|
|
251
275
|
decide<T extends Record<string, ParameterValue>>(options: DecideOptions<T>): DecisionResult;
|
|
252
276
|
/**
|
|
253
277
|
* Tracks an exposure event.
|
|
254
|
-
* Automatically deduplicates
|
|
278
|
+
* Automatically deduplicates exposure events for the same user/allocation.
|
|
255
279
|
*
|
|
256
280
|
* Skips layers marked `attributionOnly` — those were resolved for
|
|
257
281
|
* attribution/assignment purposes only (no parameters were requested
|
|
@@ -342,6 +366,16 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
|
|
|
342
366
|
* Traffical edge batcher (unless cloud events are disabled).
|
|
343
367
|
*/
|
|
344
368
|
private _dispatchEvent;
|
|
369
|
+
/**
|
|
370
|
+
* Emits assignment rows to the customer's sink.
|
|
371
|
+
*
|
|
372
|
+
* MUST be called OUTSIDE the resolution boundary, and each invocation of the
|
|
373
|
+
* customer callback is guarded individually. Calling it inside `resolve()`
|
|
374
|
+
* made a throwing sink discard the *decision*: the unit was bucketed into
|
|
375
|
+
* treatment and the caller received control, with `getParams()` — which
|
|
376
|
+
* emits nothing — still returning treatment for the same unit. Because a
|
|
377
|
+
* bounded queue throws when full, that biased assignment with traffic load.
|
|
378
|
+
*/
|
|
345
379
|
private _emitAssignmentLogEntries;
|
|
346
380
|
private _applyOverridesToResult;
|
|
347
381
|
private _getEffectiveBundle;
|
|
@@ -385,7 +419,7 @@ export declare class TrafficalClient<TEvents extends TrackEventMap = TrackEventM
|
|
|
385
419
|
* - "cumulative": Collects layers from ALL cached decisions for this unit,
|
|
386
420
|
* deduplicated by layerId:policyId (last-write-wins). This ensures cross-page
|
|
387
421
|
* funnels (e.g., catalog -> PDP -> checkout) attribute correctly to all
|
|
388
|
-
*
|
|
422
|
+
* policies the user is exposed to. For per-entity dynamic allocation
|
|
389
423
|
* policies, only the most recent allocation is kept to avoid attributing
|
|
390
424
|
* rewards to allocations from other entities (e.g., different products).
|
|
391
425
|
*
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,OAAO,EACZ,KAAK,cAAc,EAEnB,KAAK,cAAc,EAKnB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAItB,KAAK,gBAAgB,EAGrB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* - Plugin system (P2)
|
|
10
10
|
* - Auto stable ID for anonymous users
|
|
11
11
|
*/
|
|
12
|
-
import { resolveParameters, decide as coreDecide, getUnitKeyValue, getUnitKeyField as coreGetUnitKeyField, getParameterLayerId as coreGetParameterLayerId, generateExposureId, generateTrackEventId, generateDecisionId, generateAssignmentId, } from "@traffical/core";
|
|
12
|
+
import { ErrorPolicy, validateConfigBundle, resolveParameters, decide as coreDecide, getUnitKeyValue, getUnitKeyField as coreGetUnitKeyField, getParameterLayerId as coreGetParameterLayerId, generateExposureId, generateTrackEventId, generateDecisionId, generateAssignmentId, } from "@traffical/core";
|
|
13
13
|
import { DecisionClient, createEdgeDecideRequest, } from "@traffical/core-io";
|
|
14
|
-
import { ErrorBoundary } from "./error-boundary.js";
|
|
15
14
|
import { EventLogger } from "./event-logger.js";
|
|
16
15
|
import { ExposureDeduplicator } from "./exposure-dedup.js";
|
|
17
16
|
import { StableIdProvider } from "./stable-id.js";
|
|
@@ -49,33 +48,6 @@ function jitteredInterval(intervalMs) {
|
|
|
49
48
|
const delta = intervalMs * REFRESH_JITTER_RATIO;
|
|
50
49
|
return intervalMs + (Math.random() * 2 - 1) * delta;
|
|
51
50
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Structural guard for a fetched config bundle. A 200 response can still carry a
|
|
54
|
-
* malformed body (truncated CDN write, partial deploy); serving it would corrupt
|
|
55
|
-
* every bucket assignment. Requires the hashing config the resolver depends on
|
|
56
|
-
* (`unitKey` non-empty, `bucketCount` an integer >= 1) plus the top-level
|
|
57
|
-
* parameters/layers arrays.
|
|
58
|
-
*/
|
|
59
|
-
function isValidConfigBundle(bundle) {
|
|
60
|
-
if (!bundle || typeof bundle !== "object")
|
|
61
|
-
return false;
|
|
62
|
-
const b = bundle;
|
|
63
|
-
if (!Array.isArray(b.parameters))
|
|
64
|
-
return false;
|
|
65
|
-
if (!Array.isArray(b.layers))
|
|
66
|
-
return false;
|
|
67
|
-
const hashing = b.hashing;
|
|
68
|
-
if (!hashing || typeof hashing !== "object")
|
|
69
|
-
return false;
|
|
70
|
-
if (typeof hashing.unitKey !== "string" || hashing.unitKey.length === 0)
|
|
71
|
-
return false;
|
|
72
|
-
if (typeof hashing.bucketCount !== "number" ||
|
|
73
|
-
!Number.isInteger(hashing.bucketCount) ||
|
|
74
|
-
hashing.bucketCount < 1) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
51
|
// =============================================================================
|
|
80
52
|
// TrafficalClient Class
|
|
81
53
|
// =============================================================================
|
|
@@ -138,7 +110,11 @@ export class TrafficalClient {
|
|
|
138
110
|
};
|
|
139
111
|
this._decisionClient = new DecisionClient(decisionClientConfig);
|
|
140
112
|
// Initialize components
|
|
141
|
-
this.
|
|
113
|
+
this._errorPolicy = new ErrorPolicy({
|
|
114
|
+
onResolutionError: options.onResolutionError,
|
|
115
|
+
// `errorBoundary.onError` remains supported as the legacy spelling.
|
|
116
|
+
onError: options.onError ?? options.errorBoundary?.onError,
|
|
117
|
+
});
|
|
142
118
|
this._storage = options.storage ?? createStorageProvider();
|
|
143
119
|
this._lifecycleProvider = options.lifecycleProvider ?? createBrowserLifecycleProvider();
|
|
144
120
|
// Default dev-mode schema warnings handler
|
|
@@ -209,11 +185,20 @@ export class TrafficalClient {
|
|
|
209
185
|
this._plugins.register(plugin);
|
|
210
186
|
}
|
|
211
187
|
}
|
|
212
|
-
// Initialize with local config if provided
|
|
188
|
+
// Initialize with local config if provided.
|
|
189
|
+
// Validated on exactly the same terms as a fetched bundle: a hand-edited
|
|
190
|
+
// local bundle is no more trustworthy than one off the wire.
|
|
213
191
|
if (this._options.localConfig) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
192
|
+
const validation = validateConfigBundle(this._options.localConfig);
|
|
193
|
+
if (validation.ok) {
|
|
194
|
+
this._state.bundle = this._options.localConfig;
|
|
195
|
+
// Notify plugins about the local config
|
|
196
|
+
this._plugins.runConfigUpdate(this._options.localConfig);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this._errorPolicy.recordRejectedBundle("localConfig", validation.path, validation.reason);
|
|
200
|
+
this._options.localConfig = undefined;
|
|
201
|
+
}
|
|
217
202
|
}
|
|
218
203
|
// Register on global instance list so DevTools can discover ES-module SDKs
|
|
219
204
|
if (typeof window !== "undefined") {
|
|
@@ -229,7 +214,14 @@ export class TrafficalClient {
|
|
|
229
214
|
* Initializes the client by fetching the config bundle.
|
|
230
215
|
*/
|
|
231
216
|
async initialize() {
|
|
232
|
-
|
|
217
|
+
// Initialization is contained unconditionally, NOT under
|
|
218
|
+
// onResolutionError. `_readyResolve()` below must always run: rethrowing
|
|
219
|
+
// here would leave `waitForReady()` pending forever, and it would do so
|
|
220
|
+
// only in strict mode — the mode people enable in CI. Initialization
|
|
221
|
+
// strictness is a separate phase decision (cf. Eppo's
|
|
222
|
+
// `throwOnFailedInitialization`, also defaulting to false); resolution
|
|
223
|
+
// strictness still applies to decide()/getParams().
|
|
224
|
+
await this._errorPolicy.sideEffectAsync("initialize", "other", async () => {
|
|
233
225
|
if (this._options.evaluationMode === "server") {
|
|
234
226
|
await this._fetchServerResolve();
|
|
235
227
|
}
|
|
@@ -240,7 +232,7 @@ export class TrafficalClient {
|
|
|
240
232
|
this._state.isInitialized = true;
|
|
241
233
|
// Run plugin onInitialize hooks (pass client reference for autonomous plugins)
|
|
242
234
|
await this._plugins.runInitialize(this);
|
|
243
|
-
}
|
|
235
|
+
});
|
|
244
236
|
// Fail-open: resolve readiness even if the config load errored/degraded.
|
|
245
237
|
this._readyResolve();
|
|
246
238
|
}
|
|
@@ -332,7 +324,7 @@ export class TrafficalClient {
|
|
|
332
324
|
* Manually refreshes the config bundle.
|
|
333
325
|
*/
|
|
334
326
|
async refreshConfig() {
|
|
335
|
-
await this.
|
|
327
|
+
await this._errorPolicy.sideEffectAsync("refreshConfig", "other", async () => {
|
|
336
328
|
if (this._options.evaluationMode === "server") {
|
|
337
329
|
await this._fetchServerResolve();
|
|
338
330
|
}
|
|
@@ -362,9 +354,20 @@ export class TrafficalClient {
|
|
|
362
354
|
getParameterLayerId(key) {
|
|
363
355
|
return coreGetParameterLayerId(this._getEffectiveBundle(), key);
|
|
364
356
|
}
|
|
357
|
+
// ===========================================================================
|
|
358
|
+
// Parameter Resolution
|
|
359
|
+
// ===========================================================================
|
|
360
|
+
/**
|
|
361
|
+
* Counters for degradation that is otherwise invisible: contained resolution
|
|
362
|
+
* errors, dropped assignment/event log rows, rejected bundles, and the most
|
|
363
|
+
* recent contained error. Monotonic for the life of the client.
|
|
364
|
+
*/
|
|
365
|
+
getDiagnostics() {
|
|
366
|
+
return this._errorPolicy.getDiagnostics();
|
|
367
|
+
}
|
|
365
368
|
getParams(contextOrOptions, maybeDefaults) {
|
|
366
369
|
const { context: rawContext, defaults } = normalizeEvalArgs(contextOrOptions, maybeDefaults);
|
|
367
|
-
return this.
|
|
370
|
+
return this._errorPolicy.resolve("getParams", () => {
|
|
368
371
|
// Server mode: return from cached server response
|
|
369
372
|
if (this._options.evaluationMode === "server" && this._state.serverResponse) {
|
|
370
373
|
this._maybeResolveForContext(rawContext);
|
|
@@ -386,11 +389,11 @@ export class TrafficalClient {
|
|
|
386
389
|
// Apply parameter overrides (post-resolution, post-plugin)
|
|
387
390
|
this._applyOverridesToResult(params);
|
|
388
391
|
return params;
|
|
389
|
-
}, defaults);
|
|
392
|
+
}, () => ({ ...defaults }));
|
|
390
393
|
}
|
|
391
394
|
decide(contextOrOptions, maybeDefaults) {
|
|
392
395
|
const { context: rawContext, defaults } = normalizeEvalArgs(contextOrOptions, maybeDefaults);
|
|
393
|
-
|
|
396
|
+
const decision = this._errorPolicy.resolve("decide", () => {
|
|
394
397
|
// Server mode: return from cached server response
|
|
395
398
|
if (this._options.evaluationMode === "server" && this._state.serverResponse) {
|
|
396
399
|
this._maybeResolveForContext(rawContext);
|
|
@@ -415,7 +418,9 @@ export class TrafficalClient {
|
|
|
415
418
|
this._updateCumulativeAttribution(decision);
|
|
416
419
|
this._plugins.runDecision(decision);
|
|
417
420
|
this._applyOverridesToResult(decision.assignments);
|
|
418
|
-
|
|
421
|
+
decision.metadata.reason = resp.metadata.layers?.some((l) => l.policyId)
|
|
422
|
+
? "resolved"
|
|
423
|
+
: "default";
|
|
419
424
|
return decision;
|
|
420
425
|
}
|
|
421
426
|
const bundle = this._getEffectiveBundle();
|
|
@@ -433,36 +438,45 @@ export class TrafficalClient {
|
|
|
433
438
|
this._plugins.runDecision(decision);
|
|
434
439
|
// Apply parameter overrides (post-resolution, post-plugin)
|
|
435
440
|
this._applyOverridesToResult(decision.assignments);
|
|
436
|
-
|
|
441
|
+
decision.metadata.reason = !bundle
|
|
442
|
+
? "no-bundle"
|
|
443
|
+
: decision.metadata.layers.some((l) => l.policyId)
|
|
444
|
+
? "resolved"
|
|
445
|
+
: "default";
|
|
437
446
|
return decision;
|
|
438
|
-
}, {
|
|
447
|
+
}, () => ({
|
|
439
448
|
decisionId: generateDecisionId(),
|
|
440
|
-
assignments: defaults,
|
|
449
|
+
assignments: { ...defaults },
|
|
441
450
|
metadata: {
|
|
442
451
|
timestamp: new Date().toISOString(),
|
|
443
452
|
unitKeyValue: "",
|
|
444
453
|
layers: [],
|
|
454
|
+
reason: "error",
|
|
445
455
|
},
|
|
446
|
-
});
|
|
456
|
+
}));
|
|
457
|
+
// Assignment rows are emitted OUTSIDE the boundary. A throwing sink must
|
|
458
|
+
// cost a log row, never the decision the caller just received.
|
|
459
|
+
this._emitAssignmentLogEntries(decision, "decision");
|
|
460
|
+
return decision;
|
|
447
461
|
}
|
|
448
462
|
// ===========================================================================
|
|
449
463
|
// Event Tracking
|
|
450
464
|
// ===========================================================================
|
|
451
465
|
/**
|
|
452
466
|
* Tracks an exposure event.
|
|
453
|
-
* Automatically deduplicates
|
|
467
|
+
* Automatically deduplicates exposure events for the same user/allocation.
|
|
454
468
|
*
|
|
455
469
|
* Skips layers marked `attributionOnly` — those were resolved for
|
|
456
470
|
* attribution/assignment purposes only (no parameters were requested
|
|
457
471
|
* from that layer) and should not count as exposures.
|
|
458
472
|
*/
|
|
459
473
|
trackExposure(decision) {
|
|
460
|
-
|
|
474
|
+
// Outside the boundary, for the same reason as decide().
|
|
475
|
+
this._emitAssignmentLogEntries(decision, "exposure");
|
|
476
|
+
this._errorPolicy.resolve("trackExposure", () => {
|
|
461
477
|
const unitKey = decision.metadata.unitKeyValue;
|
|
462
478
|
if (!unitKey)
|
|
463
479
|
return;
|
|
464
|
-
// Emit to assignment logger (separate from cloud events)
|
|
465
|
-
this._emitAssignmentLogEntries(decision, "exposure");
|
|
466
480
|
// Config bundle version the SDK evaluated against — from the
|
|
467
481
|
// decision-time snapshot. The current version is only a fallback for
|
|
468
482
|
// decisions that predate the snapshot field.
|
|
@@ -508,7 +522,7 @@ export class TrafficalClient {
|
|
|
508
522
|
if (!this._plugins.runExposure(event))
|
|
509
523
|
return;
|
|
510
524
|
this._dispatchEvent(event);
|
|
511
|
-
}, undefined);
|
|
525
|
+
}, () => undefined);
|
|
512
526
|
}
|
|
513
527
|
/**
|
|
514
528
|
* Tracks a user event.
|
|
@@ -528,7 +542,7 @@ export class TrafficalClient {
|
|
|
528
542
|
* client.track('checkout_complete', { value: 1 }, { decisionId: decision.decisionId });
|
|
529
543
|
*/
|
|
530
544
|
track(eventName, properties, options) {
|
|
531
|
-
this.
|
|
545
|
+
this._errorPolicy.resolve("track", () => {
|
|
532
546
|
const unitKey = options?.unitKey ?? this._stableId.getId();
|
|
533
547
|
// Single numeric value: explicit options.value wins, else properties.value.
|
|
534
548
|
const value = typeof options?.value === "number"
|
|
@@ -562,13 +576,13 @@ export class TrafficalClient {
|
|
|
562
576
|
return;
|
|
563
577
|
}
|
|
564
578
|
this._dispatchEvent(event);
|
|
565
|
-
}, undefined);
|
|
579
|
+
}, () => undefined);
|
|
566
580
|
}
|
|
567
581
|
/**
|
|
568
582
|
* Flush pending events immediately.
|
|
569
583
|
*/
|
|
570
584
|
async flushEvents() {
|
|
571
|
-
await this.
|
|
585
|
+
await this._errorPolicy.sideEffectAsync("flushEvents", "other", async () => {
|
|
572
586
|
await this._eventLogger.flush();
|
|
573
587
|
});
|
|
574
588
|
}
|
|
@@ -723,6 +737,16 @@ export class TrafficalClient {
|
|
|
723
737
|
this._eventLogger.log(event);
|
|
724
738
|
}
|
|
725
739
|
}
|
|
740
|
+
/**
|
|
741
|
+
* Emits assignment rows to the customer's sink.
|
|
742
|
+
*
|
|
743
|
+
* MUST be called OUTSIDE the resolution boundary, and each invocation of the
|
|
744
|
+
* customer callback is guarded individually. Calling it inside `resolve()`
|
|
745
|
+
* made a throwing sink discard the *decision*: the unit was bucketed into
|
|
746
|
+
* treatment and the caller received control, with `getParams()` — which
|
|
747
|
+
* emits nothing — still returning treatment for the same unit. Because a
|
|
748
|
+
* bounded queue throws when full, that biased assignment with traffic load.
|
|
749
|
+
*/
|
|
726
750
|
_emitAssignmentLogEntries(decision, type) {
|
|
727
751
|
if (!this._assignmentLogger)
|
|
728
752
|
return;
|
|
@@ -741,11 +765,16 @@ export class TrafficalClient {
|
|
|
741
765
|
if (!isNew)
|
|
742
766
|
continue;
|
|
743
767
|
}
|
|
744
|
-
|
|
768
|
+
// Tier 1: the customer's sink must never change a caller's answer.
|
|
769
|
+
// Guarded HERE, narrowly, rather than relying on the surrounding
|
|
770
|
+
// resolution boundary — see the note on _emitAssignmentLogEntries.
|
|
771
|
+
const policyId = layer.policyId;
|
|
772
|
+
const allocationName = layer.allocationName;
|
|
773
|
+
this._errorPolicy.sideEffect("assignmentLogger", "assignmentLog", () => this._assignmentLogger({
|
|
745
774
|
unitKey,
|
|
746
|
-
policyId
|
|
775
|
+
policyId,
|
|
747
776
|
policyKey: layer.policyKey,
|
|
748
|
-
allocationName
|
|
777
|
+
allocationName,
|
|
749
778
|
allocationKey: layer.allocationKey,
|
|
750
779
|
timestamp: decision.metadata.timestamp,
|
|
751
780
|
layerId: layer.layerId,
|
|
@@ -764,7 +793,7 @@ export class TrafficalClient {
|
|
|
764
793
|
probability: layer.probability,
|
|
765
794
|
modelVersion: layer.modelVersion,
|
|
766
795
|
configVersion,
|
|
767
|
-
});
|
|
796
|
+
}));
|
|
768
797
|
}
|
|
769
798
|
}
|
|
770
799
|
_applyOverridesToResult(target) {
|
|
@@ -825,7 +854,9 @@ export class TrafficalClient {
|
|
|
825
854
|
// A 200 can still carry a malformed body. Discard it and keep the
|
|
826
855
|
// previous last-good bundle rather than corrupting bucket assignments or
|
|
827
856
|
// falling through to defaults when we already have a valid config.
|
|
828
|
-
|
|
857
|
+
const validation = validateConfigBundle(bundle);
|
|
858
|
+
if (!validation.ok) {
|
|
859
|
+
this._errorPolicy.recordRejectedBundle("fetchConfig", validation.path, validation.reason);
|
|
829
860
|
this._logMalformedBundleWarning();
|
|
830
861
|
return;
|
|
831
862
|
}
|
|
@@ -1034,7 +1065,7 @@ export class TrafficalClient {
|
|
|
1034
1065
|
* - "cumulative": Collects layers from ALL cached decisions for this unit,
|
|
1035
1066
|
* deduplicated by layerId:policyId (last-write-wins). This ensures cross-page
|
|
1036
1067
|
* funnels (e.g., catalog -> PDP -> checkout) attribute correctly to all
|
|
1037
|
-
*
|
|
1068
|
+
* policies the user is exposed to. For per-entity dynamic allocation
|
|
1038
1069
|
* policies, only the most recent allocation is kept to avoid attributing
|
|
1039
1070
|
* rewards to allocations from other entities (e.g., different products).
|
|
1040
1071
|
*
|