@funnelsgrove/runtime 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/components/FunnelContext.d.ts +5 -2
- package/dist/components/FunnelContext.js +3 -0
- package/dist/components/FunnelEditorPanel.d.ts +3 -5
- package/dist/components/FunnelEditorPanel.js +3 -3
- package/dist/components/ManageSubscriptionScreen.d.ts +51 -0
- package/dist/components/ManageSubscriptionScreen.js +349 -0
- package/dist/components/RuntimeDevInfoBox.d.ts +23 -0
- package/dist/components/RuntimeDevInfoBox.js +363 -0
- package/dist/components/SubscriptionHandoffScreen.d.ts +31 -0
- package/dist/components/SubscriptionHandoffScreen.js +338 -0
- package/dist/config/builder-preview.protocol.d.ts +73 -0
- package/dist/config/builder-preview.protocol.js +3 -0
- package/dist/config/env.config.d.ts +44 -0
- package/dist/config/env.config.js +161 -0
- package/dist/config/font-config.d.ts +14 -0
- package/dist/config/font-config.js +101 -0
- package/dist/config/funnel-theme.d.ts +61 -10
- package/dist/config/funnel-theme.js +355 -35
- package/dist/config/funnel.manifest.types.d.ts +13 -7
- package/dist/content/step-content.d.ts +130 -0
- package/dist/content/step-content.js +381 -0
- package/dist/index.d.ts +33 -21
- package/dist/index.js +33 -21
- package/dist/runtime/browser-helpers.d.ts +1 -0
- package/dist/runtime/browser-helpers.js +14 -0
- package/dist/runtime/experiment-assignment.d.ts +13 -4
- package/dist/runtime/experiment-assignment.js +9 -27
- package/dist/runtime/funnel-attribution.d.ts +18 -0
- package/dist/runtime/funnel-attribution.js +226 -0
- package/dist/runtime/funnel-flow.d.ts +9 -10
- package/dist/runtime/funnel-flow.js +4 -18
- package/dist/runtime/funnel-manifest.validation.d.ts +1 -1
- package/dist/runtime/funnel-manifest.validation.js +2 -6
- package/dist/runtime/funnel-runtime.d.ts +2 -3
- package/dist/runtime/funnel-runtime.js +6 -13
- package/dist/runtime/posthog-flags.d.ts +30 -0
- package/dist/runtime/posthog-flags.js +71 -0
- package/dist/runtime/preview-bridge.d.ts +13 -3
- package/dist/runtime/preview-bridge.js +96 -4
- package/dist/runtime/preview-definition-overrides.d.ts +20 -0
- package/dist/runtime/preview-definition-overrides.js +148 -0
- package/dist/runtime/route-resolver.d.ts +2 -3
- package/dist/runtime/route-resolver.js +15 -26
- package/dist/runtime/subscription-handoff.d.ts +32 -0
- package/dist/runtime/subscription-handoff.js +113 -0
- package/dist/runtime/use-funnel-flow-controller.d.ts +19 -10
- package/dist/runtime/use-funnel-flow-controller.js +190 -159
- package/dist/sdk/userAnswers.d.ts +2 -2
- package/dist/services/api.service.d.ts +21 -4
- package/dist/services/api.service.js +165 -35
- package/dist/services/funnel-state.service.d.ts +8 -0
- package/dist/services/funnel-state.service.js +44 -0
- package/dist/services/preview-frame.service.d.ts +2 -2
- package/dist/services/preview-frame.service.js +2 -2
- package/dist/services/public-env.d.ts +69 -0
- package/dist/services/public-env.js +105 -0
- package/dist/services/runtime-api.config.d.ts +5 -0
- package/dist/services/runtime-api.config.js +12 -7
- package/dist/services/runtime-mode.service.d.ts +3 -0
- package/dist/services/runtime-mode.service.js +142 -4
- package/package.json +8 -2
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export type ManifestVariant = {
|
|
2
|
+
variantKey: string;
|
|
3
|
+
routeToStepId: string;
|
|
4
|
+
};
|
|
5
|
+
export type ResolveInput = {
|
|
6
|
+
experimentId: string;
|
|
7
|
+
variants: readonly ManifestVariant[];
|
|
8
|
+
};
|
|
9
|
+
export type Assignment = {
|
|
10
|
+
variantKey: string;
|
|
11
|
+
routeToStepId: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const resolveExperimentAssignment: (input: ResolveInput) => Assignment | null;
|
|
@@ -1,32 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
4
|
-
hash = (hash * 31 + input.charCodeAt(index)) % 104729;
|
|
5
|
-
}
|
|
6
|
-
return Math.abs(hash % 100);
|
|
7
|
-
};
|
|
8
|
-
export const getExperimentAssignmentStorageKey = (funnelId, userId, experimentId) => {
|
|
9
|
-
return `funnel:experiment:${funnelId}:${userId}:${experimentId}`;
|
|
10
|
-
};
|
|
11
|
-
export const getExperimentAssignmentAttributeKey = (experimentId) => {
|
|
12
|
-
return `experiment.${experimentId}.variant`;
|
|
13
|
-
};
|
|
14
|
-
export const chooseAssignedVariantId = (experiment, userId, existingVariantId) => {
|
|
1
|
+
import { resolveExperimentVariant } from './posthog-flags.js';
|
|
2
|
+
export const resolveExperimentAssignment = (input) => {
|
|
15
3
|
var _a;
|
|
16
|
-
if (
|
|
4
|
+
if (input.variants.length === 0) {
|
|
17
5
|
return null;
|
|
18
6
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const bucket = hashToBucket(`${experiment.experimentId}:${userId}`);
|
|
24
|
-
let cumulative = 0;
|
|
25
|
-
for (const variant of experiment.variants) {
|
|
26
|
-
cumulative += Number.isFinite(variant.trafficPercent) ? variant.trafficPercent : 0;
|
|
27
|
-
if (bucket < cumulative) {
|
|
28
|
-
return variant.id;
|
|
29
|
-
}
|
|
7
|
+
const resolved = resolveExperimentVariant(input.experimentId);
|
|
8
|
+
const match = resolved ? input.variants.find((variant) => variant.variantKey === resolved) : undefined;
|
|
9
|
+
if (match) {
|
|
10
|
+
return { variantKey: match.variantKey, routeToStepId: match.routeToStepId };
|
|
30
11
|
}
|
|
31
|
-
|
|
12
|
+
const control = (_a = input.variants.find((variant) => variant.variantKey === 'control')) !== null && _a !== void 0 ? _a : input.variants[0];
|
|
13
|
+
return { variantKey: control.variantKey, routeToStepId: control.routeToStepId };
|
|
32
14
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type FunnelUserTouch = Record<string, string>;
|
|
2
|
+
export type FunnelUserAttribution = {
|
|
3
|
+
firstTouch: FunnelUserTouch;
|
|
4
|
+
lastTouch: FunnelUserTouch;
|
|
5
|
+
context: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export declare const collectCurrentFunnelAttribution: (input?: {
|
|
8
|
+
href?: string | null;
|
|
9
|
+
referrer?: string | null;
|
|
10
|
+
userAgent?: string | null;
|
|
11
|
+
language?: string | null;
|
|
12
|
+
platform?: string | null;
|
|
13
|
+
vendor?: string | null;
|
|
14
|
+
timeZone?: string | null;
|
|
15
|
+
posthogProperties?: Record<string, unknown> | null;
|
|
16
|
+
}) => FunnelUserAttribution;
|
|
17
|
+
export declare const mergeFunnelUserAttribution: (current: unknown, incoming: FunnelUserAttribution | null | undefined) => FunnelUserAttribution;
|
|
18
|
+
export declare const getFunnelUserAttribution: (document: unknown) => FunnelUserAttribution | null;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { getPostHog } from './posthog-flags.js';
|
|
2
|
+
const INTERNAL_ATTRIBUTION_QUERY_KEYS = new Set([
|
|
3
|
+
'editor',
|
|
4
|
+
'previewframe',
|
|
5
|
+
'source',
|
|
6
|
+
'step',
|
|
7
|
+
'user_id',
|
|
8
|
+
]);
|
|
9
|
+
const POSTHOG_CONTEXT_MAPPINGS = {
|
|
10
|
+
$geoip_country_code: 'countryCode',
|
|
11
|
+
$geoip_country_name: 'countryName',
|
|
12
|
+
$browser: 'browser',
|
|
13
|
+
$browser_version: 'browserVersion',
|
|
14
|
+
$os: 'os',
|
|
15
|
+
$os_version: 'osVersion',
|
|
16
|
+
$device_type: 'deviceType',
|
|
17
|
+
$referrer: 'referrer',
|
|
18
|
+
$referring_domain: 'referringDomain',
|
|
19
|
+
};
|
|
20
|
+
const asTrimmedString = (value) => {
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed || null;
|
|
26
|
+
};
|
|
27
|
+
const isRecord = (value) => {
|
|
28
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
};
|
|
30
|
+
const normalizeTouch = (value) => {
|
|
31
|
+
if (!isRecord(value)) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, candidate]) => {
|
|
35
|
+
const normalizedKey = key.trim();
|
|
36
|
+
const normalizedValue = asTrimmedString(candidate);
|
|
37
|
+
if (!normalizedKey || !normalizedValue) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return [[normalizedKey, normalizedValue]];
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
const normalizeContext = (value) => {
|
|
44
|
+
if (!isRecord(value)) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key, candidate]) => {
|
|
48
|
+
const normalizedKey = key.trim();
|
|
49
|
+
if (!normalizedKey || candidate === undefined || candidate === null || candidate === '') {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return [[normalizedKey, candidate]];
|
|
53
|
+
}));
|
|
54
|
+
};
|
|
55
|
+
const deepMergeRecords = (base, patch) => {
|
|
56
|
+
const next = Object.assign({}, base);
|
|
57
|
+
for (const [key, patchValue] of Object.entries(patch)) {
|
|
58
|
+
const currentValue = next[key];
|
|
59
|
+
if (isRecord(currentValue) && isRecord(patchValue)) {
|
|
60
|
+
next[key] = deepMergeRecords(currentValue, patchValue);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
next[key] = patchValue;
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
};
|
|
67
|
+
const resolveDeviceType = (userAgent) => {
|
|
68
|
+
if (!userAgent) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const normalized = userAgent.toLowerCase();
|
|
72
|
+
if (/iphone|android.+mobile|ipod|mobile/.test(normalized)) {
|
|
73
|
+
return 'mobile';
|
|
74
|
+
}
|
|
75
|
+
if (/ipad|tablet/.test(normalized)) {
|
|
76
|
+
return 'tablet';
|
|
77
|
+
}
|
|
78
|
+
return 'desktop';
|
|
79
|
+
};
|
|
80
|
+
const resolveOsFamily = (userAgent) => {
|
|
81
|
+
if (!userAgent) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const normalized = userAgent.toLowerCase();
|
|
85
|
+
if (/iphone|ipad|ipod|ios/.test(normalized)) {
|
|
86
|
+
return 'ios';
|
|
87
|
+
}
|
|
88
|
+
if (/android/.test(normalized)) {
|
|
89
|
+
return 'android';
|
|
90
|
+
}
|
|
91
|
+
if (/mac os x|macintosh/.test(normalized)) {
|
|
92
|
+
return 'macos';
|
|
93
|
+
}
|
|
94
|
+
if (/windows/.test(normalized)) {
|
|
95
|
+
return 'windows';
|
|
96
|
+
}
|
|
97
|
+
if (/linux/.test(normalized)) {
|
|
98
|
+
return 'linux';
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
const toUrl = (value) => {
|
|
103
|
+
const normalized = asTrimmedString(value);
|
|
104
|
+
if (!normalized) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return new URL(normalized);
|
|
109
|
+
}
|
|
110
|
+
catch (_a) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const collectTouchFromUrl = (url) => {
|
|
115
|
+
if (!url) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
const nextTouch = {};
|
|
119
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
120
|
+
const normalizedKey = key.trim();
|
|
121
|
+
const normalizedValue = value.trim();
|
|
122
|
+
if (!normalizedKey || !normalizedValue) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (INTERNAL_ATTRIBUTION_QUERY_KEYS.has(normalizedKey.toLowerCase())) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
nextTouch[normalizedKey] = normalizedValue;
|
|
129
|
+
}
|
|
130
|
+
return nextTouch;
|
|
131
|
+
};
|
|
132
|
+
const collectPostHogContext = (posthogProperties) => {
|
|
133
|
+
if (!posthogProperties) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
return Object.fromEntries(Object.entries(POSTHOG_CONTEXT_MAPPINGS).flatMap(([sourceKey, targetKey]) => {
|
|
137
|
+
const normalizedValue = asTrimmedString(posthogProperties[sourceKey]);
|
|
138
|
+
return normalizedValue ? [[targetKey, normalizedValue]] : [];
|
|
139
|
+
}));
|
|
140
|
+
};
|
|
141
|
+
const resolvePostHogProperties = (explicitProperties) => {
|
|
142
|
+
if (explicitProperties) {
|
|
143
|
+
return explicitProperties;
|
|
144
|
+
}
|
|
145
|
+
const posthog = getPostHog();
|
|
146
|
+
if (!posthog) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return Object.fromEntries(Object.keys(POSTHOG_CONTEXT_MAPPINGS).flatMap((key) => {
|
|
150
|
+
const value = posthog.get_property(key);
|
|
151
|
+
const normalized = asTrimmedString(value);
|
|
152
|
+
return normalized ? [[key, normalized]] : [];
|
|
153
|
+
}));
|
|
154
|
+
};
|
|
155
|
+
const getCurrentBrowserContext = () => {
|
|
156
|
+
if (typeof window === 'undefined') {
|
|
157
|
+
return {
|
|
158
|
+
href: null,
|
|
159
|
+
referrer: null,
|
|
160
|
+
userAgent: null,
|
|
161
|
+
language: null,
|
|
162
|
+
platform: null,
|
|
163
|
+
vendor: null,
|
|
164
|
+
timeZone: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
href: window.location.href,
|
|
169
|
+
referrer: typeof document !== 'undefined' ? document.referrer : null,
|
|
170
|
+
userAgent: navigator.userAgent,
|
|
171
|
+
language: navigator.language,
|
|
172
|
+
platform: navigator.platform,
|
|
173
|
+
vendor: navigator.vendor,
|
|
174
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || null,
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
export const collectCurrentFunnelAttribution = (input = {}) => {
|
|
178
|
+
const browserContext = getCurrentBrowserContext();
|
|
179
|
+
const href = asTrimmedString(input.href) || browserContext.href;
|
|
180
|
+
const referrer = asTrimmedString(input.referrer) || browserContext.referrer;
|
|
181
|
+
const userAgent = asTrimmedString(input.userAgent) || browserContext.userAgent;
|
|
182
|
+
const language = asTrimmedString(input.language) || browserContext.language;
|
|
183
|
+
const platform = asTrimmedString(input.platform) || browserContext.platform;
|
|
184
|
+
const vendor = asTrimmedString(input.vendor) || browserContext.vendor;
|
|
185
|
+
const timeZone = asTrimmedString(input.timeZone) || browserContext.timeZone;
|
|
186
|
+
const currentUrl = toUrl(href);
|
|
187
|
+
const referrerUrl = toUrl(referrer);
|
|
188
|
+
const firstTouch = collectTouchFromUrl(currentUrl);
|
|
189
|
+
const posthogContext = collectPostHogContext(resolvePostHogProperties(input.posthogProperties));
|
|
190
|
+
return {
|
|
191
|
+
firstTouch,
|
|
192
|
+
lastTouch: Object.assign({}, firstTouch),
|
|
193
|
+
context: normalizeContext(Object.assign({ host: (currentUrl === null || currentUrl === void 0 ? void 0 : currentUrl.host) || null, path: (currentUrl === null || currentUrl === void 0 ? void 0 : currentUrl.pathname) || null, referrer, referringDomain: (referrerUrl === null || referrerUrl === void 0 ? void 0 : referrerUrl.host) || null, userAgent,
|
|
194
|
+
language,
|
|
195
|
+
platform,
|
|
196
|
+
vendor,
|
|
197
|
+
timeZone, deviceType: resolveDeviceType(userAgent), osFamily: resolveOsFamily(userAgent) }, posthogContext)),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
export const mergeFunnelUserAttribution = (current, incoming) => {
|
|
201
|
+
const currentRecord = isRecord(current) ? current : {};
|
|
202
|
+
const currentFirstTouch = normalizeTouch(currentRecord.firstTouch);
|
|
203
|
+
const currentLastTouch = normalizeTouch(currentRecord.lastTouch);
|
|
204
|
+
const currentContext = normalizeContext(currentRecord.context);
|
|
205
|
+
const nextIncoming = incoming !== null && incoming !== void 0 ? incoming : {
|
|
206
|
+
firstTouch: {},
|
|
207
|
+
lastTouch: {},
|
|
208
|
+
context: {},
|
|
209
|
+
};
|
|
210
|
+
const nextFirstTouch = normalizeTouch(nextIncoming.firstTouch);
|
|
211
|
+
const nextLastTouch = normalizeTouch(nextIncoming.lastTouch);
|
|
212
|
+
const nextContext = normalizeContext(nextIncoming.context);
|
|
213
|
+
return {
|
|
214
|
+
firstTouch: Object.keys(currentFirstTouch).length > 0 ? currentFirstTouch : nextFirstTouch,
|
|
215
|
+
lastTouch: Object.keys(nextLastTouch).length > 0
|
|
216
|
+
? nextLastTouch
|
|
217
|
+
: currentLastTouch,
|
|
218
|
+
context: deepMergeRecords(currentContext, nextContext),
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
export const getFunnelUserAttribution = (document) => {
|
|
222
|
+
if (!isRecord(document) || !isRecord(document.attribution)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return mergeFunnelUserAttribution(document.attribution, null);
|
|
226
|
+
};
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import type { FunnelManifestExperiment } from '../config/funnel.manifest.types';
|
|
2
|
-
import type { FunnelUserAnswers } from '../sdk/userAnswers';
|
|
1
|
+
import type { FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
|
|
2
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
3
3
|
export declare const isPreviewStepLockRequested: () => boolean;
|
|
4
4
|
export declare const getRequestedEntryPointId: () => string | null;
|
|
5
5
|
export declare const getRuntimeFunnelIdentity: () => string;
|
|
6
|
-
export declare const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
export declare const shouldRunAutoAdvanceTimer: (input: {
|
|
7
|
+
autoAdvanceMs?: number;
|
|
8
|
+
isPreviewRuntime: boolean;
|
|
9
|
+
shouldLockToInitialStep: boolean;
|
|
10
|
+
}) => boolean;
|
|
10
11
|
export declare const resolveNextStepFromContext: <StepId extends string>(input: {
|
|
11
12
|
stepId: StepId;
|
|
12
13
|
attributes: FunnelUserAnswers;
|
|
13
|
-
userId: string;
|
|
14
14
|
safeInitialStepId: StepId;
|
|
15
15
|
resolveRenderableStepId: (stepId: string | null | undefined) => StepId | null;
|
|
16
16
|
getConfiguredNextStepId: (stepId: StepId, context: {
|
|
17
17
|
attributes: FunnelUserAnswers;
|
|
18
|
-
|
|
19
|
-
getAssignedVariantId: (experiment: FunnelManifestExperiment) => string | null;
|
|
18
|
+
resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
|
|
20
19
|
}) => StepId | null;
|
|
21
20
|
getSequentialNextStepId: (stepId: StepId) => StepId | null;
|
|
22
|
-
|
|
21
|
+
resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
|
|
23
22
|
}) => StepId;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { canUseDom } from './browser-helpers';
|
|
2
|
-
import { chooseAssignedVariantId, getExperimentAssignmentStorageKey, } from './experiment-assignment';
|
|
1
|
+
import { canUseDom } from './browser-helpers.js';
|
|
3
2
|
export const isPreviewStepLockRequested = () => {
|
|
4
3
|
if (!canUseDom()) {
|
|
5
4
|
return false;
|
|
@@ -37,27 +36,14 @@ export const getRuntimeFunnelIdentity = () => {
|
|
|
37
36
|
return 'default';
|
|
38
37
|
}
|
|
39
38
|
};
|
|
40
|
-
export const
|
|
41
|
-
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
const storageKey = getExperimentAssignmentStorageKey(getRuntimeFunnelIdentity(), userId, experiment.experimentId);
|
|
45
|
-
const storedVariantId = storage.read(storageKey);
|
|
46
|
-
const selectedVariantId = chooseAssignedVariantId(experiment, userId, storedVariantId);
|
|
47
|
-
if (!selectedVariantId) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
if (storedVariantId !== selectedVariantId) {
|
|
51
|
-
storage.write(storageKey, selectedVariantId);
|
|
52
|
-
}
|
|
53
|
-
return selectedVariantId;
|
|
39
|
+
export const shouldRunAutoAdvanceTimer = (input) => {
|
|
40
|
+
return Boolean(input.autoAdvanceMs) && !input.isPreviewRuntime && !input.shouldLockToInitialStep;
|
|
54
41
|
};
|
|
55
42
|
export const resolveNextStepFromContext = (input) => {
|
|
56
43
|
var _a, _b;
|
|
57
44
|
const configuredStepId = input.getConfiguredNextStepId(input.stepId, {
|
|
58
45
|
attributes: input.attributes,
|
|
59
|
-
|
|
60
|
-
getAssignedVariantId: (experiment) => input.getAssignedVariantId(experiment, input.userId),
|
|
46
|
+
resolveExperimentVariantKey: input.resolveExperimentVariantKey,
|
|
61
47
|
});
|
|
62
48
|
if (configuredStepId) {
|
|
63
49
|
return (_a = input.resolveRenderableStepId(configuredStepId)) !== null && _a !== void 0 ? _a : input.safeInitialStepId;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { FunnelManifest } from '../config/funnel.manifest.types';
|
|
1
|
+
import type { FunnelManifest } from '../config/funnel.manifest.types.js';
|
|
2
2
|
export declare const validateFunnelManifest: <T extends FunnelManifest>(manifest: T) => T;
|
|
@@ -37,11 +37,7 @@ export const validateFunnelManifest = (manifest) => {
|
|
|
37
37
|
pushDuplicateErrors('step paths', stepPaths, errors);
|
|
38
38
|
pushDuplicateErrors('step file paths', filePaths, errors);
|
|
39
39
|
pushDuplicateErrors('step component keys', componentKeys, errors);
|
|
40
|
-
const
|
|
41
|
-
if (defaultEntryPoints.length !== 1) {
|
|
42
|
-
errors.push(`manifest must declare exactly one default entry point, found ${defaultEntryPoints.length}`);
|
|
43
|
-
}
|
|
44
|
-
for (const entryPoint of manifest.entryPoints) {
|
|
40
|
+
for (const entryPoint of manifest.entryPoints || []) {
|
|
45
41
|
assertValidReference(`entry point "${entryPoint.id}"`, entryPoint.stepId, validStepIds, errors);
|
|
46
42
|
}
|
|
47
43
|
for (const [stepId, edges] of Object.entries(manifest.edgesByStepId)) {
|
|
@@ -53,7 +49,7 @@ export const validateFunnelManifest = (manifest) => {
|
|
|
53
49
|
for (const experiment of manifest.experiments) {
|
|
54
50
|
assertValidReference(`experiment "${experiment.experimentId}"`, experiment.stepId, validStepIds, errors);
|
|
55
51
|
for (const variant of experiment.variants) {
|
|
56
|
-
assertValidReference(`experiment variant "${experiment.experimentId}:${variant.
|
|
52
|
+
assertValidReference(`experiment variant "${experiment.experimentId}:${variant.variantKey}"`, variant.routeToStepId, validStepIds, errors);
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
if (errors.length > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types';
|
|
1
|
+
import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
|
|
2
2
|
export type FunnelStepId = string;
|
|
3
3
|
export declare const getFunnelStepSequence: (manifest: FunnelManifest) => FunnelStepId[];
|
|
4
4
|
export declare const getFunnelEntryPoints: (manifest: FunnelManifest) => {
|
|
@@ -26,9 +26,8 @@ export declare const getChoiceTargetsForStep: (manifest: FunnelManifest, stepId:
|
|
|
26
26
|
export declare const resolveRuntimeInitialStepId: (input: {
|
|
27
27
|
manifest: FunnelManifest;
|
|
28
28
|
requestedStepId?: string | null;
|
|
29
|
-
entryPointId?: string | null;
|
|
30
29
|
}) => FunnelStepId;
|
|
31
30
|
export declare const resolveConfiguredNextStep: (manifest: FunnelManifest, stepId: FunnelStepId, context: {
|
|
32
31
|
attributes: Record<string, unknown>;
|
|
33
|
-
|
|
32
|
+
resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
|
|
34
33
|
}) => FunnelStepId | null;
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { getChoiceTargetsFromEdges, isManifestStepId, resolveInitialStepId, resolveNextStepId, } from './route-resolver';
|
|
1
|
+
import { getChoiceTargetsFromEdges, isManifestStepId, resolveInitialStepId, resolveNextStepId, } from './route-resolver.js';
|
|
2
2
|
export const getFunnelStepSequence = (manifest) => manifest.steps.map((step) => step.id);
|
|
3
|
-
export const getFunnelEntryPoints = (manifest) => manifest.entryPoints.map((entryPoint) => (Object.assign(Object.assign({}, entryPoint), { stepId: entryPoint.stepId })));
|
|
3
|
+
export const getFunnelEntryPoints = (manifest) => (manifest.entryPoints || []).map((entryPoint) => (Object.assign(Object.assign({}, entryPoint), { stepId: entryPoint.stepId })));
|
|
4
4
|
export const getFunnelExperiments = (manifest) => manifest.experiments.map((experiment) => (Object.assign(Object.assign({}, experiment), { stepId: experiment.stepId, variants: experiment.variants.map((variant) => (Object.assign(Object.assign({}, variant), { routeToStepId: variant.routeToStepId }))) })));
|
|
5
|
-
export const getDefaultFunnelStepId = (manifest) =>
|
|
6
|
-
var _a;
|
|
7
|
-
return ((_a = manifest.entryPoints.find((entryPoint) => {
|
|
8
|
-
return 'isDefault' in entryPoint && entryPoint.isDefault;
|
|
9
|
-
})) === null || _a === void 0 ? void 0 : _a.stepId) ||
|
|
10
|
-
getFunnelStepSequence(manifest)[0];
|
|
11
|
-
};
|
|
5
|
+
export const getDefaultFunnelStepId = (manifest) => getFunnelStepSequence(manifest)[0];
|
|
12
6
|
export const getIsFunnelStepId = (manifest, value) => {
|
|
13
7
|
return new Set(getFunnelStepSequence(manifest)).has(value);
|
|
14
8
|
};
|
|
@@ -28,11 +22,11 @@ export const getDefaultEntryPointStepId = (manifest) => {
|
|
|
28
22
|
return getDefaultFunnelStepId(manifest);
|
|
29
23
|
};
|
|
30
24
|
export const getEntryPointStepId = (manifest, entryPointId) => {
|
|
31
|
-
var _a;
|
|
25
|
+
var _a, _b;
|
|
32
26
|
if (!entryPointId) {
|
|
33
27
|
return null;
|
|
34
28
|
}
|
|
35
|
-
const stepId = (_a = manifest.entryPoints.find((entryPoint) => entryPoint.id === entryPointId)) === null ||
|
|
29
|
+
const stepId = (_b = (_a = manifest.entryPoints) === null || _a === void 0 ? void 0 : _a.find((entryPoint) => entryPoint.id === entryPointId)) === null || _b === void 0 ? void 0 : _b.stepId;
|
|
36
30
|
return stepId && getIsFunnelStepId(manifest, stepId) ? stepId : null;
|
|
37
31
|
};
|
|
38
32
|
export const getSequentialNextStepId = (manifest, stepId) => {
|
|
@@ -58,7 +52,6 @@ export const resolveRuntimeInitialStepId = (input) => {
|
|
|
58
52
|
const stepId = resolveInitialStepId({
|
|
59
53
|
manifest: input.manifest,
|
|
60
54
|
requestedStepId: input.requestedStepId,
|
|
61
|
-
entryPointId: input.entryPointId,
|
|
62
55
|
});
|
|
63
56
|
return getIsFunnelStepId(input.manifest, stepId) ? stepId : getDefaultFunnelStepId(input.manifest);
|
|
64
57
|
};
|
|
@@ -67,7 +60,7 @@ export const resolveConfiguredNextStep = (manifest, stepId, context) => {
|
|
|
67
60
|
manifest,
|
|
68
61
|
currentStepId: stepId,
|
|
69
62
|
attributes: context.attributes,
|
|
70
|
-
|
|
63
|
+
resolveExperimentVariantKey: context.resolveExperimentVariantKey,
|
|
71
64
|
});
|
|
72
65
|
return nextStepId && isManifestStepId(manifest, nextStepId) && getIsFunnelStepId(manifest, nextStepId)
|
|
73
66
|
? nextStepId
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PostHog } from 'posthog-js';
|
|
2
|
+
type BootstrapConfig = {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiHost: string;
|
|
5
|
+
distinctId: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const bootstrapPostHog: (config: BootstrapConfig) => Promise<void>;
|
|
8
|
+
export declare const isPostHogReady: () => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Aliases an anonymous distinct id to a server-side user id after the user
|
|
11
|
+
* has been identified (e.g. once bootstrapSession resolves). Preserves the
|
|
12
|
+
* anonymous user's feature-flag bucket assignments via PostHog's
|
|
13
|
+
* ensure_experience_continuity behaviour, so they don't get re-bucketed to
|
|
14
|
+
* a different variant mid-session.
|
|
15
|
+
*
|
|
16
|
+
* No-op if PostHog has not finished initializing or if the ids already match.
|
|
17
|
+
*/
|
|
18
|
+
export declare const identifyPostHog: (serverUserId: string, anonymousId: string) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the variant key for the flag, or undefined if:
|
|
21
|
+
* - PostHog SDK has not finished loading feature flags yet (see isPostHogReady)
|
|
22
|
+
* - OR no flag of that key exists
|
|
23
|
+
*
|
|
24
|
+
* Callers that render experiment variants should gate on isPostHogReady() first,
|
|
25
|
+
* otherwise users may see the fallback (control) variant on first paint and
|
|
26
|
+
* flip once the SDK loads. Task 8's controller handles this gate.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resolveExperimentVariant: (flagKey: string) => string | undefined;
|
|
29
|
+
export declare const getPostHog: () => PostHog | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import posthog from 'posthog-js';
|
|
2
|
+
let initialized = false;
|
|
3
|
+
let readyPromise = null;
|
|
4
|
+
export const bootstrapPostHog = (config) => {
|
|
5
|
+
if (readyPromise) {
|
|
6
|
+
return readyPromise;
|
|
7
|
+
}
|
|
8
|
+
readyPromise = new Promise((resolve) => {
|
|
9
|
+
if (!config.apiKey || !config.apiHost) {
|
|
10
|
+
resolve();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const markReady = () => {
|
|
14
|
+
if (initialized) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
initialized = true;
|
|
18
|
+
resolve();
|
|
19
|
+
};
|
|
20
|
+
posthog.init(config.apiKey, {
|
|
21
|
+
api_host: config.apiHost,
|
|
22
|
+
persistence: 'localStorage+cookie',
|
|
23
|
+
person_profiles: 'identified_only',
|
|
24
|
+
bootstrap: { distinctID: config.distinctId },
|
|
25
|
+
loaded: () => {
|
|
26
|
+
let unsubscribe = null;
|
|
27
|
+
unsubscribe = posthog.onFeatureFlags(() => {
|
|
28
|
+
unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
|
|
29
|
+
markReady();
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
return readyPromise;
|
|
35
|
+
};
|
|
36
|
+
export const isPostHogReady = () => initialized;
|
|
37
|
+
/**
|
|
38
|
+
* Aliases an anonymous distinct id to a server-side user id after the user
|
|
39
|
+
* has been identified (e.g. once bootstrapSession resolves). Preserves the
|
|
40
|
+
* anonymous user's feature-flag bucket assignments via PostHog's
|
|
41
|
+
* ensure_experience_continuity behaviour, so they don't get re-bucketed to
|
|
42
|
+
* a different variant mid-session.
|
|
43
|
+
*
|
|
44
|
+
* No-op if PostHog has not finished initializing or if the ids already match.
|
|
45
|
+
*/
|
|
46
|
+
export const identifyPostHog = (serverUserId, anonymousId) => {
|
|
47
|
+
if (!initialized || serverUserId === anonymousId) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// posthog-js types the 3rd arg as userPropertiesToSetOnce, but passing the
|
|
51
|
+
// prior anonymous id here is the documented posthog pattern for explicitly
|
|
52
|
+
// linking an anonymous session to an identified user for experiment continuity.
|
|
53
|
+
posthog.identify(serverUserId, undefined, anonymousId);
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Returns the variant key for the flag, or undefined if:
|
|
57
|
+
* - PostHog SDK has not finished loading feature flags yet (see isPostHogReady)
|
|
58
|
+
* - OR no flag of that key exists
|
|
59
|
+
*
|
|
60
|
+
* Callers that render experiment variants should gate on isPostHogReady() first,
|
|
61
|
+
* otherwise users may see the fallback (control) variant on first paint and
|
|
62
|
+
* flip once the SDK loads. Task 8's controller handles this gate.
|
|
63
|
+
*/
|
|
64
|
+
export const resolveExperimentVariant = (flagKey) => {
|
|
65
|
+
if (!initialized) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const value = posthog.getFeatureFlag(flagKey);
|
|
69
|
+
return typeof value === 'string' ? value : undefined;
|
|
70
|
+
};
|
|
71
|
+
export const getPostHog = () => (initialized ? posthog : null);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type {
|
|
1
|
+
import { type BuilderPreviewDefinitionPatch, type BuilderPreviewPaywallPlan } from '../config/builder-preview.protocol.js';
|
|
2
|
+
import type { RuntimeMode } from '../services/runtime-mode.service.js';
|
|
3
|
+
import type { FunnelStepId } from './funnel-runtime.js';
|
|
3
4
|
export type PreviewQuickEditPatch = {
|
|
4
5
|
kind: 'tagText';
|
|
5
6
|
stepId: string;
|
|
@@ -28,11 +29,18 @@ export type PreviewQuickEditPatch = {
|
|
|
28
29
|
value: string;
|
|
29
30
|
};
|
|
30
31
|
type PreviewBridgeMessage = {
|
|
32
|
+
kind: 'definitionPatch';
|
|
33
|
+
patch: BuilderPreviewDefinitionPatch;
|
|
34
|
+
} | {
|
|
31
35
|
kind: 'quickEditPatch';
|
|
32
36
|
patch: PreviewQuickEditPatch;
|
|
33
37
|
} | {
|
|
34
38
|
kind: 'runtimeModeChanged';
|
|
35
39
|
mode: RuntimeMode;
|
|
40
|
+
} | {
|
|
41
|
+
kind: 'paywallPlansChanged';
|
|
42
|
+
stepId: string;
|
|
43
|
+
plans: BuilderPreviewPaywallPlan[];
|
|
36
44
|
} | {
|
|
37
45
|
kind: 'goToStep';
|
|
38
46
|
stepId: string;
|
|
@@ -40,6 +48,8 @@ type PreviewBridgeMessage = {
|
|
|
40
48
|
export declare const parsePreviewQuickEditPatch: (value: unknown) => PreviewQuickEditPatch | null;
|
|
41
49
|
export declare const parsePreviewBridgeMessage: (value: unknown) => PreviewBridgeMessage | null;
|
|
42
50
|
export declare const applyPreviewQuickEditPatch: (patch: PreviewQuickEditPatch) => void;
|
|
51
|
+
export declare function emitPreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
52
|
+
export declare function usePreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
43
53
|
export type PreviewBridgeOptions = {
|
|
44
54
|
activeStepId: FunnelStepId;
|
|
45
55
|
onGoToStep: (stepId: FunnelStepId) => void;
|
|
@@ -48,4 +58,4 @@ export type PreviewBridgeOptions = {
|
|
|
48
58
|
shouldLockToInitialStep: boolean;
|
|
49
59
|
};
|
|
50
60
|
export declare function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }: PreviewBridgeOptions): void;
|
|
51
|
-
export {};
|
|
61
|
+
export { applyPreviewDefinitionPatch, clearPreviewDefinitionPatches, resolvePreviewStepPaywallPlans, resolvePreviewStepCountryPricing, resolvePreviewStepLocalizedContent, usePreviewDefinitionOverrideRevision, usePreviewStepPaywallPlans, usePreviewStepCountryPricing, usePreviewStepLocalizedContent, } from './preview-definition-overrides.js';
|