@funnelsgrove/runtime 0.1.48 → 0.1.50

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.
@@ -10,4 +10,5 @@ export type Assignment = {
10
10
  variantKey: string;
11
11
  routeToStepId: string;
12
12
  };
13
+ export declare const resolveForcedExperimentAssignment: (input: ResolveInput) => Assignment | null;
13
14
  export declare const resolveExperimentAssignment: (input: ResolveInput) => Assignment | null;
@@ -16,14 +16,21 @@ const resolveForcedVariant = (input) => {
16
16
  return null;
17
17
  }
18
18
  };
19
+ export const resolveForcedExperimentAssignment = (input) => {
20
+ const forcedVariant = resolveForcedVariant(input);
21
+ if (!forcedVariant) {
22
+ return null;
23
+ }
24
+ return { variantKey: forcedVariant.variantKey, routeToStepId: forcedVariant.routeToStepId };
25
+ };
19
26
  export const resolveExperimentAssignment = (input) => {
20
27
  var _a;
21
28
  if (input.variants.length === 0) {
22
29
  return null;
23
30
  }
24
- const forcedVariant = resolveForcedVariant(input);
25
- if (forcedVariant) {
26
- return { variantKey: forcedVariant.variantKey, routeToStepId: forcedVariant.routeToStepId };
31
+ const forcedAssignment = resolveForcedExperimentAssignment(input);
32
+ if (forcedAssignment) {
33
+ return forcedAssignment;
27
34
  }
28
35
  const resolved = resolveExperimentVariant(input.experimentId);
29
36
  const match = resolved ? input.variants.find((variant) => variant.variantKey === resolved) : undefined;
@@ -7,6 +7,7 @@ export type FunnelStepOrderGraph = {
7
7
  entryPointStepIds?: readonly string[];
8
8
  edgesByStepId?: Partial<Record<string, readonly {
9
9
  toStepId: string;
10
+ conditionId?: string | null;
10
11
  }[]>>;
11
12
  };
12
13
  export declare const getFunnelStepSequence: (manifest: FunnelManifest) => FunnelStepId[];
@@ -17,7 +17,10 @@ export const getFunnelStepIdsByEdgeOrder = (graph) => {
17
17
  }
18
18
  visitedStepIds.add(stepId);
19
19
  orderedStepIds.push(stepId);
20
- for (const edge of ((_a = graph.edgesByStepId) === null || _a === void 0 ? void 0 : _a[stepId]) || []) {
20
+ const edges = [...(((_a = graph.edgesByStepId) === null || _a === void 0 ? void 0 : _a[stepId]) || [])].sort((left, right) => {
21
+ return Number(Boolean(left.conditionId)) - Number(Boolean(right.conditionId));
22
+ });
23
+ for (const edge of edges) {
21
24
  visit(edge.toStepId);
22
25
  }
23
26
  };
@@ -31,18 +31,6 @@ type FunnelFlowAnalyticsAdapter = {
31
31
  selected?: Record<string, unknown>;
32
32
  featureFlags?: Record<string, string>;
33
33
  }) => string | null;
34
- trackStepEngaged?: (input: {
35
- userId?: string;
36
- environment?: 'test' | 'live';
37
- stepId: string;
38
- stepName: string;
39
- stepType?: string;
40
- startedAt: string;
41
- engagedAt: string;
42
- durationMs: number;
43
- engagementThresholdMs: number;
44
- featureFlags?: Record<string, string>;
45
- }) => string | null;
46
34
  trackFunnelStarted?: (input: {
47
35
  userId?: string;
48
36
  environment?: 'test' | 'live';
@@ -116,8 +104,15 @@ export declare function computeRenderSuspended(input: {
116
104
  postHogReady: boolean;
117
105
  experiments: ReadonlyArray<{
118
106
  stepId: string;
119
- }>;
107
+ } & Partial<Pick<FunnelManifestExperiment, 'experimentId' | 'variants'>>>;
120
108
  isPreviewRuntime?: boolean;
121
109
  }): boolean;
110
+ export declare function resolveRenderedExperimentStepId(input: {
111
+ activeExperiment: FunnelManifestExperiment | null;
112
+ editorVariantOverrides: Record<string, string>;
113
+ postHogReady: boolean;
114
+ resolveRenderableStepId: (stepId: string | null | undefined) => string | null;
115
+ safeActiveStepId: string;
116
+ }): string;
122
117
  export declare function useFunnelFlowController<StepId extends string>({ analytics, initialStepId, initialAttributes, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
123
118
  export {};
@@ -7,18 +7,63 @@ import { FUNNEL_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } fro
7
7
  import { buildHostedStepLocation, dispatchWindowCustomEvent, } from './browser-helpers.js';
8
8
  import { isPreviewStepLockRequested, resolveNextStepFromContext, shouldRunAutoAdvanceTimer, } from './funnel-flow.js';
9
9
  import { usePreviewBridge } from './preview-bridge.js';
10
- import { resolveExperimentAssignment } from './experiment-assignment.js';
10
+ import { resolveExperimentAssignment, resolveForcedExperimentAssignment, } from './experiment-assignment.js';
11
11
  import { collectCurrentFunnelAttribution } 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
- const FIRST_STEP_ENGAGEMENT_THRESHOLD_MS = 1000;
17
16
  export function computeRenderSuspended(input) {
17
+ var _a;
18
18
  if (input.isPreviewRuntime || input.postHogReady) {
19
19
  return false;
20
20
  }
21
- return input.experiments.some((experiment) => experiment.stepId === input.activeStepId);
21
+ const activeExperiment = input.experiments.find((experiment) => experiment.stepId === input.activeStepId);
22
+ if (!activeExperiment) {
23
+ return false;
24
+ }
25
+ if (activeExperiment.experimentId && ((_a = activeExperiment.variants) === null || _a === void 0 ? void 0 : _a.length)) {
26
+ const forcedAssignment = resolveForcedExperimentAssignment({
27
+ experimentId: activeExperiment.experimentId,
28
+ variants: activeExperiment.variants,
29
+ });
30
+ if (forcedAssignment) {
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ }
36
+ export function resolveRenderedExperimentStepId(input) {
37
+ const { activeExperiment, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId, } = input;
38
+ if (!activeExperiment) {
39
+ return safeActiveStepId;
40
+ }
41
+ const override = editorVariantOverrides[activeExperiment.experimentId];
42
+ if (override) {
43
+ const matchedVariant = activeExperiment.variants.find((variant) => variant.variantKey === override);
44
+ const variantStepId = resolveRenderableStepId((matchedVariant === null || matchedVariant === void 0 ? void 0 : matchedVariant.routeToStepId) || null);
45
+ return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
46
+ }
47
+ const forcedAssignment = resolveForcedExperimentAssignment({
48
+ experimentId: activeExperiment.experimentId,
49
+ variants: activeExperiment.variants,
50
+ });
51
+ if (forcedAssignment) {
52
+ const forcedStepId = resolveRenderableStepId(forcedAssignment.routeToStepId);
53
+ return forcedStepId !== null && forcedStepId !== void 0 ? forcedStepId : safeActiveStepId;
54
+ }
55
+ if (!postHogReady) {
56
+ return safeActiveStepId;
57
+ }
58
+ const assignment = resolveExperimentAssignment({
59
+ experimentId: activeExperiment.experimentId,
60
+ variants: activeExperiment.variants,
61
+ });
62
+ if (!assignment) {
63
+ return safeActiveStepId;
64
+ }
65
+ const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
66
+ return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
22
67
  }
23
68
  export function useFunnelFlowController({ analytics, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
24
69
  var _a, _b;
@@ -75,8 +120,6 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
75
120
  const prevStepIdRef = useRef(null);
76
121
  const firstStepViewedTrackedRef = useRef(false);
77
122
  const stepStartedAtByIdRef = useRef({});
78
- const engagedStepIdsRef = useRef(new Set());
79
- const firstStepEngagementTimerRef = useRef(null);
80
123
  const currentUserIdRef = useRef(user.id);
81
124
  const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
82
125
  const activeStepMeta = stepById[safeActiveStepId];
@@ -92,35 +135,26 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
92
135
  if (override) {
93
136
  return override;
94
137
  }
138
+ const forcedAssignment = resolveForcedExperimentAssignment({
139
+ experimentId: experiment.experimentId,
140
+ variants: experiment.variants,
141
+ });
142
+ if (forcedAssignment) {
143
+ return forcedAssignment.variantKey;
144
+ }
95
145
  if (!postHogReady) {
96
146
  return null;
97
147
  }
98
148
  return (_a = resolveExperimentVariant(experiment.experimentId)) !== null && _a !== void 0 ? _a : null;
99
149
  }, [editorVariantOverrides, postHogReady]);
100
150
  const activeExperimentForStep = useMemo(() => funnelExperiments.find((candidate) => candidate.stepId === safeActiveStepId) || null, [funnelExperiments, safeActiveStepId]);
101
- const renderedStepId = useMemo(() => {
102
- if (!activeExperimentForStep) {
103
- return safeActiveStepId;
104
- }
105
- const override = editorVariantOverrides[activeExperimentForStep.experimentId];
106
- if (override) {
107
- const matchedVariant = activeExperimentForStep.variants.find((variant) => variant.variantKey === override);
108
- const variantStepId = resolveRenderableStepId((matchedVariant === null || matchedVariant === void 0 ? void 0 : matchedVariant.routeToStepId) || null);
109
- return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
110
- }
111
- if (!postHogReady) {
112
- return safeActiveStepId;
113
- }
114
- const assignment = resolveExperimentAssignment({
115
- experimentId: activeExperimentForStep.experimentId,
116
- variants: activeExperimentForStep.variants,
117
- });
118
- if (!assignment) {
119
- return safeActiveStepId;
120
- }
121
- const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
122
- return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
123
- }, [activeExperimentForStep, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId]);
151
+ const renderedStepId = useMemo(() => resolveRenderedExperimentStepId({
152
+ activeExperiment: activeExperimentForStep,
153
+ editorVariantOverrides,
154
+ postHogReady,
155
+ resolveRenderableStepId,
156
+ safeActiveStepId,
157
+ }), [activeExperimentForStep, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId]);
124
158
  const renderedStepMeta = stepById[renderedStepId];
125
159
  const activeStepComponent = stepComponentById[renderedStepId];
126
160
  const postHogFeatureFlags = useMemo(() => {
@@ -416,37 +450,6 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
416
450
  occurredAt: startedAt,
417
451
  featureFlags: postHogFeatureFlagsRef.current,
418
452
  });
419
- if (!engagedStepIdsRef.current.has(renderedStepId)) {
420
- if (firstStepEngagementTimerRef.current !== null) {
421
- window.clearTimeout(firstStepEngagementTimerRef.current);
422
- }
423
- const engagedStepId = renderedStepId;
424
- const engagedStartedAt = startedAt;
425
- const engagedStepName = stepName;
426
- const engagedStepType = stepType;
427
- firstStepEngagementTimerRef.current = window.setTimeout(() => {
428
- var _a;
429
- firstStepEngagementTimerRef.current = null;
430
- if (prevStepIdRef.current !== engagedStepId ||
431
- engagedStepIdsRef.current.has(engagedStepId)) {
432
- return;
433
- }
434
- engagedStepIdsRef.current.add(engagedStepId);
435
- const engagedAt = new Date().toISOString();
436
- (_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepEngaged) === null || _a === void 0 ? void 0 : _a.call(analytics, {
437
- userId: currentUserIdRef.current,
438
- environment: analyticsRuntimeMode,
439
- stepId: engagedStepId,
440
- stepName: engagedStepName,
441
- stepType: engagedStepType,
442
- startedAt: engagedStartedAt,
443
- engagedAt,
444
- durationMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
445
- engagementThresholdMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
446
- featureFlags: postHogFeatureFlagsRef.current,
447
- });
448
- }, FIRST_STEP_ENGAGEMENT_THRESHOLD_MS);
449
- }
450
453
  }
451
454
  }
452
455
  attributesAtStepStart.current = Object.assign({}, attributesRef.current);
@@ -464,12 +467,6 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
464
467
  renderedStepMeta === null || renderedStepMeta === void 0 ? void 0 : renderedStepMeta.type,
465
468
  safeActiveStepId,
466
469
  ]);
467
- useEffect(() => () => {
468
- if (firstStepEngagementTimerRef.current !== null) {
469
- window.clearTimeout(firstStepEngagementTimerRef.current);
470
- firstStepEngagementTimerRef.current = null;
471
- }
472
- }, []);
473
470
  useEffect(() => {
474
471
  setActiveStepId(safeInitialStepId);
475
472
  }, [safeInitialStepId]);
@@ -584,6 +581,13 @@ export function useFunnelFlowController({ analytics, initialStepId, initialAttri
584
581
  if (override) {
585
582
  return override;
586
583
  }
584
+ const forcedAssignment = resolveForcedExperimentAssignment({
585
+ experimentId: experiment.experimentId,
586
+ variants: experiment.variants,
587
+ });
588
+ if (forcedAssignment) {
589
+ return forcedAssignment.variantKey;
590
+ }
587
591
  if (postHogReady) {
588
592
  const assignment = resolveExperimentAssignment({
589
593
  experimentId: experiment.experimentId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.48",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",