@funnelsgrove/runtime 0.1.23 → 0.1.25

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,8 +15,10 @@ export declare const resolveNextStepFromContext: <StepId extends string>(input:
15
15
  resolveRenderableStepId: (stepId: string | null | undefined) => StepId | null;
16
16
  getConfiguredNextStepId: (stepId: StepId, context: {
17
17
  attributes: FunnelUserAnswers;
18
+ ignoreExperiment?: boolean;
18
19
  resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
19
20
  }) => StepId | null;
20
21
  getSequentialNextStepId: (stepId: StepId) => StepId | null;
22
+ ignoreExperiment?: boolean;
21
23
  resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
22
24
  }) => StepId;
@@ -43,6 +43,7 @@ export const resolveNextStepFromContext = (input) => {
43
43
  var _a, _b;
44
44
  const configuredStepId = input.getConfiguredNextStepId(input.stepId, {
45
45
  attributes: input.attributes,
46
+ ignoreExperiment: input.ignoreExperiment,
46
47
  resolveExperimentVariantKey: input.resolveExperimentVariantKey,
47
48
  });
48
49
  if (configuredStepId) {
@@ -1,6 +1,16 @@
1
1
  import type { FunnelManifest, FunnelManifestExperiment } from '../config/funnel.manifest.types.js';
2
2
  export type FunnelStepId = string;
3
+ export type FunnelStepOrderGraph = {
4
+ steps: readonly {
5
+ id: string;
6
+ }[];
7
+ entryPointStepIds?: readonly string[];
8
+ edgesByStepId?: Partial<Record<string, readonly {
9
+ toStepId: string;
10
+ }[]>>;
11
+ };
3
12
  export declare const getFunnelStepSequence: (manifest: FunnelManifest) => FunnelStepId[];
13
+ export declare const getFunnelStepIdsByEdgeOrder: (graph: FunnelStepOrderGraph) => FunnelStepId[];
4
14
  export declare const getFunnelEntryPoints: (manifest: FunnelManifest) => {
5
15
  stepId: FunnelStepId;
6
16
  id: string;
@@ -29,5 +39,6 @@ export declare const resolveRuntimeInitialStepId: (input: {
29
39
  }) => FunnelStepId;
30
40
  export declare const resolveConfiguredNextStep: (manifest: FunnelManifest, stepId: FunnelStepId, context: {
31
41
  attributes: Record<string, unknown>;
42
+ ignoreExperiment?: boolean;
32
43
  resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
33
44
  }) => FunnelStepId | null;
@@ -1,5 +1,31 @@
1
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 getFunnelStepIdsByEdgeOrder = (graph) => {
4
+ const stepIds = graph.steps.map((step) => step.id).filter(Boolean);
5
+ const firstStepId = stepIds[0];
6
+ if (!firstStepId || !graph.edgesByStepId || Object.keys(graph.edgesByStepId).length === 0) {
7
+ return stepIds;
8
+ }
9
+ const stepIdSet = new Set(stepIds);
10
+ const orderedStepIds = [];
11
+ const visitedStepIds = new Set();
12
+ const visit = (stepId) => {
13
+ var _a;
14
+ if (!stepIdSet.has(stepId) || visitedStepIds.has(stepId)) {
15
+ return;
16
+ }
17
+ visitedStepIds.add(stepId);
18
+ orderedStepIds.push(stepId);
19
+ for (const edge of ((_a = graph.edgesByStepId) === null || _a === void 0 ? void 0 : _a[stepId]) || []) {
20
+ visit(edge.toStepId);
21
+ }
22
+ };
23
+ visit(firstStepId);
24
+ for (const entryPointStepId of graph.entryPointStepIds || []) {
25
+ visit(entryPointStepId);
26
+ }
27
+ return orderedStepIds.length > 0 ? orderedStepIds : stepIds;
28
+ };
3
29
  export const getFunnelEntryPoints = (manifest) => (manifest.entryPoints || []).map((entryPoint) => (Object.assign(Object.assign({}, entryPoint), { stepId: entryPoint.stepId })));
4
30
  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
31
  export const getDefaultFunnelStepId = (manifest) => getFunnelStepSequence(manifest)[0];
@@ -60,6 +86,7 @@ export const resolveConfiguredNextStep = (manifest, stepId, context) => {
60
86
  manifest,
61
87
  currentStepId: stepId,
62
88
  attributes: context.attributes,
89
+ ignoreExperiment: context.ignoreExperiment,
63
90
  resolveExperimentVariantKey: context.resolveExperimentVariantKey,
64
91
  });
65
92
  return nextStepId && isManifestStepId(manifest, nextStepId) && getIsFunnelStepId(manifest, nextStepId)
@@ -13,5 +13,6 @@ export declare const resolveNextStepId: (input: {
13
13
  manifest: FunnelManifest;
14
14
  currentStepId: string;
15
15
  attributes: Record<string, unknown>;
16
+ ignoreExperiment?: boolean;
16
17
  resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
17
18
  }) => string | null;
@@ -65,7 +65,7 @@ export const resolveInitialStepId = (input) => {
65
65
  export const resolveNextStepId = (input) => {
66
66
  var _a;
67
67
  const experiment = getExperimentForStep(input.manifest, input.currentStepId);
68
- if (experiment) {
68
+ if (experiment && !input.ignoreExperiment) {
69
69
  const variantKey = input.resolveExperimentVariantKey(experiment);
70
70
  const matchedVariant = variantKey
71
71
  ? experiment.variants.find((variant) => variant.variantKey === variantKey)
@@ -36,6 +36,7 @@ type UseFunnelFlowControllerInput<StepId extends string> = {
36
36
  isFunnelStepId: (value: string) => value is StepId;
37
37
  resolveConfiguredNextStep: (stepId: StepId, context: {
38
38
  attributes: Record<string, unknown>;
39
+ ignoreExperiment?: boolean;
39
40
  resolveExperimentVariantKey: (experiment: FunnelManifestExperiment) => string | null;
40
41
  }) => StepId | null;
41
42
  resolveRuntimeInitialStepId: (input: {
@@ -130,14 +130,14 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
130
130
  }
131
131
  return (_a = resolveExperimentVariant(experiment.experimentId)) !== null && _a !== void 0 ? _a : null;
132
132
  }, [editorVariantOverrides, postHogReady]);
133
+ const activeExperimentForStep = useMemo(() => funnelExperiments.find((candidate) => candidate.stepId === safeActiveStepId) || null, [funnelExperiments, safeActiveStepId]);
133
134
  const renderedStepId = useMemo(() => {
134
- const experiment = funnelExperiments.find((candidate) => candidate.stepId === safeActiveStepId);
135
- if (!experiment) {
135
+ if (!activeExperimentForStep) {
136
136
  return safeActiveStepId;
137
137
  }
138
- const override = editorVariantOverrides[experiment.experimentId];
138
+ const override = editorVariantOverrides[activeExperimentForStep.experimentId];
139
139
  if (override) {
140
- const matchedVariant = experiment.variants.find((variant) => variant.variantKey === override);
140
+ const matchedVariant = activeExperimentForStep.variants.find((variant) => variant.variantKey === override);
141
141
  const variantStepId = resolveRenderableStepId((matchedVariant === null || matchedVariant === void 0 ? void 0 : matchedVariant.routeToStepId) || null);
142
142
  return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
143
143
  }
@@ -145,15 +145,15 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
145
145
  return safeActiveStepId;
146
146
  }
147
147
  const assignment = resolveExperimentAssignment({
148
- experimentId: experiment.experimentId,
149
- variants: experiment.variants,
148
+ experimentId: activeExperimentForStep.experimentId,
149
+ variants: activeExperimentForStep.variants,
150
150
  });
151
151
  if (!assignment) {
152
152
  return safeActiveStepId;
153
153
  }
154
154
  const variantStepId = resolveRenderableStepId(assignment.routeToStepId);
155
155
  return variantStepId !== null && variantStepId !== void 0 ? variantStepId : safeActiveStepId;
156
- }, [editorVariantOverrides, funnelExperiments, postHogReady, resolveRenderableStepId, safeActiveStepId]);
156
+ }, [activeExperimentForStep, editorVariantOverrides, postHogReady, resolveRenderableStepId, safeActiveStepId]);
157
157
  const renderedStepMeta = stepById[renderedStepId];
158
158
  const activeStepComponent = stepComponentById[renderedStepId];
159
159
  const postHogFeatureFlags = useMemo(() => {
@@ -188,6 +188,22 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
188
188
  }
189
189
  setActiveStepId(safeStepId);
190
190
  }, [getPathForStep, resolveRenderableStepId, safeInitialStepId, shouldLockToInitialStep]);
191
+ useEffect(() => {
192
+ if (!activeExperimentForStep ||
193
+ renderSuspended ||
194
+ shouldLockToInitialStep ||
195
+ renderedStepId === safeActiveStepId) {
196
+ return;
197
+ }
198
+ goToStep(renderedStepId);
199
+ }, [
200
+ activeExperimentForStep,
201
+ goToStep,
202
+ renderSuspended,
203
+ renderedStepId,
204
+ safeActiveStepId,
205
+ shouldLockToInitialStep,
206
+ ]);
191
207
  const setAnswer = useCallback((key, value) => {
192
208
  setAttributes((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
193
209
  }, []);
@@ -222,7 +238,7 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
222
238
  }, 100);
223
239
  return () => window.clearInterval(intervalId);
224
240
  }, [postHogReady]);
225
- const resolveNextStepId = useCallback((stepId) => {
241
+ const resolveNextStepId = useCallback((stepId, options) => {
226
242
  return resolveNextStepFromContext({
227
243
  stepId,
228
244
  attributes: attributesRef.current,
@@ -230,6 +246,7 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
230
246
  resolveRenderableStepId,
231
247
  getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
232
248
  getSequentialNextStepId,
249
+ ignoreExperiment: options === null || options === void 0 ? void 0 : options.ignoreExperiment,
233
250
  resolveExperimentVariantKey,
234
251
  });
235
252
  }, [
@@ -240,8 +257,10 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
240
257
  safeInitialStepId,
241
258
  ]);
242
259
  const goNext = useCallback(() => {
243
- goToStep(resolveNextStepId(safeActiveStepId));
244
- }, [goToStep, resolveNextStepId, safeActiveStepId]);
260
+ goToStep(resolveNextStepId(renderedStepId, {
261
+ ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
262
+ }));
263
+ }, [activeExperimentForStep, goToStep, renderedStepId, resolveNextStepId, safeActiveStepId]);
245
264
  useEffect(() => {
246
265
  const localUserId = apiService.getOrCreateClientUserId();
247
266
  const urlUserAttributes = resolveUrlUserAttributes();
@@ -383,6 +402,9 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
383
402
  return;
384
403
  }
385
404
  const prevId = prevStepIdRef.current;
405
+ if (prevId === renderedStepId) {
406
+ return;
407
+ }
386
408
  if (prevId && prevId !== renderedStepId) {
387
409
  recordStepCompletion(prevId);
388
410
  }
@@ -476,19 +498,22 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
476
498
  return () => window.removeEventListener('popstate', syncStepWithPath);
477
499
  }, [getStepIdFromPath, resolveRenderableStepId, safeInitialStepId, shouldLockToInitialStep]);
478
500
  const nextStepId = useMemo(() => resolveNextStepFromContext({
479
- stepId: safeActiveStepId,
501
+ stepId: renderedStepId,
480
502
  attributes,
481
503
  safeInitialStepId,
482
504
  resolveRenderableStepId,
483
505
  getConfiguredNextStepId: (currentStepId, context) => resolveConfiguredNextStep(currentStepId, context),
484
506
  getSequentialNextStepId,
507
+ ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
485
508
  resolveExperimentVariantKey,
486
509
  }), [
510
+ activeExperimentForStep,
487
511
  attributes,
488
512
  getSequentialNextStepId,
489
513
  resolveConfiguredNextStep,
490
514
  resolveExperimentVariantKey,
491
515
  resolveRenderableStepId,
516
+ renderedStepId,
492
517
  safeActiveStepId,
493
518
  safeInitialStepId,
494
519
  ]);
@@ -503,12 +528,23 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
503
528
  if (!shouldRunAutoAdvanceTimer({ autoAdvanceMs, isPreviewRuntime, shouldLockToInitialStep })) {
504
529
  return;
505
530
  }
506
- const autoAdvanceTarget = resolveNextStepId(safeActiveStepId);
531
+ const autoAdvanceTarget = resolveNextStepId(renderedStepId, {
532
+ ignoreExperiment: Boolean(activeExperimentForStep && renderedStepId === safeActiveStepId),
533
+ });
507
534
  const timer = window.setTimeout(() => {
508
535
  goToStep(autoAdvanceTarget);
509
536
  }, autoAdvanceMs);
510
537
  return () => window.clearTimeout(timer);
511
- }, [autoAdvanceMs, goToStep, isPreviewRuntime, resolveNextStepId, safeActiveStepId, shouldLockToInitialStep]);
538
+ }, [
539
+ activeExperimentForStep,
540
+ autoAdvanceMs,
541
+ goToStep,
542
+ isPreviewRuntime,
543
+ renderedStepId,
544
+ resolveNextStepId,
545
+ safeActiveStepId,
546
+ shouldLockToInitialStep,
547
+ ]);
512
548
  const handleContinue = useCallback(() => {
513
549
  goToStep(nextStepId);
514
550
  }, [goToStep, nextStepId]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",