@funnelsgrove/runtime 0.1.40 → 0.1.41

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.
@@ -1,6 +1,7 @@
1
1
  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
+ import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
4
5
  import type { FunnelStepMeta } from '../steps/types.js';
5
6
  export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManifestExperiment & {
6
7
  stepId: StepId;
@@ -11,6 +12,8 @@ export type FunnelFlowControllerExperiment<StepId extends string> = FunnelManife
11
12
  type StepComponentRegistry = Record<string, unknown>;
12
13
  type FunnelFlowAnalyticsAdapter = {
13
14
  trackStepStarted?: (input: {
15
+ userId?: string;
16
+ environment?: 'test' | 'live';
14
17
  stepId: string;
15
18
  stepName: string;
16
19
  stepType?: string;
@@ -18,6 +21,8 @@ type FunnelFlowAnalyticsAdapter = {
18
21
  featureFlags?: Record<string, string>;
19
22
  }) => string | null;
20
23
  trackStepCompleted?: (input: {
24
+ userId?: string;
25
+ environment?: 'test' | 'live';
21
26
  stepId: string;
22
27
  stepName: string;
23
28
  stepType?: string;
@@ -27,6 +32,8 @@ type FunnelFlowAnalyticsAdapter = {
27
32
  featureFlags?: Record<string, string>;
28
33
  }) => string | null;
29
34
  trackStepEngaged?: (input: {
35
+ userId?: string;
36
+ environment?: 'test' | 'live';
30
37
  stepId: string;
31
38
  stepName: string;
32
39
  stepType?: string;
@@ -37,6 +44,8 @@ type FunnelFlowAnalyticsAdapter = {
37
44
  featureFlags?: Record<string, string>;
38
45
  }) => string | null;
39
46
  trackFunnelStarted?: (input: {
47
+ userId?: string;
48
+ environment?: 'test' | 'live';
40
49
  stepId: string;
41
50
  stepName: string;
42
51
  stepType?: string;
@@ -44,6 +53,8 @@ type FunnelFlowAnalyticsAdapter = {
44
53
  featureFlags?: Record<string, string>;
45
54
  }) => string | null;
46
55
  trackFirstStepViewed?: (input: {
56
+ userId?: string;
57
+ environment?: 'test' | 'live';
47
58
  stepId: string;
48
59
  stepName: string;
49
60
  stepType?: string;
@@ -54,6 +65,7 @@ type FunnelFlowAnalyticsAdapter = {
54
65
  type UseFunnelFlowControllerInput<StepId extends string> = {
55
66
  analytics?: FunnelFlowAnalyticsAdapter;
56
67
  initialStepId?: StepId;
68
+ initialAttributes?: FunnelUserAnswers;
57
69
  lockToInitialStep?: boolean;
58
70
  defaultStepId: StepId;
59
71
  stepSequence: readonly StepId[];
@@ -107,5 +119,5 @@ export declare function computeRenderSuspended(input: {
107
119
  }>;
108
120
  isPreviewRuntime?: boolean;
109
121
  }): boolean;
110
- export declare function useFunnelFlowController<StepId extends string>({ analytics, initialStepId, lockToInitialStep, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }: UseFunnelFlowControllerInput<StepId>): UseFunnelFlowControllerResult<StepId>;
122
+ 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>;
111
123
  export {};
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { writeStepChoice } from '../sdk/userAnswers.js';
4
4
  import { apiService } from '../services/api.service.js';
5
5
  import { logger } from '../services/logger.js';
6
- import { FUNNEL_ID, FUNNEL_VERSION_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
6
+ import { FUNNEL_ID, POSTHOG_API_HOST, POSTHOG_PROJECT_API_KEY, PROJECT_ID, } from '../services/runtime-api.config.js';
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';
@@ -14,47 +14,13 @@ import { bootstrapPostHog, identifyPostHog, isPostHogReady, resolveExperimentVar
14
14
  import { isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
15
15
  import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
16
16
  const FIRST_STEP_ENGAGEMENT_THRESHOLD_MS = 1000;
17
- const buildFeatureFlagProperties = (featureFlags) => {
18
- return Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [`$feature/${key}`, value]));
19
- };
20
- const capturePostHogStepEvent = (event, properties) => {
21
- const payload = JSON.stringify({
22
- api_key: POSTHOG_PROJECT_API_KEY,
23
- batch: [
24
- {
25
- event,
26
- properties: Object.assign({ $groups: {
27
- project: PROJECT_ID,
28
- }, $insert_id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
29
- ? crypto.randomUUID()
30
- : `${event}_${Date.now()}_${Math.random().toString(16).slice(2)}`, event_source: 'client' }, properties),
31
- timestamp: new Date().toISOString(),
32
- },
33
- ],
34
- });
35
- const endpoint = `${POSTHOG_API_HOST}/batch/`;
36
- if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
37
- const accepted = navigator.sendBeacon(endpoint, new Blob([payload], { type: 'application/json' }));
38
- if (accepted) {
39
- return;
40
- }
41
- }
42
- void fetch(endpoint, {
43
- method: 'POST',
44
- headers: { 'Content-Type': 'application/json' },
45
- body: payload,
46
- keepalive: true,
47
- }).catch(() => {
48
- // Analytics delivery must never block funnel interaction.
49
- });
50
- };
51
17
  export function computeRenderSuspended(input) {
52
18
  if (input.isPreviewRuntime || input.postHogReady) {
53
19
  return false;
54
20
  }
55
21
  return input.experiments.some((experiment) => experiment.stepId === input.activeStepId);
56
22
  }
57
- export function useFunnelFlowController({ analytics, initialStepId, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
23
+ export function useFunnelFlowController({ analytics, initialStepId, initialAttributes, lockToInitialStep = false, defaultStepId, stepSequence, stepById, stepComponentById, funnelExperiments, getPathForStep, getStepIdFromPath, getSequentialNextStepId, getChoiceTargetsForStep, isFunnelStepId, resolveConfiguredNextStep, resolveRuntimeInitialStepId, }) {
58
24
  var _a, _b;
59
25
  const [runtimeMode, setRuntimeMode] = useRuntimeMode();
60
26
  const [editorModeEnabled, setEditorModeEnabled] = useState(false);
@@ -93,7 +59,7 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
93
59
  return resolveFallbackStepId(requestedStepId);
94
60
  }, [initialStepId, resolveFallbackStepId, resolveRuntimeInitialStepId]);
95
61
  const [activeStepId, setActiveStepId] = useState(safeInitialStepId);
96
- const [attributes, setAttributes] = useState({});
62
+ const [attributes, setAttributes] = useState(() => (Object.assign({}, (initialAttributes || {}))));
97
63
  const [user, setUser] = useState(() => ({
98
64
  id: apiService.getOrCreateClientUserId(),
99
65
  name: '',
@@ -357,10 +323,11 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
357
323
  };
358
324
  const stepType = meta.type;
359
325
  const startedAt = stepStartedAtByIdRef.current[meta.id] || record.completedAt;
360
- const durationMs = Math.max(0, new Date(record.completedAt).getTime() - new Date(startedAt).getTime());
361
326
  dispatchWindowCustomEvent('funnel:step-completed', record);
362
327
  if (!isPreviewRuntime) {
363
328
  (_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepCompleted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
329
+ userId: currentUserIdRef.current,
330
+ environment: runtimeMode,
364
331
  stepId: record.stepId,
365
332
  stepName: record.stepName,
366
333
  stepType,
@@ -369,18 +336,6 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
369
336
  selected: record.choices,
370
337
  featureFlags: postHogFeatureFlagsRef.current,
371
338
  });
372
- if (!(analytics === null || analytics === void 0 ? void 0 : analytics.trackStepCompleted)) {
373
- apiService.trackStepCompleted({
374
- userId: currentUserIdRef.current,
375
- stepId: record.stepId,
376
- stepName: record.stepName,
377
- stepType,
378
- startedAt,
379
- endedAt: record.completedAt,
380
- selected: record.choices,
381
- });
382
- capturePostHogStepEvent('step_completed', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, environment: runtimeMode, step_id: record.stepId, step_name: record.stepName, step_type: stepType, started_at: startedAt, ended_at: record.completedAt, duration_ms: Number.isFinite(durationMs) ? durationMs : undefined, selected: record.choices }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
383
- }
384
339
  }
385
340
  setUser((prev) => {
386
341
  var _a, _b;
@@ -430,25 +385,19 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
430
385
  stepStartedAtByIdRef.current[renderedStepId] = startedAt;
431
386
  if (!isPreviewRuntime) {
432
387
  (_a = analytics === null || analytics === void 0 ? void 0 : analytics.trackStepStarted) === null || _a === void 0 ? void 0 : _a.call(analytics, {
388
+ userId: currentUserIdRef.current,
389
+ environment: runtimeMode,
433
390
  stepId: renderedStepId,
434
391
  stepName,
435
392
  stepType,
436
393
  startedAt,
437
394
  featureFlags: postHogFeatureFlagsRef.current,
438
395
  });
439
- if (!(analytics === null || analytics === void 0 ? void 0 : analytics.trackStepStarted)) {
440
- apiService.trackStepStarted({
441
- userId: currentUserIdRef.current,
442
- stepId: renderedStepId,
443
- stepName,
444
- stepType,
445
- startedAt,
446
- });
447
- capturePostHogStepEvent('step_started', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, environment: runtimeMode, step_id: renderedStepId, step_name: stepName, step_type: stepType, started_at: startedAt }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
448
- }
449
396
  if (!firstStepViewedTrackedRef.current && safeActiveStepId === defaultStepId) {
450
397
  firstStepViewedTrackedRef.current = true;
451
398
  (_b = analytics === null || analytics === void 0 ? void 0 : analytics.trackFunnelStarted) === null || _b === void 0 ? void 0 : _b.call(analytics, {
399
+ userId: currentUserIdRef.current,
400
+ environment: runtimeMode,
452
401
  stepId: renderedStepId,
453
402
  stepName,
454
403
  stepType,
@@ -456,6 +405,8 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
456
405
  featureFlags: postHogFeatureFlagsRef.current,
457
406
  });
458
407
  (_c = analytics === null || analytics === void 0 ? void 0 : analytics.trackFirstStepViewed) === null || _c === void 0 ? void 0 : _c.call(analytics, {
408
+ userId: currentUserIdRef.current,
409
+ environment: runtimeMode,
459
410
  stepId: renderedStepId,
460
411
  stepName,
461
412
  stepType,
@@ -483,6 +434,8 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
483
434
  engagedStepIdsRef.current.add(engagedStepId);
484
435
  const engagedAt = new Date().toISOString();
485
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: runtimeMode,
486
439
  stepId: engagedStepId,
487
440
  stepName: engagedStepName,
488
441
  stepType: engagedStepType,
@@ -492,22 +445,6 @@ export function useFunnelFlowController({ analytics, initialStepId, lockToInitia
492
445
  engagementThresholdMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
493
446
  featureFlags: postHogFeatureFlagsRef.current,
494
447
  });
495
- if (!(analytics === null || analytics === void 0 ? void 0 : analytics.trackStepEngaged)) {
496
- apiService.trackFunnelEvent({
497
- userId: currentUserIdRef.current,
498
- eventType: 'step_engaged',
499
- stepId: engagedStepId,
500
- stepName: engagedStepName,
501
- stepType: engagedStepType,
502
- startedAt: engagedStartedAt,
503
- endedAt: engagedAt,
504
- metadata: {
505
- durationMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
506
- engagementThresholdMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
507
- },
508
- });
509
- capturePostHogStepEvent('step_engaged', Object.assign({ distinct_id: currentUserIdRef.current, funnel_id: FUNNEL_ID || undefined, funnel_version_id: FUNNEL_VERSION_ID || undefined, project_id: PROJECT_ID || undefined, environment: runtimeMode, step_id: engagedStepId, step_name: engagedStepName, step_type: engagedStepType, started_at: engagedStartedAt, ended_at: engagedAt, duration_ms: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS, engagement_threshold_ms: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
510
- }
511
448
  }, FIRST_STEP_ENGAGEMENT_THRESHOLD_MS);
512
449
  return () => window.clearTimeout(timer);
513
450
  }, [
@@ -11,26 +11,6 @@ export type AppUser = {
11
11
  type AppUserInput = Omit<AppUser, 'document'> & {
12
12
  document?: Record<string, unknown>;
13
13
  };
14
- export type FunnelEvent = {
15
- userId: string;
16
- eventType: string;
17
- stepId?: string;
18
- stepName?: string;
19
- stepType?: string;
20
- startedAt?: string;
21
- endedAt?: string;
22
- selected?: Record<string, unknown>;
23
- metadata?: Record<string, unknown>;
24
- };
25
- export type StepCompletionEvent = {
26
- userId: string;
27
- stepId: string;
28
- stepName: string;
29
- stepType?: string;
30
- startedAt: string;
31
- endedAt: string;
32
- selected: Record<string, unknown>;
33
- };
34
14
  export type ManageSubscriptionsResponse = {
35
15
  user: {
36
16
  id: string;
@@ -100,16 +80,6 @@ declare class ApiService {
100
80
  userId?: string;
101
81
  source?: 'camera' | 'upload';
102
82
  }): Promise<TempPhotoUploadResult>;
103
- private sendEventAsync;
104
- trackFunnelEvent(event: FunnelEvent): void;
105
- trackStepStarted(input: {
106
- userId: string;
107
- stepId: string;
108
- stepName: string;
109
- stepType?: string;
110
- startedAt: string;
111
- }): void;
112
- trackStepCompleted(event: StepCompletionEvent): void;
113
83
  getManageSubscriptions(): Promise<ManageSubscriptionsResponse>;
114
84
  updateSubscription(input: {
115
85
  subscriptionId: string;
@@ -121,18 +121,6 @@ const persistUserId = (value, funnelId = FUNNEL_ID) => {
121
121
  }
122
122
  return normalized;
123
123
  };
124
- const buildSdkEvent = (event) => {
125
- return {
126
- eventType: event.eventType,
127
- stepId: event.stepId || null,
128
- stepName: event.stepName || null,
129
- stepType: event.stepType || null,
130
- startedAt: event.startedAt || null,
131
- endedAt: event.endedAt || null,
132
- selected: event.selected || {},
133
- metadata: Object.assign({ environment: resolveRuntimeAnalyticsEnvironment() }, (event.metadata || {})),
134
- };
135
- };
136
124
  const withMergedAttributionDocument = (document, attribution) => {
137
125
  const normalizedDocument = asRecord(document);
138
126
  if (!attribution) {
@@ -455,61 +443,6 @@ class ApiService {
455
443
  uploadedAt,
456
444
  };
457
445
  }
458
- sendEventAsync(event) {
459
- const publishableKey = getFunnelSdkPublishableKey();
460
- if (!publishableKey) {
461
- return;
462
- }
463
- const payload = JSON.stringify({
464
- publishableKey,
465
- funnelId: FUNNEL_ID || undefined,
466
- funnelVersionId: FUNNEL_VERSION_ID || undefined,
467
- user_id: event.userId,
468
- events: [buildSdkEvent(event)],
469
- });
470
- const endpoint = buildMainApiUrl('/sdk/public/events');
471
- if (canUseDom() && typeof navigator.sendBeacon === 'function') {
472
- const blob = new Blob([payload], { type: 'application/json' });
473
- const accepted = navigator.sendBeacon(endpoint, blob);
474
- if (accepted) {
475
- return;
476
- }
477
- }
478
- void fetch(endpoint, {
479
- method: 'POST',
480
- headers: buildSdkHeaders({ 'Content-Type': 'application/json' }),
481
- body: payload,
482
- keepalive: true,
483
- }).catch(() => {
484
- // Never block UI if analytics delivery fails.
485
- });
486
- }
487
- trackFunnelEvent(event) {
488
- const userId = persistUserId(event.userId || this.getOrCreateClientUserId(), FUNNEL_ID);
489
- this.sendEventAsync(Object.assign(Object.assign({}, event), { userId }));
490
- }
491
- trackStepStarted(input) {
492
- this.trackFunnelEvent({
493
- userId: input.userId,
494
- eventType: 'step_start',
495
- stepId: input.stepId,
496
- stepName: input.stepName,
497
- stepType: input.stepType,
498
- startedAt: input.startedAt,
499
- });
500
- }
501
- trackStepCompleted(event) {
502
- this.trackFunnelEvent({
503
- userId: event.userId,
504
- eventType: 'step_end',
505
- stepId: event.stepId,
506
- stepName: event.stepName,
507
- stepType: event.stepType,
508
- startedAt: event.startedAt,
509
- endedAt: event.endedAt,
510
- selected: event.selected,
511
- });
512
- }
513
446
  async getManageSubscriptions() {
514
447
  const userId = this.getSubscriptionManagementUserId();
515
448
  const stripeCustomerId = this.getSubscriptionManagementStripeCustomerId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/runtime",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",