flowgrid-sdk 1.2.1 → 1.3.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 +224 -250
- package/dist/237.flowgrid.min.js +2 -0
- package/dist/237.flowgrid.min.js.map +1 -0
- package/dist/546.flowgrid.min.js +3 -0
- package/dist/546.flowgrid.min.js.LICENSE.txt +14 -0
- package/dist/546.flowgrid.min.js.map +1 -0
- package/dist/EXAMPLE.d.ts +1 -0
- package/dist/EXAMPLE.js +55 -0
- package/dist/EXAMPLE.js.map +1 -0
- package/dist/browser.d.ts +46 -103
- package/dist/browser.js +102 -0
- package/dist/browser.js.map +1 -0
- package/dist/flowgrid.min.js +1 -1
- package/dist/flowgrid.min.js.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/client/transport.d.ts +14 -4
- package/dist/lib/client/transport.js +432 -0
- package/dist/lib/client/transport.js.map +1 -0
- package/dist/lib/consent/index.d.ts +0 -5
- package/dist/lib/consent/index.js +32 -0
- package/dist/lib/consent/index.js.map +1 -0
- package/dist/lib/consent/manager.js +336 -0
- package/dist/lib/consent/manager.js.map +1 -0
- package/dist/lib/consent/types.d.ts +3 -20
- package/dist/lib/consent/types.js +10 -0
- package/dist/lib/consent/types.js.map +1 -0
- package/dist/lib/features/analytics/attribution.d.ts +6 -47
- package/dist/lib/features/analytics/attribution.js +181 -0
- package/dist/lib/features/analytics/attribution.js.map +1 -0
- package/dist/lib/features/analytics/events.d.ts +1 -28
- package/dist/lib/features/analytics/events.js +256 -0
- package/dist/lib/features/analytics/events.js.map +1 -0
- package/dist/lib/features/analytics/funnels.js +260 -0
- package/dist/lib/features/analytics/funnels.js.map +1 -0
- package/dist/lib/features/analytics/heatmaps.d.ts +12 -19
- package/dist/lib/features/analytics/heatmaps.js +258 -0
- package/dist/lib/features/analytics/heatmaps.js.map +1 -0
- package/dist/lib/features/analytics/identify.d.ts +1 -21
- package/dist/lib/features/analytics/identify.js +213 -0
- package/dist/lib/features/analytics/identify.js.map +1 -0
- package/dist/lib/features/analytics/index.d.ts +2 -0
- package/dist/lib/features/analytics/index.js +15 -0
- package/dist/lib/features/analytics/index.js.map +1 -0
- package/dist/lib/features/analytics/page-views.d.ts +13 -30
- package/dist/lib/features/analytics/page-views.js +268 -0
- package/dist/lib/features/analytics/page-views.js.map +1 -0
- package/dist/lib/features/analytics/performance.d.ts +18 -24
- package/dist/lib/features/analytics/performance.js +232 -0
- package/dist/lib/features/analytics/performance.js.map +1 -0
- package/dist/lib/features/analytics/replay.d.ts +170 -0
- package/dist/lib/features/analytics/replay.js +381 -0
- package/dist/lib/features/analytics/replay.js.map +1 -0
- package/dist/lib/features/analytics/retention.d.ts +1 -76
- package/dist/lib/features/analytics/retention.js +135 -0
- package/dist/lib/features/analytics/retention.js.map +1 -0
- package/dist/lib/features/analytics/sessions.d.ts +12 -37
- package/dist/lib/features/analytics/sessions.js +213 -0
- package/dist/lib/features/analytics/sessions.js.map +1 -0
- package/dist/lib/features/core/activation.d.ts +1 -24
- package/dist/lib/features/core/activation.js +175 -0
- package/dist/lib/features/core/activation.js.map +1 -0
- package/dist/lib/features/core/experiment.d.ts +53 -21
- package/dist/lib/features/core/experiment.js +554 -0
- package/dist/lib/features/core/experiment.js.map +1 -0
- package/dist/lib/features/core/index.js +9 -0
- package/dist/lib/features/core/index.js.map +1 -0
- package/dist/lib/features/core/track-feature.d.ts +1 -8
- package/dist/lib/features/core/track-feature.js +119 -0
- package/dist/lib/features/core/track-feature.js.map +1 -0
- package/dist/lib/features/core/track-prompt.d.ts +5 -8
- package/dist/lib/features/core/track-prompt.js +181 -0
- package/dist/lib/features/core/track-prompt.js.map +1 -0
- package/dist/lib/features/ecommerce/cart.d.ts +1 -24
- package/dist/lib/features/ecommerce/cart.js +244 -0
- package/dist/lib/features/ecommerce/cart.js.map +1 -0
- package/dist/lib/features/ecommerce/checkout.d.ts +1 -28
- package/dist/lib/features/ecommerce/checkout.js +254 -0
- package/dist/lib/features/ecommerce/checkout.js.map +1 -0
- package/dist/lib/features/ecommerce/index.d.ts +0 -2
- package/dist/lib/features/ecommerce/index.js +15 -0
- package/dist/lib/features/ecommerce/index.js.map +1 -0
- package/dist/lib/features/ecommerce/ltv.d.ts +1 -69
- package/dist/lib/features/ecommerce/ltv.js +114 -0
- package/dist/lib/features/ecommerce/ltv.js.map +1 -0
- package/dist/lib/features/ecommerce/products.d.ts +1 -33
- package/dist/lib/features/ecommerce/products.js +223 -0
- package/dist/lib/features/ecommerce/products.js.map +1 -0
- package/dist/lib/features/ecommerce/promotions.d.ts +1 -30
- package/dist/lib/features/ecommerce/promotions.js +197 -0
- package/dist/lib/features/ecommerce/promotions.js.map +1 -0
- package/dist/lib/features/ecommerce/purchases.d.ts +1 -48
- package/dist/lib/features/ecommerce/purchases.js +193 -0
- package/dist/lib/features/ecommerce/purchases.js.map +1 -0
- package/dist/lib/features/ecommerce/refunds.d.ts +1 -27
- package/dist/lib/features/ecommerce/refunds.js +195 -0
- package/dist/lib/features/ecommerce/refunds.js.map +1 -0
- package/dist/lib/features/ecommerce/search.d.ts +1 -24
- package/dist/lib/features/ecommerce/search.js +156 -0
- package/dist/lib/features/ecommerce/search.js.map +1 -0
- package/dist/lib/features/ecommerce/subscriptions.d.ts +1 -28
- package/dist/lib/features/ecommerce/subscriptions.js +207 -0
- package/dist/lib/features/ecommerce/subscriptions.js.map +1 -0
- package/dist/lib/features/ecommerce/wishlist.d.ts +1 -28
- package/dist/lib/features/ecommerce/wishlist.js +186 -0
- package/dist/lib/features/ecommerce/wishlist.js.map +1 -0
- package/dist/lib/features/enterprise/acquisition.d.ts +2 -57
- package/dist/lib/features/enterprise/acquisition.js +166 -0
- package/dist/lib/features/enterprise/acquisition.js.map +1 -0
- package/dist/lib/features/enterprise/alerts.d.ts +1 -37
- package/dist/lib/features/enterprise/alerts.js +246 -0
- package/dist/lib/features/enterprise/alerts.js.map +1 -0
- package/dist/lib/features/enterprise/churn.d.ts +1 -42
- package/dist/lib/features/enterprise/churn.js +175 -0
- package/dist/lib/features/enterprise/churn.js.map +1 -0
- package/dist/lib/features/enterprise/cohorts.d.ts +1 -37
- package/dist/lib/features/enterprise/cohorts.js +201 -0
- package/dist/lib/features/enterprise/cohorts.js.map +1 -0
- package/dist/lib/features/enterprise/engagement.d.ts +40 -46
- package/dist/lib/features/enterprise/engagement.js +335 -0
- package/dist/lib/features/enterprise/engagement.js.map +1 -0
- package/dist/lib/features/enterprise/forecasting.d.ts +0 -50
- package/dist/lib/features/enterprise/forecasting.js +232 -0
- package/dist/lib/features/enterprise/forecasting.js.map +1 -0
- package/dist/lib/features/enterprise/index.d.ts +1 -1
- package/dist/lib/features/enterprise/index.js +76 -0
- package/dist/lib/features/enterprise/index.js.map +1 -0
- package/dist/lib/features/enterprise/monetization.d.ts +1 -60
- package/dist/lib/features/enterprise/monetization.js +146 -0
- package/dist/lib/features/enterprise/monetization.js.map +1 -0
- package/dist/lib/features/enterprise/multi-path-funnels.d.ts +1 -17
- package/dist/lib/features/enterprise/multi-path-funnels.js +205 -0
- package/dist/lib/features/enterprise/multi-path-funnels.js.map +1 -0
- package/dist/lib/features/enterprise/paths.d.ts +1 -56
- package/dist/lib/features/enterprise/paths.js +149 -0
- package/dist/lib/features/enterprise/paths.js.map +1 -0
- package/dist/lib/features/enterprise/security.d.ts +8 -57
- package/dist/lib/features/enterprise/security.js +202 -0
- package/dist/lib/features/enterprise/security.js.map +1 -0
- package/dist/lib/features/enterprise/support.d.ts +3 -42
- package/dist/lib/features/enterprise/support.js +162 -0
- package/dist/lib/features/enterprise/support.js.map +1 -0
- package/dist/lib/flowgrid.d.ts +278 -0
- package/dist/lib/flowgrid.js +323 -0
- package/dist/lib/flowgrid.js.map +1 -0
- package/dist/lib/frameworks/index.d.ts +13 -6
- package/dist/lib/frameworks/index.js +18 -0
- package/dist/lib/frameworks/index.js.map +1 -0
- package/dist/lib/frameworks/nextjs/client.js +355 -0
- package/dist/lib/frameworks/nextjs/client.js.map +1 -0
- package/dist/lib/frameworks/nextjs/index.js +31 -0
- package/dist/lib/frameworks/nextjs/index.js.map +1 -0
- package/dist/lib/frameworks/nextjs/server.js +259 -0
- package/dist/lib/frameworks/nextjs/server.js.map +1 -0
- package/dist/lib/frameworks/node/index.js +391 -0
- package/dist/lib/frameworks/node/index.js.map +1 -0
- package/dist/lib/frameworks/nuxt/index.js +288 -0
- package/dist/lib/frameworks/nuxt/index.js.map +1 -0
- package/dist/lib/frameworks/react/CookieBanner.js +166 -0
- package/dist/lib/frameworks/react/CookieBanner.js.map +1 -0
- package/dist/lib/frameworks/react/index.js +271 -0
- package/dist/lib/frameworks/react/index.js.map +1 -0
- package/dist/lib/frameworks/vue/CookieBanner.js +165 -0
- package/dist/lib/frameworks/vue/CookieBanner.js.map +1 -0
- package/dist/lib/frameworks/vue/index.js +390 -0
- package/dist/lib/frameworks/vue/index.js.map +1 -0
- package/dist/lib/tracking/analytics/attribution.d.ts +14 -0
- package/dist/lib/tracking/analytics/attribution.js +18 -0
- package/dist/lib/tracking/analytics/attribution.js.map +1 -0
- package/dist/lib/tracking/analytics/events.d.ts +26 -0
- package/dist/lib/tracking/analytics/events.js +30 -0
- package/dist/lib/tracking/analytics/events.js.map +1 -0
- package/dist/lib/tracking/analytics/funnels.d.ts +16 -0
- package/dist/lib/tracking/analytics/funnels.js +22 -0
- package/dist/lib/tracking/analytics/funnels.js.map +1 -0
- package/dist/lib/tracking/analytics/heatmaps.d.ts +27 -0
- package/dist/lib/tracking/analytics/heatmaps.js +22 -0
- package/dist/lib/tracking/analytics/heatmaps.js.map +1 -0
- package/dist/lib/tracking/analytics/identify.d.ts +16 -0
- package/dist/lib/tracking/analytics/identify.js +22 -0
- package/dist/lib/tracking/analytics/identify.js.map +1 -0
- package/dist/lib/tracking/analytics/index.d.ts +10 -0
- package/dist/lib/tracking/analytics/index.js +11 -0
- package/dist/lib/tracking/analytics/index.js.map +1 -0
- package/dist/lib/tracking/analytics/page-views.d.ts +29 -0
- package/dist/lib/tracking/analytics/page-views.js +50 -0
- package/dist/lib/tracking/analytics/page-views.js.map +1 -0
- package/dist/lib/tracking/analytics/performance.d.ts +22 -0
- package/dist/lib/tracking/analytics/performance.js +22 -0
- package/dist/lib/tracking/analytics/performance.js.map +1 -0
- package/dist/lib/tracking/analytics/replay.d.ts +34 -0
- package/dist/lib/tracking/analytics/replay.js +46 -0
- package/dist/lib/tracking/analytics/replay.js.map +1 -0
- package/dist/lib/tracking/analytics/retention.d.ts +10 -0
- package/dist/lib/tracking/analytics/retention.js +10 -0
- package/dist/lib/tracking/analytics/retention.js.map +1 -0
- package/dist/lib/tracking/analytics/sessions.d.ts +20 -0
- package/dist/lib/tracking/analytics/sessions.js +26 -0
- package/dist/lib/tracking/analytics/sessions.js.map +1 -0
- package/dist/lib/tracking/core/activation.d.ts +23 -0
- package/dist/lib/tracking/core/activation.js +33 -0
- package/dist/lib/tracking/core/activation.js.map +1 -0
- package/dist/lib/tracking/core/experiment.d.ts +122 -0
- package/dist/lib/tracking/core/experiment.js +150 -0
- package/dist/lib/tracking/core/experiment.js.map +1 -0
- package/dist/lib/tracking/core/feature.d.ts +13 -0
- package/dist/lib/tracking/core/feature.js +14 -0
- package/dist/lib/tracking/core/feature.js.map +1 -0
- package/dist/lib/tracking/core/index.d.ts +4 -0
- package/dist/lib/tracking/core/index.js +5 -0
- package/dist/lib/tracking/core/index.js.map +1 -0
- package/dist/lib/tracking/core/prompt.d.ts +18 -0
- package/dist/lib/tracking/core/prompt.js +26 -0
- package/dist/lib/tracking/core/prompt.js.map +1 -0
- package/dist/lib/tracking/ecommerce/cart.d.ts +17 -0
- package/dist/lib/tracking/ecommerce/cart.js +22 -0
- package/dist/lib/tracking/ecommerce/cart.js.map +1 -0
- package/dist/lib/tracking/ecommerce/checkout.d.ts +17 -0
- package/dist/lib/tracking/ecommerce/checkout.js +22 -0
- package/dist/lib/tracking/ecommerce/checkout.js.map +1 -0
- package/dist/lib/tracking/ecommerce/index.d.ts +10 -0
- package/dist/lib/tracking/ecommerce/index.js +11 -0
- package/dist/lib/tracking/ecommerce/index.js.map +1 -0
- package/dist/lib/tracking/ecommerce/ltv.d.ts +15 -0
- package/dist/lib/tracking/ecommerce/ltv.js +14 -0
- package/dist/lib/tracking/ecommerce/ltv.js.map +1 -0
- package/dist/lib/tracking/ecommerce/products.d.ts +15 -0
- package/dist/lib/tracking/ecommerce/products.js +18 -0
- package/dist/lib/tracking/ecommerce/products.js.map +1 -0
- package/dist/lib/tracking/ecommerce/promotions.d.ts +14 -0
- package/dist/lib/tracking/ecommerce/promotions.js +25 -0
- package/dist/lib/tracking/ecommerce/promotions.js.map +1 -0
- package/dist/lib/tracking/ecommerce/purchases.d.ts +11 -0
- package/dist/lib/tracking/ecommerce/purchases.js +10 -0
- package/dist/lib/tracking/ecommerce/purchases.js.map +1 -0
- package/dist/lib/tracking/ecommerce/refunds.d.ts +17 -0
- package/dist/lib/tracking/ecommerce/refunds.js +22 -0
- package/dist/lib/tracking/ecommerce/refunds.js.map +1 -0
- package/dist/lib/tracking/ecommerce/search.d.ts +24 -0
- package/dist/lib/tracking/ecommerce/search.js +22 -0
- package/dist/lib/tracking/ecommerce/search.js.map +1 -0
- package/dist/lib/tracking/ecommerce/subscriptions.d.ts +21 -0
- package/dist/lib/tracking/ecommerce/subscriptions.js +38 -0
- package/dist/lib/tracking/ecommerce/subscriptions.js.map +1 -0
- package/dist/lib/tracking/ecommerce/wishlist.d.ts +19 -0
- package/dist/lib/tracking/ecommerce/wishlist.js +26 -0
- package/dist/lib/tracking/ecommerce/wishlist.js.map +1 -0
- package/dist/lib/tracking/enterprise/acquisition.d.ts +25 -0
- package/dist/lib/tracking/enterprise/acquisition.js +22 -0
- package/dist/lib/tracking/enterprise/acquisition.js.map +1 -0
- package/dist/lib/tracking/enterprise/alerts.d.ts +32 -0
- package/dist/lib/tracking/enterprise/alerts.js +30 -0
- package/dist/lib/tracking/enterprise/alerts.js.map +1 -0
- package/dist/lib/tracking/enterprise/churn.d.ts +23 -0
- package/dist/lib/tracking/enterprise/churn.js +22 -0
- package/dist/lib/tracking/enterprise/churn.js.map +1 -0
- package/dist/lib/tracking/enterprise/cohorts.d.ts +18 -0
- package/dist/lib/tracking/enterprise/cohorts.js +18 -0
- package/dist/lib/tracking/enterprise/cohorts.js.map +1 -0
- package/dist/lib/tracking/enterprise/engagement.d.ts +31 -0
- package/dist/lib/tracking/enterprise/engagement.js +44 -0
- package/dist/lib/tracking/enterprise/engagement.js.map +1 -0
- package/dist/lib/tracking/enterprise/forecasting.d.ts +18 -0
- package/dist/lib/tracking/enterprise/forecasting.js +22 -0
- package/dist/lib/tracking/enterprise/forecasting.js.map +1 -0
- package/dist/lib/tracking/enterprise/index.d.ts +11 -0
- package/dist/lib/tracking/enterprise/index.js +12 -0
- package/dist/lib/tracking/enterprise/index.js.map +1 -0
- package/dist/lib/tracking/enterprise/monetization.d.ts +14 -0
- package/dist/lib/tracking/enterprise/monetization.js +10 -0
- package/dist/lib/tracking/enterprise/monetization.js.map +1 -0
- package/dist/lib/tracking/enterprise/multi-path-funnels.d.ts +20 -0
- package/dist/lib/tracking/enterprise/multi-path-funnels.js +18 -0
- package/dist/lib/tracking/enterprise/multi-path-funnels.js.map +1 -0
- package/dist/lib/tracking/enterprise/paths.d.ts +20 -0
- package/dist/lib/tracking/enterprise/paths.js +18 -0
- package/dist/lib/tracking/enterprise/paths.js.map +1 -0
- package/dist/lib/tracking/enterprise/security.d.ts +28 -0
- package/dist/lib/tracking/enterprise/security.js +26 -0
- package/dist/lib/tracking/enterprise/security.js.map +1 -0
- package/dist/lib/tracking/enterprise/support.d.ts +25 -0
- package/dist/lib/tracking/enterprise/support.js +22 -0
- package/dist/lib/tracking/enterprise/support.js.map +1 -0
- package/dist/lib/tracking/index.d.ts +4 -0
- package/dist/lib/tracking/index.js +5 -0
- package/dist/lib/tracking/index.js.map +1 -0
- package/dist/lib/types/analytics.d.ts +10 -2
- package/dist/lib/types/analytics.js +6 -0
- package/dist/lib/types/analytics.js.map +1 -0
- package/dist/lib/types/common.js +6 -0
- package/dist/lib/types/common.js.map +1 -0
- package/dist/lib/types/ecommerce.js +6 -0
- package/dist/lib/types/ecommerce.js.map +1 -0
- package/dist/lib/types/index.js +8 -0
- package/dist/lib/types/index.js.map +1 -0
- package/dist/lib/utils/browser.d.ts +16 -0
- package/dist/lib/utils/browser.js +32 -0
- package/dist/lib/utils/browser.js.map +1 -0
- package/dist/lib/utils/identity.js +346 -0
- package/dist/lib/utils/identity.js.map +1 -0
- package/dist/lib/utils/storage.js +105 -0
- package/dist/lib/utils/storage.js.map +1 -0
- package/dist/umd/237.index.js +2 -0
- package/dist/umd/237.index.js.map +1 -0
- package/dist/umd/546.index.js +3 -0
- package/dist/umd/546.index.js.LICENSE.txt +14 -0
- package/dist/umd/546.index.js.map +1 -0
- package/dist/umd/index.js +2 -0
- package/dist/umd/index.js.map +1 -0
- package/package.json +27 -80
- package/dist/lib/features/ecommerce/inventory.d.ts +0 -219
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
import { FlowGridTransport } from "../../client/transport";
|
|
41
|
-
import {
|
|
41
|
+
import { TimeSeriesData } from "../../types";
|
|
42
|
+
import { ServerCookieReader } from "../../utils/identity";
|
|
42
43
|
/**
|
|
43
44
|
* Configuration for tracking an experiment-related event.
|
|
44
45
|
*
|
|
@@ -65,14 +66,19 @@ export interface ExperimentDefinition {
|
|
|
65
66
|
name: string;
|
|
66
67
|
/** Optional description of experiment purpose */
|
|
67
68
|
description?: string;
|
|
68
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Variants participating in the experiment.
|
|
71
|
+
*
|
|
72
|
+
* Shape matches the verify endpoint and the client-side engine:
|
|
73
|
+
* `{ id, weight }` (with optional human-readable `name`).
|
|
74
|
+
*/
|
|
69
75
|
variants: Array<{
|
|
70
76
|
/** Unique identifier for the variant */
|
|
71
|
-
|
|
72
|
-
/** Human-readable variant name */
|
|
73
|
-
name: string;
|
|
77
|
+
id: string;
|
|
74
78
|
/** Traffic allocation weight (relative distribution) */
|
|
75
79
|
weight: number;
|
|
80
|
+
/** Optional human-readable variant name */
|
|
81
|
+
name?: string;
|
|
76
82
|
}>;
|
|
77
83
|
/**
|
|
78
84
|
* Optional audience configuration.
|
|
@@ -339,6 +345,18 @@ export declare class Experiment extends FlowGridTransport {
|
|
|
339
345
|
private experiments;
|
|
340
346
|
/** Local variant assignments: { [experiment_id]: variant_id } */
|
|
341
347
|
private assignments;
|
|
348
|
+
/**
|
|
349
|
+
* Decode the cookie’s URL-encoded JSON payload into an assignment map.
|
|
350
|
+
* Returns `{}` on any parse failure so a stale cookie never throws.
|
|
351
|
+
*/
|
|
352
|
+
private parseAssignmentCookie;
|
|
353
|
+
private readAssignmentCookie;
|
|
354
|
+
/**
|
|
355
|
+
* Persist `this.assignments` to a first-party cookie so SSR requests on
|
|
356
|
+
* subsequent navigations see the same variant. Best-effort — no-op in
|
|
357
|
+
* non-browser contexts or when document.cookie is unavailable.
|
|
358
|
+
*/
|
|
359
|
+
private writeAssignmentCookie;
|
|
342
360
|
private loadAssignments;
|
|
343
361
|
private saveAssignments;
|
|
344
362
|
/**
|
|
@@ -351,6 +369,34 @@ export declare class Experiment extends FlowGridTransport {
|
|
|
351
369
|
* Returns all current assignments (for SSR hydration).
|
|
352
370
|
*/
|
|
353
371
|
getAssignments(): Record<string, string>;
|
|
372
|
+
/**
|
|
373
|
+
* Hydrate assignments from a server-side cookie reader
|
|
374
|
+
* (Next.js `cookies()`, Nuxt `useCookie()`, Hono ctx, etc.).
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* import { cookies } from "next/headers";
|
|
379
|
+
* fg.experiments.client.hydrateFromCookies(cookies());
|
|
380
|
+
* const variant = fg.experiments.variant("checkout_v2");
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
hydrateFromCookies(reader: ServerCookieReader): void;
|
|
384
|
+
/**
|
|
385
|
+
* Hydrate assignments from a raw `Cookie` request header string.
|
|
386
|
+
* Useful for runtimes without a structured cookie reader (Workers, Bun, etc.).
|
|
387
|
+
*/
|
|
388
|
+
hydrateFromCookieHeader(cookieHeader: string | null | undefined): void;
|
|
389
|
+
/**
|
|
390
|
+
* Serialise current assignments as a `Set-Cookie` header value.
|
|
391
|
+
* Use server-side after a server-only assignment so the response can
|
|
392
|
+
* seed the visitor’s browser with the same variant.
|
|
393
|
+
*/
|
|
394
|
+
serializeAssignmentCookie(): string;
|
|
395
|
+
/**
|
|
396
|
+
* Clear all locally cached assignments (cookie + localStorage + memory).
|
|
397
|
+
* Useful for logout flows or test resets.
|
|
398
|
+
*/
|
|
399
|
+
clearAssignments(): void;
|
|
354
400
|
private static readonly ANON_ID_KEY;
|
|
355
401
|
/**
|
|
356
402
|
* Returns a stable anonymous ID. Generates a UUID on first call and
|
|
@@ -389,10 +435,8 @@ export declare class Experiment extends FlowGridTransport {
|
|
|
389
435
|
*/
|
|
390
436
|
trackLocalConversion(experimentId: string, metricName?: string, metricValue?: number): Promise<void>;
|
|
391
437
|
/**
|
|
392
|
-
* @deprecated
|
|
393
|
-
*
|
|
394
|
-
* backend handler rejects. It is retained only for backwards compatibility
|
|
395
|
-
* and will be removed in a future major version.
|
|
438
|
+
* @deprecated Prefer `assign()`, `trackExposure()`, or `trackConversion()`.
|
|
439
|
+
* Retained as a write-only compatibility shim for generic experiment events.
|
|
396
440
|
*/
|
|
397
441
|
track(config: ExperimentConfig): Promise<ExperimentResponse>;
|
|
398
442
|
/**
|
|
@@ -411,17 +455,5 @@ export declare class Experiment extends FlowGridTransport {
|
|
|
411
455
|
* Records that a user was exposed to a variant.
|
|
412
456
|
*/
|
|
413
457
|
trackExposure(experimentId: string, userId: string, variantId: string): Promise<ExperimentResponse>;
|
|
414
|
-
/**
|
|
415
|
-
* Retrieves statistical results for an experiment.
|
|
416
|
-
*/
|
|
417
|
-
getResults(experimentId: string): Promise<ExperimentResultsData>;
|
|
418
|
-
/**
|
|
419
|
-
* Gets experiment analytics overview.
|
|
420
|
-
*/
|
|
421
|
-
getAnalytics(filter?: DateRangeFilter): Promise<ExperimentAnalyticsData>;
|
|
422
|
-
/**
|
|
423
|
-
* Retrieves the variant assigned to a user for a specific experiment (server-side).
|
|
424
|
-
*/
|
|
425
|
-
getVariant(experimentId: string, userId: string): Promise<VariantResponse | null>;
|
|
426
458
|
}
|
|
427
459
|
export default Experiment;
|
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core - A/B Testing & Experiments
|
|
3
|
+
* @module features/core/experiment
|
|
4
|
+
*
|
|
5
|
+
* @description
|
|
6
|
+
* Track A/B tests, experiments, and variant assignments
|
|
7
|
+
* to measure impact and optimize product decisions.
|
|
8
|
+
*
|
|
9
|
+
* @useCases
|
|
10
|
+
* - Run A/B tests on features
|
|
11
|
+
* - Track variant assignments
|
|
12
|
+
* - Measure experiment impact
|
|
13
|
+
* - Statistical significance analysis
|
|
14
|
+
* - Multi-variant testing
|
|
15
|
+
*
|
|
16
|
+
* @metrics
|
|
17
|
+
* - Conversion by variant
|
|
18
|
+
* - Statistical significance
|
|
19
|
+
* - Sample size per variant
|
|
20
|
+
* - Experiment duration
|
|
21
|
+
* - Impact on key metrics
|
|
22
|
+
*
|
|
23
|
+
* @chartData
|
|
24
|
+
* - Bar chart: Conversion by variant
|
|
25
|
+
* - Line chart: Metric trends by variant
|
|
26
|
+
* - Table: Experiment results summary
|
|
27
|
+
* - Confidence interval visualization
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const experiment = new Experiment(webId, endpoint, apiKey);
|
|
32
|
+
*
|
|
33
|
+
* await experiment.track({
|
|
34
|
+
* eventName: 'checkout_flow_test',
|
|
35
|
+
* variant: 'simplified_v2',
|
|
36
|
+
* properties: { userId: 'user_123' }
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
import { FlowGridTransport } from "../../client/transport";
|
|
41
|
+
import { getCookie } from "../../utils/storage";
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// HASHING UTILITY (FNV-1a)
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* FNV-1a 32-bit hash — lightweight deterministic hash for variant assignment.
|
|
47
|
+
* Ensures the same user always gets the same variant regardless of device.
|
|
48
|
+
*/
|
|
49
|
+
function fnv1a(input) {
|
|
50
|
+
let hash = 0x811c9dc5; // FNV offset basis
|
|
51
|
+
for (let i = 0; i < input.length; i++) {
|
|
52
|
+
hash ^= input.charCodeAt(i);
|
|
53
|
+
hash = (hash * 0x01000193) >>> 0; // FNV prime, unsigned 32-bit
|
|
54
|
+
}
|
|
55
|
+
return hash;
|
|
56
|
+
}
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// CONSTANTS
|
|
59
|
+
// ============================================================================
|
|
60
|
+
const ASSIGNMENT_STORAGE_KEY = 'fg_exp_assignments';
|
|
61
|
+
const ASSIGNMENT_COOKIE_KEY = 'fg_exp_assignments';
|
|
62
|
+
const ASSIGNMENT_COOKIE_MAX_AGE_DAYS = 365;
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// EXPERIMENT CLASS
|
|
65
|
+
// ============================================================================
|
|
66
|
+
/**
|
|
67
|
+
* Experiment - A/B Testing & Experimentation SDK
|
|
68
|
+
*
|
|
69
|
+
* @extends FlowGridTransport
|
|
70
|
+
*
|
|
71
|
+
* @description
|
|
72
|
+
* Provides structured experiment lifecycle management including:
|
|
73
|
+
* - Experiment definition
|
|
74
|
+
* - Deterministic or server-based variant assignment
|
|
75
|
+
* - Exposure tracking
|
|
76
|
+
* - Conversion tracking
|
|
77
|
+
* - Statistical results & analysis
|
|
78
|
+
*
|
|
79
|
+
* This class enforces correct HTTP semantics:
|
|
80
|
+
*
|
|
81
|
+
* Exposure and conversion events are strictly POST to preserve:
|
|
82
|
+
* - Statistical validity
|
|
83
|
+
* - Unique participant counting
|
|
84
|
+
* - Accurate conversion attribution
|
|
85
|
+
* - Idempotent replay safety
|
|
86
|
+
*
|
|
87
|
+
* @lifecycle
|
|
88
|
+
* 1. Define experiment
|
|
89
|
+
* 2. Assign user to variant
|
|
90
|
+
* 3. Track exposure (when user actually sees variant)
|
|
91
|
+
* 4. Track conversions (primary/secondary metrics)
|
|
92
|
+
* 5. Query results & statistical analysis
|
|
93
|
+
*
|
|
94
|
+
* @statistical_model
|
|
95
|
+
* - Variant-level sample sizes tracked independently
|
|
96
|
+
* - Conversion rates calculated per variant
|
|
97
|
+
* - Confidence intervals computed server-side
|
|
98
|
+
* - Statistical significance evaluated against control
|
|
99
|
+
*
|
|
100
|
+
* @guarantees
|
|
101
|
+
* - Assignment is explicitly recorded
|
|
102
|
+
* - Exposure tracking is immutable
|
|
103
|
+
* - Conversion events are attributable to assigned variant
|
|
104
|
+
* - Results queries never mutate experiment state
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const experiment = new Experiment(webId, endpoint, apiKey);
|
|
109
|
+
*
|
|
110
|
+
* // 1. Define experiment
|
|
111
|
+
* await experiment.define({
|
|
112
|
+
* experimentId: 'checkout_v2',
|
|
113
|
+
* name: 'Simplified Checkout',
|
|
114
|
+
* variants: [
|
|
115
|
+
* { variantId: 'control', name: 'Current', weight: 50 },
|
|
116
|
+
* { variantId: 'treatment', name: 'Simplified', weight: 50 }
|
|
117
|
+
* ],
|
|
118
|
+
* primaryMetric: 'checkout_completed'
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* // 2. Assign user
|
|
122
|
+
* await experiment.assign({
|
|
123
|
+
* experimentId: 'checkout_v2',
|
|
124
|
+
* userId: 'user_123',
|
|
125
|
+
* variantId: 'treatment'
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* // 3. Track exposure
|
|
129
|
+
* await experiment.trackExposure('checkout_v2', 'user_123', 'treatment');
|
|
130
|
+
*
|
|
131
|
+
* // 4. Track conversion
|
|
132
|
+
* await experiment.trackConversion({
|
|
133
|
+
* experimentId: 'checkout_v2',
|
|
134
|
+
* userId: 'user_123',
|
|
135
|
+
* variantId: 'treatment',
|
|
136
|
+
* metricName: 'checkout_completed',
|
|
137
|
+
* value: 1
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* // 5. Fetch results
|
|
141
|
+
* const results = await experiment.getResults('checkout_v2');
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export class Experiment extends FlowGridTransport {
|
|
145
|
+
constructor() {
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// CLIENT-SIDE EXPERIMENT ENGINE STATE
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
super(...arguments);
|
|
150
|
+
/** Verified experiment configs: { [experiment_id]: { variants, properties, verified } } */
|
|
151
|
+
this.experiments = {};
|
|
152
|
+
/** Local variant assignments: { [experiment_id]: variant_id } */
|
|
153
|
+
this.assignments = {};
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// CLIENT-SIDE ENGINE — SSR-safe persistence (localStorage + cookie mirror)
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Decode the cookie’s URL-encoded JSON payload into an assignment map.
|
|
160
|
+
* Returns `{}` on any parse failure so a stale cookie never throws.
|
|
161
|
+
*/
|
|
162
|
+
parseAssignmentCookie(raw) {
|
|
163
|
+
if (!raw)
|
|
164
|
+
return {};
|
|
165
|
+
try {
|
|
166
|
+
const decoded = decodeURIComponent(raw);
|
|
167
|
+
const parsed = JSON.parse(decoded);
|
|
168
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
readAssignmentCookie() {
|
|
175
|
+
return this.parseAssignmentCookie(getCookie(ASSIGNMENT_COOKIE_KEY));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Persist `this.assignments` to a first-party cookie so SSR requests on
|
|
179
|
+
* subsequent navigations see the same variant. Best-effort — no-op in
|
|
180
|
+
* non-browser contexts or when document.cookie is unavailable.
|
|
181
|
+
*/
|
|
182
|
+
writeAssignmentCookie() {
|
|
183
|
+
try {
|
|
184
|
+
if (typeof document === 'undefined')
|
|
185
|
+
return;
|
|
186
|
+
const value = encodeURIComponent(JSON.stringify(this.assignments));
|
|
187
|
+
const maxAge = ASSIGNMENT_COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
|
|
188
|
+
const sameSite = 'Lax';
|
|
189
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:'
|
|
190
|
+
? '; Secure'
|
|
191
|
+
: '';
|
|
192
|
+
document.cookie = `${ASSIGNMENT_COOKIE_KEY}=${value}; Path=/; Max-Age=${maxAge}; SameSite=${sameSite}${secure}`;
|
|
193
|
+
}
|
|
194
|
+
catch { /* SSR or restricted env */ }
|
|
195
|
+
}
|
|
196
|
+
loadAssignments() {
|
|
197
|
+
// 1. localStorage — authoritative on the client
|
|
198
|
+
try {
|
|
199
|
+
if (typeof localStorage !== 'undefined') {
|
|
200
|
+
const stored = localStorage.getItem(ASSIGNMENT_STORAGE_KEY);
|
|
201
|
+
if (stored) {
|
|
202
|
+
const parsed = JSON.parse(stored);
|
|
203
|
+
if (parsed && typeof parsed === 'object')
|
|
204
|
+
return parsed;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch { /* SSR or privacy mode */ }
|
|
209
|
+
// 2. Cookie fallback — lets SSR + first-paint resolve a variant
|
|
210
|
+
const cookieAssignments = this.readAssignmentCookie();
|
|
211
|
+
if (Object.keys(cookieAssignments).length > 0)
|
|
212
|
+
return cookieAssignments;
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
saveAssignments() {
|
|
216
|
+
try {
|
|
217
|
+
if (typeof localStorage !== 'undefined') {
|
|
218
|
+
localStorage.setItem(ASSIGNMENT_STORAGE_KEY, JSON.stringify(this.assignments));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch { /* SSR or privacy mode */ }
|
|
222
|
+
// Mirror to cookie so the next SSR request can hydrate without flashing.
|
|
223
|
+
this.writeAssignmentCookie();
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Inject assignments from server context (SSR/Node).
|
|
227
|
+
* Use this when assignments were resolved server-side and need to be
|
|
228
|
+
* available before localStorage is accessible.
|
|
229
|
+
*/
|
|
230
|
+
injectAssignments(assignments) {
|
|
231
|
+
this.assignments = { ...this.assignments, ...assignments };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Returns all current assignments (for SSR hydration).
|
|
235
|
+
*/
|
|
236
|
+
getAssignments() {
|
|
237
|
+
return { ...this.assignments };
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Hydrate assignments from a server-side cookie reader
|
|
241
|
+
* (Next.js `cookies()`, Nuxt `useCookie()`, Hono ctx, etc.).
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { cookies } from "next/headers";
|
|
246
|
+
* fg.experiments.client.hydrateFromCookies(cookies());
|
|
247
|
+
* const variant = fg.experiments.variant("checkout_v2");
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
hydrateFromCookies(reader) {
|
|
251
|
+
const raw = reader.get(ASSIGNMENT_COOKIE_KEY);
|
|
252
|
+
const value = typeof raw === 'string' ? raw : raw?.value ?? null;
|
|
253
|
+
const parsed = this.parseAssignmentCookie(value ?? null);
|
|
254
|
+
if (Object.keys(parsed).length > 0)
|
|
255
|
+
this.injectAssignments(parsed);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Hydrate assignments from a raw `Cookie` request header string.
|
|
259
|
+
* Useful for runtimes without a structured cookie reader (Workers, Bun, etc.).
|
|
260
|
+
*/
|
|
261
|
+
hydrateFromCookieHeader(cookieHeader) {
|
|
262
|
+
if (!cookieHeader)
|
|
263
|
+
return;
|
|
264
|
+
const parts = cookieHeader.split(/;\s*/);
|
|
265
|
+
const prefix = `${ASSIGNMENT_COOKIE_KEY}=`;
|
|
266
|
+
const match = parts.find(p => p.startsWith(prefix));
|
|
267
|
+
if (!match)
|
|
268
|
+
return;
|
|
269
|
+
const parsed = this.parseAssignmentCookie(match.slice(prefix.length));
|
|
270
|
+
if (Object.keys(parsed).length > 0)
|
|
271
|
+
this.injectAssignments(parsed);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Serialise current assignments as a `Set-Cookie` header value.
|
|
275
|
+
* Use server-side after a server-only assignment so the response can
|
|
276
|
+
* seed the visitor’s browser with the same variant.
|
|
277
|
+
*/
|
|
278
|
+
serializeAssignmentCookie() {
|
|
279
|
+
const value = encodeURIComponent(JSON.stringify(this.assignments));
|
|
280
|
+
const maxAge = ASSIGNMENT_COOKIE_MAX_AGE_DAYS * 24 * 60 * 60;
|
|
281
|
+
return `${ASSIGNMENT_COOKIE_KEY}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax`;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Clear all locally cached assignments (cookie + localStorage + memory).
|
|
285
|
+
* Useful for logout flows or test resets.
|
|
286
|
+
*/
|
|
287
|
+
clearAssignments() {
|
|
288
|
+
this.assignments = {};
|
|
289
|
+
try {
|
|
290
|
+
if (typeof localStorage !== 'undefined')
|
|
291
|
+
localStorage.removeItem(ASSIGNMENT_STORAGE_KEY);
|
|
292
|
+
}
|
|
293
|
+
catch { /* noop */ }
|
|
294
|
+
try {
|
|
295
|
+
if (typeof document !== 'undefined') {
|
|
296
|
+
document.cookie = `${ASSIGNMENT_COOKIE_KEY}=; Path=/; Max-Age=0; SameSite=Lax`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { /* noop */ }
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Returns a stable anonymous ID. Generates a UUID on first call and
|
|
303
|
+
* persists it in localStorage so the same anonymous visitor always
|
|
304
|
+
* receives a consistent variant assignment.
|
|
305
|
+
*/
|
|
306
|
+
getOrCreateAnonId() {
|
|
307
|
+
try {
|
|
308
|
+
if (typeof localStorage !== 'undefined') {
|
|
309
|
+
const existing = localStorage.getItem(Experiment.ANON_ID_KEY);
|
|
310
|
+
if (existing)
|
|
311
|
+
return existing;
|
|
312
|
+
const id = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
313
|
+
? crypto.randomUUID()
|
|
314
|
+
: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
315
|
+
const r = (Math.random() * 16) | 0;
|
|
316
|
+
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
317
|
+
});
|
|
318
|
+
localStorage.setItem(Experiment.ANON_ID_KEY, id);
|
|
319
|
+
return id;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch { /* SSR or privacy mode */ }
|
|
323
|
+
// Non-browser fallback — still deterministic within process lifetime
|
|
324
|
+
return 'anon_' + fnv1a(String(Math.random())).toString(36);
|
|
325
|
+
}
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// CLIENT-SIDE ENGINE — deterministic hash-based assignment
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
/**
|
|
330
|
+
* Deterministic variant assignment using FNV-1a hash.
|
|
331
|
+
* Same userId + experimentId always produces the same variant.
|
|
332
|
+
*/
|
|
333
|
+
deterministicAssign(userId, experimentId, variants) {
|
|
334
|
+
const hashValue = fnv1a(userId + experimentId);
|
|
335
|
+
let totalWeight = 0;
|
|
336
|
+
for (const v of variants)
|
|
337
|
+
totalWeight += v.weight;
|
|
338
|
+
const bucket = (hashValue % 10000) / 10000 * totalWeight;
|
|
339
|
+
let cumulative = 0;
|
|
340
|
+
for (const v of variants) {
|
|
341
|
+
cumulative += v.weight;
|
|
342
|
+
if (bucket < cumulative)
|
|
343
|
+
return v.id;
|
|
344
|
+
}
|
|
345
|
+
return variants[variants.length - 1].id;
|
|
346
|
+
}
|
|
347
|
+
assignVariant(experimentId, variants) {
|
|
348
|
+
// Reload from localStorage to handle cross-tab race conditions
|
|
349
|
+
this.assignments = this.loadAssignments();
|
|
350
|
+
const existing = this.assignments[experimentId];
|
|
351
|
+
if (existing && variants.some(v => v.id === existing)) {
|
|
352
|
+
// Existing assignment still valid — keep it
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Resolve a userId for the hash: prefer identityManager's visitorId, then fall back
|
|
356
|
+
const userId = this.identityManager?.getVisitorId?.()
|
|
357
|
+
|| this.visitorId
|
|
358
|
+
|| this.getOrCreateAnonId();
|
|
359
|
+
// Deterministic assignment via hash
|
|
360
|
+
const assignedVariantId = this.deterministicAssign(userId, experimentId, variants);
|
|
361
|
+
this.assignments[experimentId] = assignedVariantId;
|
|
362
|
+
this.saveAssignments();
|
|
363
|
+
// Fire-and-forget assignment event.
|
|
364
|
+
// The backend AssignmentHandlerNode requires `userId` in properties.
|
|
365
|
+
this.init('experiment_assignment', {
|
|
366
|
+
type: 'experiment',
|
|
367
|
+
subtype: 'assignment',
|
|
368
|
+
experimentId,
|
|
369
|
+
userId,
|
|
370
|
+
variantId: assignedVariantId,
|
|
371
|
+
source: 'random',
|
|
372
|
+
assignmentSource: 'sdk_init',
|
|
373
|
+
timestamp: new Date().toISOString(),
|
|
374
|
+
}).catch(() => { });
|
|
375
|
+
}
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// CLIENT-SIDE ENGINE — server verification
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
async verifyExperiments(configs) {
|
|
380
|
+
const payload = {
|
|
381
|
+
web_id: this.webId,
|
|
382
|
+
experiments: configs.map(exp => ({
|
|
383
|
+
experiment_id: exp.id,
|
|
384
|
+
variants: exp.variants,
|
|
385
|
+
properties: exp.properties || {},
|
|
386
|
+
})),
|
|
387
|
+
};
|
|
388
|
+
try {
|
|
389
|
+
const res = await fetch(`${this.endpoint}/api/v1/experiments/verify`, {
|
|
390
|
+
method: 'POST',
|
|
391
|
+
headers: { 'Content-Type': 'application/json' },
|
|
392
|
+
body: JSON.stringify(payload),
|
|
393
|
+
});
|
|
394
|
+
const response = await res.json();
|
|
395
|
+
if (response.ok && Array.isArray(response.data)) {
|
|
396
|
+
for (const item of response.data) {
|
|
397
|
+
if (item.action !== 'error') {
|
|
398
|
+
this.experiments[item.experiment_id] = {
|
|
399
|
+
variants: item.variants,
|
|
400
|
+
properties: item.properties ?? {},
|
|
401
|
+
verified: true,
|
|
402
|
+
};
|
|
403
|
+
this.assignVariant(item.experiment_id, item.variants);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// Network failure — fall back to local config
|
|
410
|
+
for (const exp of configs) {
|
|
411
|
+
this.experiments[exp.id] = {
|
|
412
|
+
variants: exp.variants,
|
|
413
|
+
properties: exp.properties || {},
|
|
414
|
+
verified: false,
|
|
415
|
+
};
|
|
416
|
+
this.assignVariant(exp.id, exp.variants);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// CLIENT-SIDE ENGINE — public init
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
/**
|
|
424
|
+
* Initialise the client-side experiment engine.
|
|
425
|
+
*
|
|
426
|
+
* Verifies experiment configs against the API, assigns variants using
|
|
427
|
+
* weighted random allocation, and persists assignments in localStorage
|
|
428
|
+
* under `fg_exp_assignments`.
|
|
429
|
+
*
|
|
430
|
+
* @param experiments - Array of experiment configs to register
|
|
431
|
+
*/
|
|
432
|
+
async initExperiments(experiments) {
|
|
433
|
+
this.assignments = this.loadAssignments();
|
|
434
|
+
if (experiments.length > 0) {
|
|
435
|
+
await this.verifyExperiments(experiments);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Returns the locally assigned variant for an experiment.
|
|
440
|
+
*
|
|
441
|
+
* Reads from the in-memory assignment map (backed by localStorage).
|
|
442
|
+
* Returns `null` if no assignment exists.
|
|
443
|
+
*/
|
|
444
|
+
getLocalVariant(experimentId) {
|
|
445
|
+
// Re-read from storage to stay in sync with other tabs
|
|
446
|
+
this.assignments = this.loadAssignments();
|
|
447
|
+
return this.assignments[experimentId] ?? null;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Track a conversion against the locally assigned variant.
|
|
451
|
+
*
|
|
452
|
+
* No-op if the visitor has no assignment for this experiment.
|
|
453
|
+
*/
|
|
454
|
+
async trackLocalConversion(experimentId, metricName = 'conversion', metricValue = 1) {
|
|
455
|
+
const variantId = this.getLocalVariant(experimentId);
|
|
456
|
+
if (!variantId)
|
|
457
|
+
return;
|
|
458
|
+
// Backend ConversionHandlerNode requires `userId` — resolve from
|
|
459
|
+
// identity manager or the persisted anonymous ID.
|
|
460
|
+
const userId = this.identityManager?.getVisitorId?.()
|
|
461
|
+
|| this.visitorId
|
|
462
|
+
|| this.getOrCreateAnonId();
|
|
463
|
+
await this.init('experiment_conversion', {
|
|
464
|
+
type: 'experiment',
|
|
465
|
+
subtype: 'conversion',
|
|
466
|
+
experimentId,
|
|
467
|
+
userId,
|
|
468
|
+
variantId,
|
|
469
|
+
metricName,
|
|
470
|
+
value: metricValue,
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// SERVER-SIDE EXPERIMENT METHODS
|
|
476
|
+
//
|
|
477
|
+
// Backend supports write events only: definition, assignment, conversion,
|
|
478
|
+
// exposure, plus the deprecated generic custom shim below. Read endpoints
|
|
479
|
+
// (results / analytics / variant lookup) are not exposed via the SDK
|
|
480
|
+
// functions router.
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
/**
|
|
483
|
+
* @deprecated Prefer `assign()`, `trackExposure()`, or `trackConversion()`.
|
|
484
|
+
* Retained as a write-only compatibility shim for generic experiment events.
|
|
485
|
+
*/
|
|
486
|
+
async track(config) {
|
|
487
|
+
return await this.init(config.eventName, {
|
|
488
|
+
...config.properties,
|
|
489
|
+
type: "experiment",
|
|
490
|
+
subtype: "custom",
|
|
491
|
+
variant: config.variant,
|
|
492
|
+
timestamp: new Date().toISOString()
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Defines a new experiment.
|
|
497
|
+
*/
|
|
498
|
+
async define(definition) {
|
|
499
|
+
return await this.init("experiment_definition", {
|
|
500
|
+
type: "experiment",
|
|
501
|
+
subtype: "definition",
|
|
502
|
+
...definition,
|
|
503
|
+
timestamp: new Date().toISOString()
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Assigns a user to a variant.
|
|
508
|
+
*/
|
|
509
|
+
async assign(config) {
|
|
510
|
+
return await this.init("experiment_assignment", {
|
|
511
|
+
type: "experiment",
|
|
512
|
+
subtype: "assignment",
|
|
513
|
+
experimentId: config.experimentId,
|
|
514
|
+
userId: config.userId,
|
|
515
|
+
variantId: config.variantId,
|
|
516
|
+
source: config.source || "random",
|
|
517
|
+
timestamp: new Date().toISOString()
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Records a conversion event for an experiment.
|
|
522
|
+
*/
|
|
523
|
+
async trackConversion(config) {
|
|
524
|
+
return await this.init("experiment_conversion", {
|
|
525
|
+
type: "experiment",
|
|
526
|
+
subtype: "conversion",
|
|
527
|
+
experimentId: config.experimentId,
|
|
528
|
+
userId: config.userId,
|
|
529
|
+
variantId: config.variantId,
|
|
530
|
+
metricName: config.metricName,
|
|
531
|
+
value: config.value,
|
|
532
|
+
timestamp: new Date().toISOString()
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Records that a user was exposed to a variant.
|
|
537
|
+
*/
|
|
538
|
+
async trackExposure(experimentId, userId, variantId) {
|
|
539
|
+
return await this.init("experiment_exposure", {
|
|
540
|
+
type: "experiment",
|
|
541
|
+
subtype: "exposure",
|
|
542
|
+
experimentId,
|
|
543
|
+
userId,
|
|
544
|
+
variantId,
|
|
545
|
+
timestamp: new Date().toISOString()
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
// CLIENT-SIDE ENGINE — stable anonymous ID
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
Experiment.ANON_ID_KEY = 'fg_anon_id';
|
|
553
|
+
export default Experiment;
|
|
554
|
+
//# sourceMappingURL=experiment.js.map
|