@funnelsgrove/runtime 0.1.55 → 0.1.56
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.
|
@@ -15,4 +15,5 @@ export declare const collectCurrentFunnelAttribution: (input?: {
|
|
|
15
15
|
posthogProperties?: Record<string, unknown> | null;
|
|
16
16
|
}) => FunnelUserAttribution;
|
|
17
17
|
export declare const mergeFunnelUserAttribution: (current: unknown, incoming: FunnelUserAttribution | null | undefined) => FunnelUserAttribution;
|
|
18
|
+
export declare const buildFunnelAttributionEventMetadata: (attribution: FunnelUserAttribution | null | undefined) => Record<string, string>;
|
|
18
19
|
export declare const getFunnelUserAttribution: (document: unknown) => FunnelUserAttribution | null;
|
|
@@ -6,6 +6,22 @@ const INTERNAL_ATTRIBUTION_QUERY_KEYS = new Set([
|
|
|
6
6
|
'step',
|
|
7
7
|
'user_id',
|
|
8
8
|
]);
|
|
9
|
+
const EVENT_ATTRIBUTION_CLICK_ID_KEYS = new Set([
|
|
10
|
+
'_fbc',
|
|
11
|
+
'_fbp',
|
|
12
|
+
'epik',
|
|
13
|
+
'fbc',
|
|
14
|
+
'fbclid',
|
|
15
|
+
'fbp',
|
|
16
|
+
'gbraid',
|
|
17
|
+
'gclid',
|
|
18
|
+
'li_fat_id',
|
|
19
|
+
'msclkid',
|
|
20
|
+
'scclid',
|
|
21
|
+
'ttclid',
|
|
22
|
+
'twclid',
|
|
23
|
+
'wbraid',
|
|
24
|
+
]);
|
|
9
25
|
const POSTHOG_CONTEXT_MAPPINGS = {
|
|
10
26
|
$geoip_city_name: 'cityName',
|
|
11
27
|
$geoip_country_code: 'countryCode',
|
|
@@ -41,6 +57,13 @@ const normalizeTouch = (value) => {
|
|
|
41
57
|
return [[normalizedKey, normalizedValue]];
|
|
42
58
|
}));
|
|
43
59
|
};
|
|
60
|
+
const isEventAttributionKey = (key) => {
|
|
61
|
+
const normalizedKey = key.trim().toLowerCase();
|
|
62
|
+
return normalizedKey.startsWith('utm_') || EVENT_ATTRIBUTION_CLICK_ID_KEYS.has(normalizedKey);
|
|
63
|
+
};
|
|
64
|
+
const normalizeEventAttributionTouch = (value) => {
|
|
65
|
+
return Object.fromEntries(Object.entries(normalizeTouch(value)).filter(([key]) => isEventAttributionKey(key)));
|
|
66
|
+
};
|
|
44
67
|
const normalizeContext = (value) => {
|
|
45
68
|
if (!isRecord(value)) {
|
|
46
69
|
return {};
|
|
@@ -219,6 +242,7 @@ export const mergeFunnelUserAttribution = (current, incoming) => {
|
|
|
219
242
|
context: deepMergeRecords(currentContext, nextContext),
|
|
220
243
|
};
|
|
221
244
|
};
|
|
245
|
+
export const buildFunnelAttributionEventMetadata = (attribution) => normalizeEventAttributionTouch(attribution === null || attribution === void 0 ? void 0 : attribution.firstTouch);
|
|
222
246
|
export const getFunnelUserAttribution = (document) => {
|
|
223
247
|
if (!isRecord(document) || !isRecord(document.attribution)) {
|
|
224
248
|
return null;
|
|
@@ -2,6 +2,7 @@ import { type Dispatch, type SetStateAction } from 'react';
|
|
|
2
2
|
import type { FunnelContextValue } from '../components/FunnelContext.js';
|
|
3
3
|
import type { FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
|
|
4
4
|
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
5
|
+
import { type FunnelUserAttribution } from './funnel-attribution.js';
|
|
5
6
|
import type { FunnelStepMeta } from '../steps/types.js';
|
|
6
7
|
export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManifestExperiment & {
|
|
7
8
|
stepId: StepId;
|
|
@@ -18,6 +19,7 @@ type FunnelFlowAnalyticsAdapter = {
|
|
|
18
19
|
stepName: string;
|
|
19
20
|
stepType?: string;
|
|
20
21
|
startedAt: string;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
21
23
|
featureFlags?: Record<string, string>;
|
|
22
24
|
}) => string | null;
|
|
23
25
|
trackStepCompleted?: (input: {
|
|
@@ -29,6 +31,7 @@ type FunnelFlowAnalyticsAdapter = {
|
|
|
29
31
|
startedAt: string;
|
|
30
32
|
endedAt: string;
|
|
31
33
|
selected?: Record<string, unknown>;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
32
35
|
featureFlags?: Record<string, string>;
|
|
33
36
|
}) => string | null;
|
|
34
37
|
trackFunnelStarted?: (input: {
|
|
@@ -38,6 +41,7 @@ type FunnelFlowAnalyticsAdapter = {
|
|
|
38
41
|
stepName: string;
|
|
39
42
|
stepType?: string;
|
|
40
43
|
occurredAt: string;
|
|
44
|
+
metadata?: Record<string, unknown>;
|
|
41
45
|
featureFlags?: Record<string, string>;
|
|
42
46
|
}) => string | null;
|
|
43
47
|
trackFirstStepViewed?: (input: {
|
|
@@ -47,6 +51,7 @@ type FunnelFlowAnalyticsAdapter = {
|
|
|
47
51
|
stepName: string;
|
|
48
52
|
stepType?: string;
|
|
49
53
|
occurredAt: string;
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
50
55
|
featureFlags?: Record<string, string>;
|
|
51
56
|
}) => string | null;
|
|
52
57
|
};
|
|
@@ -99,8 +104,16 @@ type UseFunnelFlowControllerResult<StepId extends string> = {
|
|
|
99
104
|
setRuntimeMode: (mode: 'test' | 'live') => void;
|
|
100
105
|
showShellContinue: boolean;
|
|
101
106
|
};
|
|
107
|
+
export declare function resolveFunnelEventAttribution(document: unknown, provisionalAttribution: FunnelUserAttribution | null): FunnelUserAttribution | null;
|
|
108
|
+
export declare function buildReadyFunnelAttributionEventMetadata(attributionReady: boolean, attribution: FunnelUserAttribution | null): Record<string, string> | null;
|
|
109
|
+
export declare function scheduleAttributionFailOpen(onReady: () => void, timeoutMs: number): () => void;
|
|
110
|
+
export declare function takeInitialFunnelAttributionMetadata(tracked: boolean, attributionReady: boolean, attribution: FunnelUserAttribution | null): {
|
|
111
|
+
tracked: boolean;
|
|
112
|
+
metadata: Record<string, string> | null;
|
|
113
|
+
};
|
|
102
114
|
export declare function computeRenderSuspended(input: {
|
|
103
115
|
activeStepId: string;
|
|
116
|
+
attributionReady?: boolean;
|
|
104
117
|
postHogReady: boolean;
|
|
105
118
|
experiments: ReadonlyArray<{
|
|
106
119
|
stepId: string;
|
|
@@ -8,14 +8,38 @@ import { buildHostedStepLocation, dispatchWindowCustomEvent, } from './browser-h
|
|
|
8
8
|
import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
|
|
9
9
|
import { usePreviewBridge } from './preview-bridge.js';
|
|
10
10
|
import { resolveExperimentAssignment, resolveForcedExperimentAssignment, } from './experiment-assignment.js';
|
|
11
|
-
import { collectCurrentFunnelAttribution } from './funnel-attribution.js';
|
|
11
|
+
import { buildFunnelAttributionEventMetadata, collectCurrentFunnelAttribution, getFunnelUserAttribution, } from './funnel-attribution.js';
|
|
12
12
|
import { resolveUrlUserAttributes } from './url-user-attributes.js';
|
|
13
13
|
import { bootstrapPostHog, identifyPostHog, isPostHogReady, resolveExperimentVariant, } from './posthog-flags.js';
|
|
14
14
|
import { getRuntimeMode, isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
|
|
15
15
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
16
|
+
export function resolveFunnelEventAttribution(document, provisionalAttribution) {
|
|
17
|
+
var _a;
|
|
18
|
+
return (_a = getFunnelUserAttribution(document)) !== null && _a !== void 0 ? _a : provisionalAttribution;
|
|
19
|
+
}
|
|
20
|
+
export function buildReadyFunnelAttributionEventMetadata(attributionReady, attribution) {
|
|
21
|
+
return attributionReady ? buildFunnelAttributionEventMetadata(attribution) : null;
|
|
22
|
+
}
|
|
23
|
+
export function scheduleAttributionFailOpen(onReady, timeoutMs) {
|
|
24
|
+
const timeoutId = setTimeout(onReady, timeoutMs);
|
|
25
|
+
return () => clearTimeout(timeoutId);
|
|
26
|
+
}
|
|
27
|
+
export function takeInitialFunnelAttributionMetadata(tracked, attributionReady, attribution) {
|
|
28
|
+
if (tracked) {
|
|
29
|
+
return { tracked, metadata: null };
|
|
30
|
+
}
|
|
31
|
+
const metadata = buildReadyFunnelAttributionEventMetadata(attributionReady, attribution);
|
|
32
|
+
return { tracked: metadata !== null, metadata };
|
|
33
|
+
}
|
|
16
34
|
export function computeRenderSuspended(input) {
|
|
17
35
|
var _a;
|
|
18
|
-
if (input.isPreviewRuntime
|
|
36
|
+
if (input.isPreviewRuntime) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (input.attributionReady === false) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (input.postHogReady) {
|
|
19
43
|
return false;
|
|
20
44
|
}
|
|
21
45
|
const activeExperiment = input.experiments.find((experiment) => experiment.stepId === input.activeStepId);
|
|
@@ -114,6 +138,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
114
138
|
completedSteps: [],
|
|
115
139
|
}));
|
|
116
140
|
const [userBootstrapped, setUserBootstrapped] = useState(isPreviewRuntime);
|
|
141
|
+
const [attributionReady, setAttributionReady] = useState(isPreviewRuntime);
|
|
117
142
|
const attributesAtStepStart = useRef({});
|
|
118
143
|
const attributesRef = useRef(attributes);
|
|
119
144
|
const postHogFeatureFlagsRef = useRef({});
|
|
@@ -121,14 +146,16 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
121
146
|
const firstStepViewedTrackedRef = useRef(false);
|
|
122
147
|
const stepStartedAtByIdRef = useRef({});
|
|
123
148
|
const currentUserIdRef = useRef(user.id);
|
|
149
|
+
const initialAttributionRef = useRef(null);
|
|
124
150
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
125
151
|
const activeStepMeta = stepById[safeActiveStepId];
|
|
126
152
|
const renderSuspended = useMemo(() => computeRenderSuspended({
|
|
127
153
|
activeStepId: safeActiveStepId,
|
|
154
|
+
attributionReady,
|
|
128
155
|
postHogReady,
|
|
129
156
|
experiments: funnelExperiments,
|
|
130
157
|
isPreviewRuntime,
|
|
131
|
-
}), [funnelExperiments, isPreviewRuntime, postHogReady, safeActiveStepId]);
|
|
158
|
+
}), [attributionReady, funnelExperiments, isPreviewRuntime, postHogReady, safeActiveStepId]);
|
|
132
159
|
const resolveExperimentVariantKey = useCallback((experiment) => {
|
|
133
160
|
var _a;
|
|
134
161
|
const override = editorVariantOverrides[experiment.experimentId];
|
|
@@ -251,11 +278,13 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
251
278
|
const urlUserAttributes = resolveUrlUserAttributes();
|
|
252
279
|
const bootstrapCandidateUserId = apiService.getBootstrapCandidateUserId(localUserId);
|
|
253
280
|
const initialAttribution = collectCurrentFunnelAttribution();
|
|
281
|
+
initialAttributionRef.current = initialAttribution;
|
|
254
282
|
setUserBootstrapped(isPreviewRuntime);
|
|
255
283
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: localUserId })));
|
|
256
284
|
if (isPreviewRuntime) {
|
|
257
285
|
return;
|
|
258
286
|
}
|
|
287
|
+
const cancelAttributionFailOpen = scheduleAttributionFailOpen(() => setAttributionReady(true), 5000);
|
|
259
288
|
const postHogBootstrap = bootstrapPostHog({
|
|
260
289
|
apiKey: POSTHOG_PROJECT_API_KEY,
|
|
261
290
|
apiHost: POSTHOG_API_HOST,
|
|
@@ -276,14 +305,17 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
276
305
|
attribution: initialAttribution,
|
|
277
306
|
})
|
|
278
307
|
.then((sessionUser) => {
|
|
308
|
+
cancelAttributionFailOpen();
|
|
279
309
|
const sessionAttributes = Object.keys(sessionUser.attributes || {}).length > 0
|
|
280
310
|
? sessionUser.attributes
|
|
281
311
|
: null;
|
|
282
312
|
if (sessionAttributes) {
|
|
283
313
|
setAttributes((prev) => (Object.assign(Object.assign({}, sessionAttributes), prev)));
|
|
284
314
|
}
|
|
315
|
+
initialAttributionRef.current = resolveFunnelEventAttribution(sessionUser.document, initialAttribution);
|
|
285
316
|
setUser((prev) => (Object.assign(Object.assign({}, prev), { id: sessionUser.id || prev.id, name: sessionUser.name || prev.name, email: sessionUser.email || prev.email, document: sessionUser.document, attributes: sessionAttributes ? Object.assign(Object.assign({}, sessionAttributes), prev.attributes) : prev.attributes })));
|
|
286
317
|
setUserBootstrapped(true);
|
|
318
|
+
setAttributionReady(true);
|
|
287
319
|
void apiService.pingUserContext(sessionUser.id)
|
|
288
320
|
.then((pingedUser) => {
|
|
289
321
|
if (!pingedUser) {
|
|
@@ -301,8 +333,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
301
333
|
return sessionUser;
|
|
302
334
|
})
|
|
303
335
|
.catch((error) => {
|
|
336
|
+
cancelAttributionFailOpen();
|
|
304
337
|
logger.error('Failed to bootstrap user session:', error);
|
|
305
338
|
setUserBootstrapped(false);
|
|
339
|
+
setAttributionReady(true);
|
|
306
340
|
return null;
|
|
307
341
|
});
|
|
308
342
|
Promise.all([postHogBootstrap, sessionBootstrap])
|
|
@@ -314,6 +348,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
314
348
|
.catch((error) => {
|
|
315
349
|
logger.error('Failed to identify PostHog user:', error);
|
|
316
350
|
});
|
|
351
|
+
return cancelAttributionFailOpen;
|
|
317
352
|
}, [isPreviewRuntime]);
|
|
318
353
|
const recordStepCompletion = useCallback((stepId, explicitChoices) => {
|
|
319
354
|
var _a;
|
|
@@ -321,6 +356,10 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
321
356
|
if (!meta) {
|
|
322
357
|
return;
|
|
323
358
|
}
|
|
359
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
360
|
+
if (attributionMetadata === null) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
324
363
|
let choices;
|
|
325
364
|
if (explicitChoices) {
|
|
326
365
|
choices = explicitChoices;
|
|
@@ -354,6 +393,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
354
393
|
startedAt,
|
|
355
394
|
endedAt: record.completedAt,
|
|
356
395
|
selected: record.choices,
|
|
396
|
+
metadata: attributionMetadata,
|
|
357
397
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
358
398
|
});
|
|
359
399
|
}
|
|
@@ -378,7 +418,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
378
418
|
}
|
|
379
419
|
return updatedUser;
|
|
380
420
|
});
|
|
381
|
-
}, [analytics, isPreviewRuntime, stepById]);
|
|
421
|
+
}, [analytics, attributionReady, isPreviewRuntime, stepById]);
|
|
382
422
|
const completeStep = useCallback((stepId, choices) => {
|
|
383
423
|
recordStepCompletion(stepId, choices);
|
|
384
424
|
}, [recordStepCompletion]);
|
|
@@ -404,6 +444,12 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
404
444
|
const stepType = renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.type;
|
|
405
445
|
stepStartedAtByIdRef.current[renderedStepId] = startedAt;
|
|
406
446
|
const analyticsRuntimeMode = getRuntimeMode();
|
|
447
|
+
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
448
|
+
prevStepIdRef.current = renderedStepId;
|
|
449
|
+
const attributionMetadata = buildReadyFunnelAttributionEventMetadata(attributionReady, initialAttributionRef.current);
|
|
450
|
+
if (attributionMetadata === null) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
407
453
|
if (!isPreviewRuntime) {
|
|
408
454
|
(_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepStarted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
|
|
409
455
|
userId: currentUserIdRef.current,
|
|
@@ -412,10 +458,15 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
412
458
|
stepName,
|
|
413
459
|
stepType,
|
|
414
460
|
startedAt,
|
|
461
|
+
metadata: attributionMetadata,
|
|
415
462
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
416
463
|
});
|
|
417
464
|
if (!firstStepViewedTrackedRef.current && safeActiveStepId === defaultStepId) {
|
|
418
|
-
firstStepViewedTrackedRef.current
|
|
465
|
+
const initialAttribution = takeInitialFunnelAttributionMetadata(firstStepViewedTrackedRef.current, attributionReady, initialAttributionRef.current);
|
|
466
|
+
firstStepViewedTrackedRef.current = initialAttribution.tracked;
|
|
467
|
+
if (!initialAttribution.metadata) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
419
470
|
(_b = analytics === null || analytics === void 0 ? void 0 : analytics.trackFunnelStarted) === null || _b === void 0 ? void 0 : _b.call(analytics, {
|
|
420
471
|
userId: currentUserIdRef.current,
|
|
421
472
|
environment: analyticsRuntimeMode,
|
|
@@ -423,6 +474,7 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
423
474
|
stepName,
|
|
424
475
|
stepType,
|
|
425
476
|
occurredAt: startedAt,
|
|
477
|
+
metadata: initialAttribution.metadata,
|
|
426
478
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
427
479
|
});
|
|
428
480
|
(_c = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepViewed) === null || _c === void 0 ? void 0 : _c.call(analytics, {
|
|
@@ -432,15 +484,15 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
|
|
|
432
484
|
stepName,
|
|
433
485
|
stepType,
|
|
434
486
|
occurredAt: startedAt,
|
|
487
|
+
metadata: initialAttribution.metadata,
|
|
435
488
|
featureFlags: postHogFeatureFlagsRef.current,
|
|
436
489
|
});
|
|
437
490
|
}
|
|
438
491
|
}
|
|
439
|
-
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
440
|
-
prevStepIdRef.current = renderedStepId;
|
|
441
492
|
}, [
|
|
442
493
|
activeStepId,
|
|
443
494
|
analytics,
|
|
495
|
+
attributionReady,
|
|
444
496
|
defaultStepId,
|
|
445
497
|
isPreviewRuntime,
|
|
446
498
|
recordStepCompletion,
|