@webiny/website-builder-sdk 0.0.0-unstable.7be00a75a9 → 0.0.0-unstable.7c2def32a7
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/BindingsResolver.d.ts +4 -1
- package/BindingsResolver.js.map +1 -1
- package/ComponentInputTraverser.js +4 -0
- package/ComponentInputTraverser.js.map +1 -1
- package/ComponentResolver.d.ts +6 -3
- package/ComponentResolver.js +9 -4
- package/ComponentResolver.js.map +1 -1
- package/ContentSdk.d.ts +67 -1
- package/ContentSdk.js +90 -8
- package/ContentSdk.js.map +1 -1
- package/DocumentStoreManager.d.ts +9 -2
- package/DocumentStoreManager.js +24 -5
- package/DocumentStoreManager.js.map +1 -1
- package/EditingSdk.d.ts +7 -1
- package/EditingSdk.js +5 -3
- package/EditingSdk.js.map +1 -1
- package/ElementFactory.js +7 -2
- package/ElementFactory.js.map +1 -1
- package/ElementFactory.test.js +102 -0
- package/ElementFactory.test.js.map +1 -1
- package/LiveSdk.d.ts +3 -1
- package/LiveSdk.js +3 -2
- package/LiveSdk.js.map +1 -1
- package/PreviewDocument.d.ts +3 -1
- package/PreviewDocument.js +5 -2
- package/PreviewDocument.js.map +1 -1
- package/PreviewSdk.d.ts +4 -1
- package/PreviewSdk.js +6 -2
- package/PreviewSdk.js.map +1 -1
- package/ResizeObserver.js +1 -1
- package/ResizeObserver.js.map +1 -1
- package/StylesBindingsProcessor.js +3 -2
- package/StylesBindingsProcessor.js.map +1 -1
- package/Theme.js +2 -1
- package/Theme.js.map +1 -1
- package/asset/deliveryUrl.d.ts +41 -0
- package/asset/deliveryUrl.js +34 -0
- package/asset/deliveryUrl.js.map +1 -0
- package/asset/deliveryUrl.test.d.ts +1 -0
- package/asset/deliveryUrl.test.js +127 -0
- package/asset/deliveryUrl.test.js.map +1 -0
- package/asset/geometry.d.ts +7 -0
- package/asset/geometry.js +10 -0
- package/asset/geometry.js.map +1 -0
- package/asset/imageSrcSet.d.ts +63 -0
- package/asset/imageSrcSet.js +98 -0
- package/asset/imageSrcSet.js.map +1 -0
- package/asset/imageSrcSet.test.d.ts +1 -0
- package/asset/imageSrcSet.test.js +103 -0
- package/asset/imageSrcSet.test.js.map +1 -0
- package/asset/index.d.ts +5 -0
- package/asset/index.js +5 -0
- package/asset/normalize.d.ts +24 -0
- package/asset/normalize.js +106 -0
- package/asset/normalize.js.map +1 -0
- package/asset/normalize.test.d.ts +1 -0
- package/asset/normalize.test.js +263 -0
- package/asset/normalize.test.js.map +1 -0
- package/asset/types.d.ts +2 -0
- package/asset/types.js +0 -0
- package/componentLoader/ComponentLoader.d.ts +16 -0
- package/componentLoader/ComponentLoader.js +0 -0
- package/componentLoader/RegistryOnlyComponentLoader.d.ts +9 -0
- package/componentLoader/RegistryOnlyComponentLoader.js +8 -0
- package/componentLoader/RegistryOnlyComponentLoader.js.map +1 -0
- package/componentLoader/createComponentLoader.d.ts +7 -0
- package/componentLoader/createComponentLoader.js +10 -0
- package/componentLoader/createComponentLoader.js.map +1 -0
- package/componentLoader/defaultComponentLoader.d.ts +7 -0
- package/componentLoader/defaultComponentLoader.js +8 -0
- package/componentLoader/defaultComponentLoader.js.map +1 -0
- package/componentLoader/index.d.ts +3 -0
- package/componentLoader/index.js +3 -0
- package/contentClientState.test.d.ts +1 -0
- package/contentClientState.test.js +384 -0
- package/contentClientState.test.js.map +1 -0
- package/contentEntry/ContentEntryBindingsResolver.d.ts +33 -0
- package/contentEntry/ContentEntryBindingsResolver.js +52 -0
- package/contentEntry/ContentEntryBindingsResolver.js.map +1 -0
- package/contentEntry/ContentEntryCache.d.ts +49 -0
- package/contentEntry/ContentEntryCache.js +51 -0
- package/contentEntry/ContentEntryCache.js.map +1 -0
- package/contentEntry/resolveContentEntry.d.ts +74 -0
- package/contentEntry/resolveContentEntry.js +53 -0
- package/contentEntry/resolveContentEntry.js.map +1 -0
- package/contentEntry/resolveContentEntry.test.d.ts +1 -0
- package/contentEntry/resolveContentEntry.test.js +213 -0
- package/contentEntry/resolveContentEntry.test.js.map +1 -0
- package/createComponent.d.ts +5 -0
- package/createComponent.js +32 -0
- package/createComponent.js.map +1 -0
- package/createContentClient.d.ts +17 -0
- package/createContentClient.js +16 -0
- package/createContentClient.js.map +1 -0
- package/createContentClient.test.d.ts +1 -0
- package/createContentClient.test.js +165 -0
- package/createContentClient.test.js.map +1 -0
- package/createInput.d.ts +10 -2
- package/createInput.js +8 -1
- package/createInput.js.map +1 -1
- package/dataProviders/ApiClient.d.ts +10 -5
- package/dataProviders/ApiClient.js +10 -10
- package/dataProviders/ApiClient.js.map +1 -1
- package/dataProviders/DefaultDataProvider.d.ts +4 -0
- package/dataProviders/DefaultDataProvider.js +35 -0
- package/dataProviders/DefaultDataProvider.js.map +1 -1
- package/dataProviders/GET_EXPERIMENT_PAUSED.d.ts +1 -0
- package/dataProviders/GET_EXPERIMENT_PAUSED.js +17 -0
- package/dataProviders/GET_EXPERIMENT_PAUSED.js.map +1 -0
- package/dataProviders/GET_PAGE_BY_ID.d.ts +1 -1
- package/dataProviders/GET_PAGE_BY_ID.js +2 -0
- package/dataProviders/GET_PAGE_BY_ID.js.map +1 -1
- package/dataProviders/GET_PAGE_BY_PATH.d.ts +1 -1
- package/dataProviders/GET_PAGE_BY_PATH.js +2 -0
- package/dataProviders/GET_PAGE_BY_PATH.js.map +1 -1
- package/dataProviders/GET_PAGE_EXPERIMENT.d.ts +1 -0
- package/dataProviders/GET_PAGE_EXPERIMENT.js +32 -0
- package/dataProviders/GET_PAGE_EXPERIMENT.js.map +1 -0
- package/dataProviders/GET_VARIANT_CONTENT.d.ts +1 -0
- package/dataProviders/GET_VARIANT_CONTENT.js +24 -0
- package/dataProviders/GET_VARIANT_CONTENT.js.map +1 -0
- package/dataProviders/NullDataProvider.d.ts +4 -0
- package/dataProviders/NullDataProvider.js +9 -0
- package/dataProviders/NullDataProvider.js.map +1 -1
- package/dataProviders/queries.test.d.ts +1 -0
- package/dataProviders/queries.test.js +45 -0
- package/dataProviders/queries.test.js.map +1 -0
- package/defaultBreakpoints.js +23 -3
- package/defaultBreakpoints.js.map +1 -1
- package/experiments/PostHogAnalyticsProvider.d.ts +31 -0
- package/experiments/PostHogAnalyticsProvider.js +28 -0
- package/experiments/PostHogAnalyticsProvider.js.map +1 -0
- package/experiments/analyticsProvider.d.ts +4 -0
- package/experiments/analyticsProvider.js +11 -0
- package/experiments/analyticsProvider.js.map +1 -0
- package/experiments/bucketing.d.ts +27 -0
- package/experiments/bucketing.js +91 -0
- package/experiments/bucketing.js.map +1 -0
- package/experiments/bucketing.test.d.ts +1 -0
- package/experiments/bucketing.test.js +183 -0
- package/experiments/bucketing.test.js.map +1 -0
- package/experiments/index.d.ts +5 -0
- package/experiments/index.js +5 -0
- package/experiments/render.d.ts +82 -0
- package/experiments/render.js +149 -0
- package/experiments/render.js.map +1 -0
- package/experiments/types.d.ts +108 -0
- package/experiments/types.js +4 -0
- package/experiments/types.js.map +1 -0
- package/headersProvider.d.ts +1 -2
- package/headersProvider.js.map +1 -1
- package/image/geometry.d.ts +54 -0
- package/image/geometry.js +112 -0
- package/image/geometry.js.map +1 -0
- package/image/geometry.test.d.ts +1 -0
- package/image/geometry.test.js +190 -0
- package/image/geometry.test.js.map +1 -0
- package/image/index.d.ts +3 -0
- package/image/index.js +3 -0
- package/image/presets.d.ts +6 -0
- package/image/presets.js +20 -0
- package/image/presets.js.map +1 -0
- package/image/types.d.ts +10 -0
- package/image/types.js +0 -0
- package/index.d.ts +12 -0
- package/index.js +12 -0
- package/jsonPatch.js +3 -3
- package/package.json +7 -6
- package/types/WebsiteBuilderTheme.d.ts +5 -0
- package/types.d.ts +75 -1
package/defaultBreakpoints.js
CHANGED
|
@@ -2,21 +2,41 @@ const defaultBreakpoints = {
|
|
|
2
2
|
desktop: {
|
|
3
3
|
title: "Desktop",
|
|
4
4
|
description: "Desktop styles apply at all breakpoints, unless they're edited at a lower breakpoint. Start your styling here.",
|
|
5
|
-
icon:
|
|
5
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
6
|
+
<path d="M13.3333 12C14.0667 12 14.6667 11.4 14.6667 10.6666V3.99996C14.6667 3.26663 14.0667 2.66663 13.3333 2.66663H2.66667C1.93333 2.66663 1.33333 3.26663 1.33333 3.99996V10.6666C1.33333 11.4 1.93333 12 2.66667 12H0V13.3333H16V12H13.3333ZM2.66667 3.99996H13.3333V10.6666H2.66667V3.99996Z" fill="#59626D"/>
|
|
7
|
+
</svg>`,
|
|
6
8
|
minWidth: 0,
|
|
7
9
|
maxWidth: 4000
|
|
8
10
|
},
|
|
9
11
|
tablet: {
|
|
10
12
|
title: "Tablet",
|
|
11
13
|
description: "Styles added here will apply at 991px and below, unless they're edited at a smaller breakpoint.",
|
|
12
|
-
icon:
|
|
14
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
15
|
+
<g clip-path="url(#clip0_6424_8826)">
|
|
16
|
+
<path d="M12.666 0H3.33268C2.41268 0 1.66602 0.746667 1.66602 1.66667V14.3333C1.66602 15.2533 2.41268 16 3.33268 16H12.666C13.586 16 14.3327 15.2533 14.3327 14.3333V1.66667C14.3327 0.746667 13.586 0 12.666 0ZM7.99935 15.3333C7.44602 15.3333 6.99935 14.8867 6.99935 14.3333C6.99935 13.78 7.44602 13.3333 7.99935 13.3333C8.55268 13.3333 8.99935 13.78 8.99935 14.3333C8.99935 14.8867 8.55268 15.3333 7.99935 15.3333ZM12.9993 12.6667H2.99935V2H12.9993V12.6667Z" fill="#9DA4B0"/>
|
|
17
|
+
</g>
|
|
18
|
+
<defs>
|
|
19
|
+
<clipPath id="clip0_6424_8826">
|
|
20
|
+
<rect width="16" height="16" fill="white"/>
|
|
21
|
+
</clipPath>
|
|
22
|
+
</defs>
|
|
23
|
+
</svg>`,
|
|
13
24
|
minWidth: 0,
|
|
14
25
|
maxWidth: 991
|
|
15
26
|
},
|
|
16
27
|
mobile: {
|
|
17
28
|
title: "Mobile",
|
|
18
29
|
description: "Styles added here will apply at 430px and below.",
|
|
19
|
-
icon:
|
|
30
|
+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
31
|
+
<g clip-path="url(#clip0_6405_34405)">
|
|
32
|
+
<path d="M11.333 0.673293L4.66634 0.666626C3.93301 0.666626 3.33301 1.26663 3.33301 1.99996V14C3.33301 14.7333 3.93301 15.3333 4.66634 15.3333H11.333C12.0663 15.3333 12.6663 14.7333 12.6663 14V1.99996C12.6663 1.26663 12.0663 0.673293 11.333 0.673293ZM11.333 12.6666H4.66634V3.33329H11.333V12.6666Z" fill="#9DA4B0"/>
|
|
33
|
+
</g>
|
|
34
|
+
<defs>
|
|
35
|
+
<clipPath id="clip0_6405_34405">
|
|
36
|
+
<rect width="16" height="16" fill="white"/>
|
|
37
|
+
</clipPath>
|
|
38
|
+
</defs>
|
|
39
|
+
</svg>`,
|
|
20
40
|
minWidth: 0,
|
|
21
41
|
maxWidth: 430
|
|
22
42
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaultBreakpoints.js","sources":["../src/defaultBreakpoints.ts"],"sourcesContent":["import type { WebsiteBuilderThemeInput } from \"~/types/WebsiteBuilderTheme.js\";\n\nexport const defaultBreakpoints: NonNullable<WebsiteBuilderThemeInput[\"breakpoints\"]> = {\n desktop: {\n title: \"Desktop\",\n description: `Desktop styles apply at all breakpoints, unless they're edited at a lower breakpoint. Start your styling here.`,\n icon: `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"
|
|
1
|
+
{"version":3,"file":"defaultBreakpoints.js","sources":["../src/defaultBreakpoints.ts"],"sourcesContent":["import type { WebsiteBuilderThemeInput } from \"~/types/WebsiteBuilderTheme.js\";\n\nexport const defaultBreakpoints: NonNullable<WebsiteBuilderThemeInput[\"breakpoints\"]> = {\n desktop: {\n title: \"Desktop\",\n description: `Desktop styles apply at all breakpoints, unless they're edited at a lower breakpoint. Start your styling here.`,\n icon: `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <path d=\"M13.3333 12C14.0667 12 14.6667 11.4 14.6667 10.6666V3.99996C14.6667 3.26663 14.0667 2.66663 13.3333 2.66663H2.66667C1.93333 2.66663 1.33333 3.26663 1.33333 3.99996V10.6666C1.33333 11.4 1.93333 12 2.66667 12H0V13.3333H16V12H13.3333ZM2.66667 3.99996H13.3333V10.6666H2.66667V3.99996Z\" fill=\"#59626D\"/>\n</svg>`,\n minWidth: 0,\n maxWidth: 4000\n },\n tablet: {\n title: \"Tablet\",\n description: `Styles added here will apply at 991px and below, unless they're edited at a smaller breakpoint.`,\n icon: `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <g clip-path=\"url(#clip0_6424_8826)\">\n <path d=\"M12.666 0H3.33268C2.41268 0 1.66602 0.746667 1.66602 1.66667V14.3333C1.66602 15.2533 2.41268 16 3.33268 16H12.666C13.586 16 14.3327 15.2533 14.3327 14.3333V1.66667C14.3327 0.746667 13.586 0 12.666 0ZM7.99935 15.3333C7.44602 15.3333 6.99935 14.8867 6.99935 14.3333C6.99935 13.78 7.44602 13.3333 7.99935 13.3333C8.55268 13.3333 8.99935 13.78 8.99935 14.3333C8.99935 14.8867 8.55268 15.3333 7.99935 15.3333ZM12.9993 12.6667H2.99935V2H12.9993V12.6667Z\" fill=\"#9DA4B0\"/>\n </g>\n <defs>\n <clipPath id=\"clip0_6424_8826\">\n <rect width=\"16\" height=\"16\" fill=\"white\"/>\n </clipPath>\n </defs>\n</svg>`,\n minWidth: 0,\n maxWidth: 991\n },\n mobile: {\n title: \"Mobile\",\n description: `Styles added here will apply at 430px and below.`,\n icon: `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n <g clip-path=\"url(#clip0_6405_34405)\">\n <path d=\"M11.333 0.673293L4.66634 0.666626C3.93301 0.666626 3.33301 1.26663 3.33301 1.99996V14C3.33301 14.7333 3.93301 15.3333 4.66634 15.3333H11.333C12.0663 15.3333 12.6663 14.7333 12.6663 14V1.99996C12.6663 1.26663 12.0663 0.673293 11.333 0.673293ZM11.333 12.6666H4.66634V3.33329H11.333V12.6666Z\" fill=\"#9DA4B0\"/>\n </g>\n <defs>\n <clipPath id=\"clip0_6405_34405\">\n <rect width=\"16\" height=\"16\" fill=\"white\"/>\n </clipPath>\n </defs>\n</svg>`,\n minWidth: 0,\n maxWidth: 430\n }\n};\n"],"names":["defaultBreakpoints"],"mappings":"AAEO,MAAMA,qBAA2E;IACpF,SAAS;QACL,OAAO;QACP,aAAa;QACb,MAAM,CAAC;;MAET,CAAC;QACC,UAAU;QACV,UAAU;IACd;IACA,QAAQ;QACJ,OAAO;QACP,aAAa;QACb,MAAM,CAAC;;;;;;;;;MAST,CAAC;QACC,UAAU;QACV,UAAU;IACd;IACA,QAAQ;QACJ,OAAO;QACP,aAAa;QACb,MAAM,CAAC;;;;;;;;;MAST,CAAC;QACC,UAAU;QACV,UAAU;IACd;AACJ"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AnalyticsProvider, ExposureEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The minimal shape of a PostHog client (server-side `posthog-node` or browser `posthog-js`).
|
|
4
|
+
* The project supplies its own client so this package stays dependency-free and framework
|
|
5
|
+
* agnostic — no PostHog package is bundled into the SDK.
|
|
6
|
+
*/
|
|
7
|
+
export interface PostHogClientLike {
|
|
8
|
+
capture(params: {
|
|
9
|
+
distinctId: string;
|
|
10
|
+
event: string;
|
|
11
|
+
properties?: Record<string, any>;
|
|
12
|
+
}): void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface PostHogAnalyticsProviderOptions {
|
|
15
|
+
/** Event name to emit. Defaults to "$experiment_exposure". */
|
|
16
|
+
exposureEventName?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Maps the canonical {@link ExposureEvent} onto a PostHog capture call. PostHog attributes
|
|
20
|
+
* experiment results from the `$feature/<experimentKey>` property carried on the exposure
|
|
21
|
+
* event (and, project-side, on subsequent metric events) — no native PostHog feature flag is
|
|
22
|
+
* required. This is the ONLY place PostHog-specific field names exist; they never reach the
|
|
23
|
+
* data model, the assignment logic, or the render path.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PostHogAnalyticsProvider implements AnalyticsProvider {
|
|
26
|
+
private client;
|
|
27
|
+
private options;
|
|
28
|
+
readonly name = "posthog";
|
|
29
|
+
constructor(client: PostHogClientLike, options?: PostHogAnalyticsProviderOptions);
|
|
30
|
+
emitExposure(event: ExposureEvent): Promise<void>;
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class PostHogAnalyticsProvider {
|
|
2
|
+
constructor(client, options = {}){
|
|
3
|
+
this.client = client;
|
|
4
|
+
this.options = options;
|
|
5
|
+
this.name = "posthog";
|
|
6
|
+
}
|
|
7
|
+
async emitExposure(event) {
|
|
8
|
+
const featureKey = event.experimentKey ?? event.experimentId;
|
|
9
|
+
await this.client.capture({
|
|
10
|
+
distinctId: event.visitorId,
|
|
11
|
+
event: this.options.exposureEventName ?? "$experiment_exposure",
|
|
12
|
+
properties: {
|
|
13
|
+
[`$feature/${featureKey}`]: event.variantId,
|
|
14
|
+
experiment_id: event.experimentId,
|
|
15
|
+
variant_id: event.variantId,
|
|
16
|
+
revision_id: event.revisionId,
|
|
17
|
+
tenant_id: event.tenantId,
|
|
18
|
+
page_id: event.pageId,
|
|
19
|
+
path: event.path,
|
|
20
|
+
$current_url: event.path,
|
|
21
|
+
timestamp: event.timestamp
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export { PostHogAnalyticsProvider };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=PostHogAnalyticsProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments/PostHogAnalyticsProvider.js","sources":["../../src/experiments/PostHogAnalyticsProvider.ts"],"sourcesContent":["import type { AnalyticsProvider, ExposureEvent } from \"./types.js\";\n\n/**\n * The minimal shape of a PostHog client (server-side `posthog-node` or browser `posthog-js`).\n * The project supplies its own client so this package stays dependency-free and framework\n * agnostic — no PostHog package is bundled into the SDK.\n */\nexport interface PostHogClientLike {\n capture(params: {\n distinctId: string;\n event: string;\n properties?: Record<string, any>;\n }): void | Promise<void>;\n}\n\nexport interface PostHogAnalyticsProviderOptions {\n /** Event name to emit. Defaults to \"$experiment_exposure\". */\n exposureEventName?: string;\n}\n\n/**\n * Maps the canonical {@link ExposureEvent} onto a PostHog capture call. PostHog attributes\n * experiment results from the `$feature/<experimentKey>` property carried on the exposure\n * event (and, project-side, on subsequent metric events) — no native PostHog feature flag is\n * required. This is the ONLY place PostHog-specific field names exist; they never reach the\n * data model, the assignment logic, or the render path.\n */\nexport class PostHogAnalyticsProvider implements AnalyticsProvider {\n public readonly name = \"posthog\";\n\n constructor(\n private client: PostHogClientLike,\n private options: PostHogAnalyticsProviderOptions = {}\n ) {}\n\n async emitExposure(event: ExposureEvent): Promise<void> {\n const featureKey = event.experimentKey ?? event.experimentId;\n\n await this.client.capture({\n distinctId: event.visitorId,\n event: this.options.exposureEventName ?? \"$experiment_exposure\",\n properties: {\n // PostHog reads this property to attribute exposures and metrics to a variant.\n [`$feature/${featureKey}`]: event.variantId,\n experiment_id: event.experimentId,\n variant_id: event.variantId,\n revision_id: event.revisionId,\n tenant_id: event.tenantId,\n page_id: event.pageId,\n path: event.path,\n $current_url: event.path,\n timestamp: event.timestamp\n }\n });\n }\n}\n"],"names":["PostHogAnalyticsProvider","client","options","event","featureKey"],"mappings":"AA2BO,MAAMA;IAGT,YACYC,MAAyB,EACzBC,UAA2C,CAAC,CAAC,CACvD;aAFUD,MAAM,GAANA;aACAC,OAAO,GAAPA;aAJI,IAAI,GAAG;IAKpB;IAEH,MAAM,aAAaC,KAAoB,EAAiB;QACpD,MAAMC,aAAaD,MAAM,aAAa,IAAIA,MAAM,YAAY;QAE5D,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;YACtB,YAAYA,MAAM,SAAS;YAC3B,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,IAAI;YACzC,YAAY;gBAER,CAAC,CAAC,SAAS,EAAEC,YAAY,CAAC,EAAED,MAAM,SAAS;gBAC3C,eAAeA,MAAM,YAAY;gBACjC,YAAYA,MAAM,SAAS;gBAC3B,aAAaA,MAAM,UAAU;gBAC7B,WAAWA,MAAM,QAAQ;gBACzB,SAASA,MAAM,MAAM;gBACrB,MAAMA,MAAM,IAAI;gBAChB,cAAcA,MAAM,IAAI;gBACxB,WAAWA,MAAM,SAAS;YAC9B;QACJ;IACJ;AACJ"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AnalyticsProvider } from "./types.js";
|
|
2
|
+
export declare const registerAnalyticsProvider: (provider: AnalyticsProvider) => void;
|
|
3
|
+
export declare const getAnalyticsProvider: (name: string) => AnalyticsProvider | undefined;
|
|
4
|
+
export declare const clearAnalyticsProviders: () => void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const providers = new Map();
|
|
2
|
+
const registerAnalyticsProvider = (provider)=>{
|
|
3
|
+
providers.set(provider.name, provider);
|
|
4
|
+
};
|
|
5
|
+
const getAnalyticsProvider = (name)=>providers.get(name);
|
|
6
|
+
const clearAnalyticsProviders = ()=>{
|
|
7
|
+
providers.clear();
|
|
8
|
+
};
|
|
9
|
+
export { clearAnalyticsProviders, getAnalyticsProvider, registerAnalyticsProvider };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=analyticsProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments/analyticsProvider.js","sources":["../../src/experiments/analyticsProvider.ts"],"sourcesContent":["import type { AnalyticsProvider } from \"./types.js\";\n\n/**\n * A small registry mapping a provider id (e.g. \"posthog\") to its adapter. The render path\n * looks the provider up by the experiment's analytics.provider value, so adding a GA4 adapter\n * later requires no change to assignment or the render path.\n */\nconst providers = new Map<string, AnalyticsProvider>();\n\nexport const registerAnalyticsProvider = (provider: AnalyticsProvider): void => {\n providers.set(provider.name, provider);\n};\n\nexport const getAnalyticsProvider = (name: string): AnalyticsProvider | undefined => {\n return providers.get(name);\n};\n\nexport const clearAnalyticsProviders = (): void => {\n providers.clear();\n};\n"],"names":["providers","Map","registerAnalyticsProvider","provider","getAnalyticsProvider","name","clearAnalyticsProviders"],"mappings":"AAOA,MAAMA,YAAY,IAAIC;AAEf,MAAMC,4BAA4B,CAACC;IACtCH,UAAU,GAAG,CAACG,SAAS,IAAI,EAAEA;AACjC;AAEO,MAAMC,uBAAuB,CAACC,OAC1BL,UAAU,GAAG,CAACK;AAGlB,MAAMC,0BAA0B;IACnCN,UAAU,KAAK;AACnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ActiveExperiment, ExperimentTargeting, VariantAssignment, VisitorContext } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* cyrb53 — a fast, well-distributed non-cryptographic string hash. Deterministic across
|
|
4
|
+
* runtimes, which is what makes server-side bucketing stable for a given visitor + experiment.
|
|
5
|
+
*/
|
|
6
|
+
export declare const cyrb53: (str: string, seed?: number) => number;
|
|
7
|
+
/** Hash a seed to a stable number in the half-open interval [0, 1). */
|
|
8
|
+
export declare const hashToUnit: (seed: string) => number;
|
|
9
|
+
/**
|
|
10
|
+
* Whether a visitor matches the experiment's geo/device targeting. Tenant scoping is implicit:
|
|
11
|
+
* the experiment id is unique within a tenant, so a visitor's bucket never crosses tenants.
|
|
12
|
+
*/
|
|
13
|
+
export declare const matchesTargeting: (targeting: ExperimentTargeting, ctx: VisitorContext) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Deterministically assign a visitor to the control bucket or a variant.
|
|
16
|
+
*
|
|
17
|
+
* 1. If geo/device targeting excludes the visitor → control, excluded (no exposure).
|
|
18
|
+
* 2. If the visitor falls outside the traffic percentage → control, excluded (no exposure).
|
|
19
|
+
* 3. Otherwise bucket among the control + variants by their weights, seeded by
|
|
20
|
+
* visitorId + experimentId so the result is stable and tenant-isolated.
|
|
21
|
+
*/
|
|
22
|
+
export declare const assignVariant: (experiment: ActiveExperiment, ctx: VisitorContext) => VariantAssignment;
|
|
23
|
+
/**
|
|
24
|
+
* A forced assignment for QA/review. Overrides bucketing and is never counted in results.
|
|
25
|
+
* Returns null when the forced id does not match the control or any ready variant.
|
|
26
|
+
*/
|
|
27
|
+
export declare const forcedAssignment: (experiment: ActiveExperiment, forcedVariantId: string) => VariantAssignment | null;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const cyrb53 = (str, seed = 0)=>{
|
|
2
|
+
let h1 = 0xdeadbeef ^ seed;
|
|
3
|
+
let h2 = 0x41c6ce57 ^ seed;
|
|
4
|
+
for(let i = 0; i < str.length; i++){
|
|
5
|
+
const ch = str.charCodeAt(i);
|
|
6
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
7
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
8
|
+
}
|
|
9
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
10
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
11
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
12
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
13
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
14
|
+
};
|
|
15
|
+
const hashToUnit = (seed)=>cyrb53(seed) / 9007199254740992;
|
|
16
|
+
const matchesTargeting = (targeting, ctx)=>{
|
|
17
|
+
if (targeting.geo && targeting.geo.length > 0) {
|
|
18
|
+
if (!ctx.country || !targeting.geo.includes(ctx.country)) return false;
|
|
19
|
+
}
|
|
20
|
+
if (targeting.device && targeting.device.length > 0) {
|
|
21
|
+
if (!ctx.device || !targeting.device.includes(ctx.device)) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
const assignVariant = (experiment, ctx)=>{
|
|
26
|
+
const controlVariantId = experiment.controlVariantId;
|
|
27
|
+
const control = {
|
|
28
|
+
variantId: controlVariantId,
|
|
29
|
+
isControl: true,
|
|
30
|
+
excluded: true,
|
|
31
|
+
forced: false
|
|
32
|
+
};
|
|
33
|
+
if (!matchesTargeting(experiment.targeting, ctx)) return control;
|
|
34
|
+
const trafficPercentage = experiment.targeting.trafficPercentage ?? 100;
|
|
35
|
+
const inclusion = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}:inclusion`);
|
|
36
|
+
if (inclusion >= trafficPercentage / 100) return control;
|
|
37
|
+
const readyVariantIds = new Set(experiment.variants.map((variant)=>variant.variantId));
|
|
38
|
+
const buckets = [
|
|
39
|
+
{
|
|
40
|
+
id: controlVariantId,
|
|
41
|
+
weight: Math.max(0, experiment.trafficSplit.control)
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
for (const [variantId, weight] of Object.entries(experiment.trafficSplit.variants))if (readyVariantIds.has(variantId) && weight > 0) buckets.push({
|
|
45
|
+
id: variantId,
|
|
46
|
+
weight
|
|
47
|
+
});
|
|
48
|
+
const totalWeight = buckets.reduce((sum, bucket)=>sum + bucket.weight, 0);
|
|
49
|
+
if (totalWeight <= 0) return {
|
|
50
|
+
variantId: controlVariantId,
|
|
51
|
+
isControl: true,
|
|
52
|
+
excluded: false,
|
|
53
|
+
forced: false
|
|
54
|
+
};
|
|
55
|
+
let roll = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}`) * totalWeight;
|
|
56
|
+
for (const bucket of buckets){
|
|
57
|
+
roll -= bucket.weight;
|
|
58
|
+
if (roll < 0) return {
|
|
59
|
+
variantId: bucket.id,
|
|
60
|
+
isControl: bucket.id === controlVariantId,
|
|
61
|
+
excluded: false,
|
|
62
|
+
forced: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const last = buckets[buckets.length - 1];
|
|
66
|
+
return {
|
|
67
|
+
variantId: last.id,
|
|
68
|
+
isControl: last.id === controlVariantId,
|
|
69
|
+
excluded: false,
|
|
70
|
+
forced: false
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const forcedAssignment = (experiment, forcedVariantId)=>{
|
|
74
|
+
if (forcedVariantId === experiment.controlVariantId) return {
|
|
75
|
+
variantId: forcedVariantId,
|
|
76
|
+
isControl: true,
|
|
77
|
+
excluded: false,
|
|
78
|
+
forced: true
|
|
79
|
+
};
|
|
80
|
+
const isReady = experiment.variants.some((variant)=>variant.variantId === forcedVariantId);
|
|
81
|
+
if (!isReady) return null;
|
|
82
|
+
return {
|
|
83
|
+
variantId: forcedVariantId,
|
|
84
|
+
isControl: false,
|
|
85
|
+
excluded: false,
|
|
86
|
+
forced: true
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
export { assignVariant, cyrb53, forcedAssignment, hashToUnit, matchesTargeting };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=bucketing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments/bucketing.js","sources":["../../src/experiments/bucketing.ts"],"sourcesContent":["import type {\n ActiveExperiment,\n ExperimentTargeting,\n VariantAssignment,\n VisitorContext\n} from \"./types.js\";\n\n/**\n * cyrb53 — a fast, well-distributed non-cryptographic string hash. Deterministic across\n * runtimes, which is what makes server-side bucketing stable for a given visitor + experiment.\n */\nexport const cyrb53 = (str: string, seed = 0): number => {\n let h1 = 0xdeadbeef ^ seed;\n let h2 = 0x41c6ce57 ^ seed;\n for (let i = 0; i < str.length; i++) {\n const ch = str.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return 4294967296 * (2097151 & h2) + (h1 >>> 0);\n};\n\n/** Hash a seed to a stable number in the half-open interval [0, 1). */\nexport const hashToUnit = (seed: string): number => {\n return cyrb53(seed) / 9007199254740992;\n};\n\n/**\n * Whether a visitor matches the experiment's geo/device targeting. Tenant scoping is implicit:\n * the experiment id is unique within a tenant, so a visitor's bucket never crosses tenants.\n */\nexport const matchesTargeting = (targeting: ExperimentTargeting, ctx: VisitorContext): boolean => {\n if (targeting.geo && targeting.geo.length > 0) {\n if (!ctx.country || !targeting.geo.includes(ctx.country)) {\n return false;\n }\n }\n if (targeting.device && targeting.device.length > 0) {\n if (!ctx.device || !targeting.device.includes(ctx.device)) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Deterministically assign a visitor to the control bucket or a variant.\n *\n * 1. If geo/device targeting excludes the visitor → control, excluded (no exposure).\n * 2. If the visitor falls outside the traffic percentage → control, excluded (no exposure).\n * 3. Otherwise bucket among the control + variants by their weights, seeded by\n * visitorId + experimentId so the result is stable and tenant-isolated.\n */\nexport const assignVariant = (\n experiment: ActiveExperiment,\n ctx: VisitorContext\n): VariantAssignment => {\n const controlVariantId = experiment.controlVariantId;\n const control: VariantAssignment = {\n variantId: controlVariantId,\n isControl: true,\n excluded: true,\n forced: false\n };\n\n if (!matchesTargeting(experiment.targeting, ctx)) {\n return control;\n }\n\n const trafficPercentage = experiment.targeting.trafficPercentage ?? 100;\n const inclusion = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}:inclusion`);\n if (inclusion >= trafficPercentage / 100) {\n return control;\n }\n\n // Build the bucket list: control first, then every variant that is both ready (present in\n // experiment.variants) and assigned a weight in the traffic split.\n const readyVariantIds = new Set(experiment.variants.map(variant => variant.variantId));\n const buckets: Array<{ id: string; weight: number }> = [\n { id: controlVariantId, weight: Math.max(0, experiment.trafficSplit.control) }\n ];\n for (const [variantId, weight] of Object.entries(experiment.trafficSplit.variants)) {\n if (readyVariantIds.has(variantId) && weight > 0) {\n buckets.push({ id: variantId, weight });\n }\n }\n\n const totalWeight = buckets.reduce((sum, bucket) => sum + bucket.weight, 0);\n if (totalWeight <= 0) {\n return { variantId: controlVariantId, isControl: true, excluded: false, forced: false };\n }\n\n let roll = hashToUnit(`${ctx.visitorId}:${experiment.experimentId}`) * totalWeight;\n for (const bucket of buckets) {\n roll -= bucket.weight;\n if (roll < 0) {\n return {\n variantId: bucket.id,\n isControl: bucket.id === controlVariantId,\n excluded: false,\n forced: false\n };\n }\n }\n\n // Floating-point edge: fall back to the last bucket.\n const last = buckets[buckets.length - 1];\n return {\n variantId: last.id,\n isControl: last.id === controlVariantId,\n excluded: false,\n forced: false\n };\n};\n\n/**\n * A forced assignment for QA/review. Overrides bucketing and is never counted in results.\n * Returns null when the forced id does not match the control or any ready variant.\n */\nexport const forcedAssignment = (\n experiment: ActiveExperiment,\n forcedVariantId: string\n): VariantAssignment | null => {\n if (forcedVariantId === experiment.controlVariantId) {\n return { variantId: forcedVariantId, isControl: true, excluded: false, forced: true };\n }\n const isReady = experiment.variants.some(variant => variant.variantId === forcedVariantId);\n if (!isReady) {\n return null;\n }\n return { variantId: forcedVariantId, isControl: false, excluded: false, forced: true };\n};\n"],"names":["cyrb53","str","seed","h1","h2","i","ch","Math","hashToUnit","matchesTargeting","targeting","ctx","assignVariant","experiment","controlVariantId","control","trafficPercentage","inclusion","readyVariantIds","Set","variant","buckets","variantId","weight","Object","totalWeight","sum","bucket","roll","last","forcedAssignment","forcedVariantId","isReady"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { assignVariant, forcedAssignment, hashToUnit, matchesTargeting } from "./bucketing.js";
|
|
3
|
+
import { CONTROL_VARIANT_ID } from "./types.js";
|
|
4
|
+
const experiment = (overrides = {})=>({
|
|
5
|
+
experimentId: "exp-1",
|
|
6
|
+
revisionId: "page-1#0001",
|
|
7
|
+
pageEntryId: "page-1",
|
|
8
|
+
path: "/pricing",
|
|
9
|
+
status: "running",
|
|
10
|
+
tenantId: "root",
|
|
11
|
+
controlVariantId: CONTROL_VARIANT_ID,
|
|
12
|
+
trafficSplit: {
|
|
13
|
+
control: 50,
|
|
14
|
+
variants: {
|
|
15
|
+
"variant-a": 50
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
targeting: {
|
|
19
|
+
trafficPercentage: 100
|
|
20
|
+
},
|
|
21
|
+
analytics: {
|
|
22
|
+
provider: "posthog"
|
|
23
|
+
},
|
|
24
|
+
variants: [
|
|
25
|
+
{
|
|
26
|
+
variantId: "variant-a",
|
|
27
|
+
name: "Variant A"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
...overrides
|
|
31
|
+
});
|
|
32
|
+
const visitor = (overrides = {})=>({
|
|
33
|
+
visitorId: "visitor-1",
|
|
34
|
+
...overrides
|
|
35
|
+
});
|
|
36
|
+
describe("hashToUnit", ()=>{
|
|
37
|
+
it("is deterministic and within [0, 1)", ()=>{
|
|
38
|
+
const a = hashToUnit("visitor-1:exp-1");
|
|
39
|
+
const b = hashToUnit("visitor-1:exp-1");
|
|
40
|
+
expect(a).toBe(b);
|
|
41
|
+
expect(a).toBeGreaterThanOrEqual(0);
|
|
42
|
+
expect(a).toBeLessThan(1);
|
|
43
|
+
});
|
|
44
|
+
it("differs across seeds", ()=>{
|
|
45
|
+
expect(hashToUnit("visitor-1:exp-1")).not.toBe(hashToUnit("visitor-2:exp-1"));
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("assignVariant", ()=>{
|
|
49
|
+
it("assigns the same visitor to the same bucket every time", ()=>{
|
|
50
|
+
const first = assignVariant(experiment(), visitor());
|
|
51
|
+
const second = assignVariant(experiment(), visitor());
|
|
52
|
+
expect(second).toEqual(first);
|
|
53
|
+
});
|
|
54
|
+
it("buckets the same visitor differently across experiments", ()=>{
|
|
55
|
+
const a = assignVariant(experiment({
|
|
56
|
+
experimentId: "exp-1"
|
|
57
|
+
}), visitor());
|
|
58
|
+
const b = assignVariant(experiment({
|
|
59
|
+
experimentId: "exp-2"
|
|
60
|
+
}), visitor());
|
|
61
|
+
expect(hashToUnit("visitor-1:exp-1")).not.toBe(hashToUnit("visitor-1:exp-2"));
|
|
62
|
+
expect([
|
|
63
|
+
a.variantId,
|
|
64
|
+
b.variantId
|
|
65
|
+
].every(Boolean)).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it("respects the traffic split distribution", ()=>{
|
|
68
|
+
const exp = experiment({
|
|
69
|
+
trafficSplit: {
|
|
70
|
+
control: 80,
|
|
71
|
+
variants: {
|
|
72
|
+
"variant-a": 20
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
let control = 0;
|
|
77
|
+
let variant = 0;
|
|
78
|
+
for(let i = 0; i < 5000; i++){
|
|
79
|
+
const assignment = assignVariant(exp, visitor({
|
|
80
|
+
visitorId: `v-${i}`
|
|
81
|
+
}));
|
|
82
|
+
if (assignment.isControl) control++;
|
|
83
|
+
else variant++;
|
|
84
|
+
}
|
|
85
|
+
const variantShare = variant / (control + variant);
|
|
86
|
+
expect(variantShare).toBeGreaterThan(0.15);
|
|
87
|
+
expect(variantShare).toBeLessThan(0.25);
|
|
88
|
+
});
|
|
89
|
+
it("excludes visitors outside the traffic percentage and serves control", ()=>{
|
|
90
|
+
const exp = experiment({
|
|
91
|
+
targeting: {
|
|
92
|
+
trafficPercentage: 0
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const assignment = assignVariant(exp, visitor());
|
|
96
|
+
expect(assignment.isControl).toBe(true);
|
|
97
|
+
expect(assignment.excluded).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it("excludes visitors who do not match geo targeting", ()=>{
|
|
100
|
+
const exp = experiment({
|
|
101
|
+
targeting: {
|
|
102
|
+
trafficPercentage: 100,
|
|
103
|
+
geo: [
|
|
104
|
+
"GB"
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const assignment = assignVariant(exp, visitor({
|
|
109
|
+
country: "US"
|
|
110
|
+
}));
|
|
111
|
+
expect(assignment.excluded).toBe(true);
|
|
112
|
+
expect(assignment.isControl).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it("ignores variants that are not ready", ()=>{
|
|
115
|
+
const exp = experiment({
|
|
116
|
+
trafficSplit: {
|
|
117
|
+
control: 0,
|
|
118
|
+
variants: {
|
|
119
|
+
"variant-a": 50,
|
|
120
|
+
"variant-ghost": 50
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
variants: [
|
|
124
|
+
{
|
|
125
|
+
variantId: "variant-a",
|
|
126
|
+
name: "Variant A"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
for(let i = 0; i < 200; i++){
|
|
131
|
+
const assignment = assignVariant(exp, visitor({
|
|
132
|
+
visitorId: `v-${i}`
|
|
133
|
+
}));
|
|
134
|
+
expect(assignment.variantId).not.toBe("variant-ghost");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("matchesTargeting", ()=>{
|
|
139
|
+
it("matches when no geo/device restriction is set", ()=>{
|
|
140
|
+
expect(matchesTargeting({
|
|
141
|
+
trafficPercentage: 100
|
|
142
|
+
}, visitor())).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
it("matches device targeting", ()=>{
|
|
145
|
+
expect(matchesTargeting({
|
|
146
|
+
trafficPercentage: 100,
|
|
147
|
+
device: [
|
|
148
|
+
"mobile"
|
|
149
|
+
]
|
|
150
|
+
}, visitor({
|
|
151
|
+
device: "mobile"
|
|
152
|
+
}))).toBe(true);
|
|
153
|
+
expect(matchesTargeting({
|
|
154
|
+
trafficPercentage: 100,
|
|
155
|
+
device: [
|
|
156
|
+
"mobile"
|
|
157
|
+
]
|
|
158
|
+
}, visitor({
|
|
159
|
+
device: "desktop"
|
|
160
|
+
}))).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe("forcedAssignment", ()=>{
|
|
164
|
+
it("forces a ready variant and marks it as not counted", ()=>{
|
|
165
|
+
const assignment = forcedAssignment(experiment(), "variant-a");
|
|
166
|
+
expect(assignment).toEqual({
|
|
167
|
+
variantId: "variant-a",
|
|
168
|
+
isControl: false,
|
|
169
|
+
excluded: false,
|
|
170
|
+
forced: true
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it("forces the control bucket", ()=>{
|
|
174
|
+
const assignment = forcedAssignment(experiment(), CONTROL_VARIANT_ID);
|
|
175
|
+
expect(assignment?.isControl).toBe(true);
|
|
176
|
+
expect(assignment?.forced).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
it("returns null for an unknown variant", ()=>{
|
|
179
|
+
expect(forcedAssignment(experiment(), "nope")).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
//# sourceMappingURL=bucketing.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments/bucketing.test.js","sources":["../../src/experiments/bucketing.test.ts"],"sourcesContent":["import { describe, it, expect } from \"vitest\";\nimport { assignVariant, forcedAssignment, hashToUnit, matchesTargeting } from \"./bucketing.js\";\nimport { CONTROL_VARIANT_ID, type ActiveExperiment, type VisitorContext } from \"./types.js\";\n\nconst experiment = (overrides: Partial<ActiveExperiment> = {}): ActiveExperiment => ({\n experimentId: \"exp-1\",\n revisionId: \"page-1#0001\",\n pageEntryId: \"page-1\",\n path: \"/pricing\",\n status: \"running\",\n tenantId: \"root\",\n controlVariantId: CONTROL_VARIANT_ID,\n trafficSplit: { control: 50, variants: { \"variant-a\": 50 } },\n targeting: { trafficPercentage: 100 },\n analytics: { provider: \"posthog\" },\n variants: [{ variantId: \"variant-a\", name: \"Variant A\" }],\n ...overrides\n});\n\nconst visitor = (overrides: Partial<VisitorContext> = {}): VisitorContext => ({\n visitorId: \"visitor-1\",\n ...overrides\n});\n\ndescribe(\"hashToUnit\", () => {\n it(\"is deterministic and within [0, 1)\", () => {\n const a = hashToUnit(\"visitor-1:exp-1\");\n const b = hashToUnit(\"visitor-1:exp-1\");\n expect(a).toBe(b);\n expect(a).toBeGreaterThanOrEqual(0);\n expect(a).toBeLessThan(1);\n });\n\n it(\"differs across seeds\", () => {\n expect(hashToUnit(\"visitor-1:exp-1\")).not.toBe(hashToUnit(\"visitor-2:exp-1\"));\n });\n});\n\ndescribe(\"assignVariant\", () => {\n it(\"assigns the same visitor to the same bucket every time\", () => {\n const first = assignVariant(experiment(), visitor());\n const second = assignVariant(experiment(), visitor());\n expect(second).toEqual(first);\n });\n\n it(\"buckets the same visitor differently across experiments\", () => {\n const a = assignVariant(experiment({ experimentId: \"exp-1\" }), visitor());\n const b = assignVariant(experiment({ experimentId: \"exp-2\" }), visitor());\n // Not guaranteed to differ, but the seed differs; assert the seeds are independent.\n expect(hashToUnit(\"visitor-1:exp-1\")).not.toBe(hashToUnit(\"visitor-1:exp-2\"));\n expect([a.variantId, b.variantId].every(Boolean)).toBe(true);\n });\n\n it(\"respects the traffic split distribution\", () => {\n const exp = experiment({\n trafficSplit: { control: 80, variants: { \"variant-a\": 20 } }\n });\n let control = 0;\n let variant = 0;\n for (let i = 0; i < 5000; i++) {\n const assignment = assignVariant(exp, visitor({ visitorId: `v-${i}` }));\n if (assignment.isControl) {\n control++;\n } else {\n variant++;\n }\n }\n const variantShare = variant / (control + variant);\n // Expect ~20% in the variant, allow a generous tolerance.\n expect(variantShare).toBeGreaterThan(0.15);\n expect(variantShare).toBeLessThan(0.25);\n });\n\n it(\"excludes visitors outside the traffic percentage and serves control\", () => {\n const exp = experiment({ targeting: { trafficPercentage: 0 } });\n const assignment = assignVariant(exp, visitor());\n expect(assignment.isControl).toBe(true);\n expect(assignment.excluded).toBe(true);\n });\n\n it(\"excludes visitors who do not match geo targeting\", () => {\n const exp = experiment({ targeting: { trafficPercentage: 100, geo: [\"GB\"] } });\n const assignment = assignVariant(exp, visitor({ country: \"US\" }));\n expect(assignment.excluded).toBe(true);\n expect(assignment.isControl).toBe(true);\n });\n\n it(\"ignores variants that are not ready\", () => {\n const exp = experiment({\n trafficSplit: { control: 0, variants: { \"variant-a\": 50, \"variant-ghost\": 50 } },\n variants: [{ variantId: \"variant-a\", name: \"Variant A\" }]\n });\n for (let i = 0; i < 200; i++) {\n const assignment = assignVariant(exp, visitor({ visitorId: `v-${i}` }));\n expect(assignment.variantId).not.toBe(\"variant-ghost\");\n }\n });\n});\n\ndescribe(\"matchesTargeting\", () => {\n it(\"matches when no geo/device restriction is set\", () => {\n expect(matchesTargeting({ trafficPercentage: 100 }, visitor())).toBe(true);\n });\n\n it(\"matches device targeting\", () => {\n expect(\n matchesTargeting(\n { trafficPercentage: 100, device: [\"mobile\"] },\n visitor({ device: \"mobile\" })\n )\n ).toBe(true);\n expect(\n matchesTargeting(\n { trafficPercentage: 100, device: [\"mobile\"] },\n visitor({ device: \"desktop\" })\n )\n ).toBe(false);\n });\n});\n\ndescribe(\"forcedAssignment\", () => {\n it(\"forces a ready variant and marks it as not counted\", () => {\n const assignment = forcedAssignment(experiment(), \"variant-a\");\n expect(assignment).toEqual({\n variantId: \"variant-a\",\n isControl: false,\n excluded: false,\n forced: true\n });\n });\n\n it(\"forces the control bucket\", () => {\n const assignment = forcedAssignment(experiment(), CONTROL_VARIANT_ID);\n expect(assignment?.isControl).toBe(true);\n expect(assignment?.forced).toBe(true);\n });\n\n it(\"returns null for an unknown variant\", () => {\n expect(forcedAssignment(experiment(), \"nope\")).toBeNull();\n });\n});\n"],"names":["experiment","overrides","CONTROL_VARIANT_ID","visitor","describe","it","a","hashToUnit","b","expect","first","assignVariant","second","Boolean","exp","control","variant","i","assignment","variantShare","matchesTargeting","forcedAssignment"],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { PublicPage } from "../types.js";
|
|
2
|
+
import type { HeadersProvider } from "../headersProvider.js";
|
|
3
|
+
import type { ActiveExperiment, DeviceType, VariantAssignment, VariantContent, VisitorContext } from "./types.js";
|
|
4
|
+
/** The cookie that persists the stable visitor id used for deterministic bucketing. */
|
|
5
|
+
export declare const DEFAULT_VISITOR_COOKIE = "wb_ab_vid";
|
|
6
|
+
/** Default query parameter used to force a variant for QA/review. */
|
|
7
|
+
export declare const FORCED_VARIANT_PARAM = "wb-variant";
|
|
8
|
+
/** The minimal data surface the render helper needs. ContentSdk satisfies this. */
|
|
9
|
+
export interface ExperimentSdk {
|
|
10
|
+
getPage(path: string): Promise<PublicPage | null>;
|
|
11
|
+
getPageExperiment(path: string): Promise<ActiveExperiment | null>;
|
|
12
|
+
getVariantContent(variantId: string): Promise<VariantContent | null>;
|
|
13
|
+
/** Runtime kill-switch check. When omitted, the pause state is not consulted. */
|
|
14
|
+
getExperimentPaused?(experimentId: string): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
export interface ExperimentCookie {
|
|
17
|
+
name: string;
|
|
18
|
+
value: string;
|
|
19
|
+
maxAge: number;
|
|
20
|
+
}
|
|
21
|
+
export interface GetPageWithExperimentOptions {
|
|
22
|
+
/** Forced variant id (from the `?wb-variant=` query parameter). Overrides bucketing, never counted. */
|
|
23
|
+
forcedVariantId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Request query parameters. When provided, the forced variant is read from
|
|
26
|
+
* `searchParams[FORCED_VARIANT_PARAM]` unless `forcedVariantId` is set explicitly. Convenient
|
|
27
|
+
* for framework handlers that already have the parsed query (e.g. Next.js `searchParams`).
|
|
28
|
+
*/
|
|
29
|
+
searchParams?: Record<string, string | string[] | undefined>;
|
|
30
|
+
/** Explicit visitor id. When omitted, it is read from the cookie or generated. */
|
|
31
|
+
visitorId?: string;
|
|
32
|
+
/** Explicit ISO country code. When omitted, it is read from common CDN geo headers. */
|
|
33
|
+
country?: string;
|
|
34
|
+
/** Explicit device type. When omitted, it is derived from the user agent. */
|
|
35
|
+
device?: DeviceType;
|
|
36
|
+
/** Header names to read the visitor country from, in priority order. */
|
|
37
|
+
geoHeaders?: string[];
|
|
38
|
+
/** Bucketing cookie name. Defaults to `wb_ab_vid`. */
|
|
39
|
+
cookieName?: string;
|
|
40
|
+
/** Bucketing cookie max age in seconds. Defaults to one year. */
|
|
41
|
+
cookieMaxAge?: number;
|
|
42
|
+
/** Set false to skip exposure emission (e.g. when consent has not been granted). */
|
|
43
|
+
emitExposure?: boolean;
|
|
44
|
+
/** Visitor id generator. Defaults to nanoid. */
|
|
45
|
+
generateVisitorId?: () => string;
|
|
46
|
+
/** Request headers to read cookies, geo and user agent from. Defaults to the global provider. */
|
|
47
|
+
headersProvider?: HeadersProvider;
|
|
48
|
+
}
|
|
49
|
+
export interface ExperimentRenderResult {
|
|
50
|
+
/** The content to render server-side: the baseline (control) or a variant overlay. */
|
|
51
|
+
page: PublicPage | null;
|
|
52
|
+
/** The active experiment, or null when there is none for the path. */
|
|
53
|
+
experiment: ActiveExperiment | null;
|
|
54
|
+
/** The visitor's assignment, or null when no experiment applies. */
|
|
55
|
+
assignment: VariantAssignment | null;
|
|
56
|
+
/**
|
|
57
|
+
* Set when the caller should persist the bucketing cookie (i.e. a visitor id was generated
|
|
58
|
+
* this request). Persisting it in a Route Handler / middleware keeps bucketing stable.
|
|
59
|
+
*/
|
|
60
|
+
cookie?: ExperimentCookie;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve request-time visitor context from request headers/cookies. Never uses a logged-in
|
|
64
|
+
* identity. Returns whether a visitor id was generated, so the caller can persist the cookie.
|
|
65
|
+
*/
|
|
66
|
+
export declare const resolveVisitorContext: (options?: GetPageWithExperimentOptions) => Promise<{
|
|
67
|
+
context: VisitorContext;
|
|
68
|
+
generatedVisitorId: boolean;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve and render the right page for a visitor, server-side, with no client-side content
|
|
72
|
+
* swap.
|
|
73
|
+
*
|
|
74
|
+
* Caching contract (important — handled here so projects do not reimplement it):
|
|
75
|
+
* - The baseline (control) is fetched via `getPage(path)` and caches per path.
|
|
76
|
+
* - The active experiment metadata is fetched via `getPageExperiment(path)` and caches per path.
|
|
77
|
+
* - A variant's content is fetched via `getVariantContent(variantId)` and caches per variant id.
|
|
78
|
+
* Every request therefore resolves to one of a small, fixed set of cacheable objects keyed by
|
|
79
|
+
* (path) or (variant id) — NEVER by the raw visitor id. Each active variant adds exactly one
|
|
80
|
+
* cacheable object per URL.
|
|
81
|
+
*/
|
|
82
|
+
export declare const getPageWithExperiment: (sdk: ExperimentSdk, path: string, options?: GetPageWithExperimentOptions) => Promise<ExperimentRenderResult>;
|