@funnelsgrove/runtime 0.1.2 → 0.1.4
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/dist/config/env.config.d.ts +1 -0
- package/dist/config/env.config.js +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/url-user-attributes.d.ts +17 -0
- package/dist/runtime/url-user-attributes.js +38 -0
- package/dist/runtime/use-funnel-flow-controller.js +37 -3
- package/dist/services/api.service.d.ts +7 -0
- package/dist/services/api.service.js +19 -0
- package/dist/services/runtime-api.config.js +2 -1
- package/package.json +1 -1
|
@@ -38,6 +38,7 @@ export declare const RUNTIME_ENV_KEYS: {
|
|
|
38
38
|
readonly claimbeeMonthlyPriceId: readonly ["NEXT_PUBLIC_CLAIMBEE_MONTHLY_PRICE_ID", "CLAIMBEE_MONTHLY_PRICE_ID"];
|
|
39
39
|
readonly claimbeeYearlyPriceId: readonly ["NEXT_PUBLIC_CLAIMBEE_YEARLY_PRICE_ID", "CLAIMBEE_YEARLY_PRICE_ID"];
|
|
40
40
|
};
|
|
41
|
+
export declare const resolveRuntimeSdkApiBaseUrl: (value: string) => string;
|
|
41
42
|
export type RuntimeEnvConfig = {
|
|
42
43
|
readonly [Key in keyof typeof RUNTIME_ENV_KEYS]: string | undefined;
|
|
43
44
|
};
|
|
@@ -60,6 +60,20 @@ export const RUNTIME_ENV_KEYS = {
|
|
|
60
60
|
claimbeeMonthlyPriceId: ['NEXT_PUBLIC_CLAIMBEE_MONTHLY_PRICE_ID', 'CLAIMBEE_MONTHLY_PRICE_ID'],
|
|
61
61
|
claimbeeYearlyPriceId: ['NEXT_PUBLIC_CLAIMBEE_YEARLY_PRICE_ID', 'CLAIMBEE_YEARLY_PRICE_ID'],
|
|
62
62
|
};
|
|
63
|
+
const FUNNELSGROVE_CONTROL_PLANE_API_BASE_URL = 'https://api.funnelsgrove.com';
|
|
64
|
+
const FUNNELSGROVE_SDK_API_BASE_URL = 'https://sdk-api.funnelsgrove.com';
|
|
65
|
+
const trimTrailingSlash = (value) => {
|
|
66
|
+
return value.trim().replace(/\/+$/, '');
|
|
67
|
+
};
|
|
68
|
+
export const resolveRuntimeSdkApiBaseUrl = (value) => {
|
|
69
|
+
const normalized = trimTrailingSlash(value);
|
|
70
|
+
if (!normalized) {
|
|
71
|
+
return FUNNELSGROVE_SDK_API_BASE_URL;
|
|
72
|
+
}
|
|
73
|
+
return normalized === FUNNELSGROVE_CONTROL_PLANE_API_BASE_URL
|
|
74
|
+
? FUNNELSGROVE_SDK_API_BASE_URL
|
|
75
|
+
: normalized;
|
|
76
|
+
};
|
|
63
77
|
const RUNTIME_ENV_VALUES = {
|
|
64
78
|
APP_DEALS_API_BASE_URL: readRuntimeEnv(() => process.env.APP_DEALS_API_BASE_URL),
|
|
65
79
|
FUNNEL_ID: readRuntimeEnv(() => process.env.FUNNEL_ID),
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './runtime/preview-bridge.js';
|
|
|
16
16
|
export * from './runtime/preview-definition-overrides.js';
|
|
17
17
|
export * from './runtime/route-resolver.js';
|
|
18
18
|
export * from './runtime/subscription-handoff.js';
|
|
19
|
+
export * from './runtime/url-user-attributes.js';
|
|
19
20
|
export * from './services/api.service.js';
|
|
20
21
|
export * from './services/funnel-state.service.js';
|
|
21
22
|
export * from './services/logger.js';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './runtime/preview-bridge.js';
|
|
|
16
16
|
export * from './runtime/preview-definition-overrides.js';
|
|
17
17
|
export * from './runtime/route-resolver.js';
|
|
18
18
|
export * from './runtime/subscription-handoff.js';
|
|
19
|
+
export * from './runtime/url-user-attributes.js';
|
|
19
20
|
export * from './services/api.service.js';
|
|
20
21
|
export * from './services/funnel-state.service.js';
|
|
21
22
|
export * from './services/logger.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
2
|
+
export type UrlUserAttributes = {
|
|
3
|
+
email: string | null;
|
|
4
|
+
};
|
|
5
|
+
export type UrlUserAttributeUser = {
|
|
6
|
+
email: string;
|
|
7
|
+
attributes?: FunnelUserAnswers;
|
|
8
|
+
document?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
export declare function normalizeUrlUserEmail(value: string | null | undefined): string | null;
|
|
11
|
+
export declare function resolveUrlUserAttributes(search?: string): UrlUserAttributes;
|
|
12
|
+
export declare function hasUrlUserAttributes(urlAttributes: UrlUserAttributes | null | undefined): boolean;
|
|
13
|
+
export declare function applyUrlUserAttributesToUser<TUser extends UrlUserAttributeUser>(input: {
|
|
14
|
+
user: TUser;
|
|
15
|
+
attributes?: FunnelUserAnswers;
|
|
16
|
+
urlAttributes?: UrlUserAttributes | null;
|
|
17
|
+
}): TUser;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const EMAIL_QUERY_KEY = 'email';
|
|
2
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
|
+
export function normalizeUrlUserEmail(value) {
|
|
4
|
+
const email = value === null || value === void 0 ? void 0 : value.trim();
|
|
5
|
+
if (!email || !EMAIL_PATTERN.test(email)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return email;
|
|
9
|
+
}
|
|
10
|
+
export function resolveUrlUserAttributes(search) {
|
|
11
|
+
if (typeof window === 'undefined' && typeof search !== 'string') {
|
|
12
|
+
return {
|
|
13
|
+
email: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const params = new URLSearchParams(search !== null && search !== void 0 ? search : window.location.search);
|
|
18
|
+
return {
|
|
19
|
+
email: normalizeUrlUserEmail(params.get(EMAIL_QUERY_KEY)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
return {
|
|
24
|
+
email: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function hasUrlUserAttributes(urlAttributes) {
|
|
29
|
+
return Boolean(urlAttributes === null || urlAttributes === void 0 ? void 0 : urlAttributes.email);
|
|
30
|
+
}
|
|
31
|
+
export function applyUrlUserAttributesToUser(input) {
|
|
32
|
+
var _a, _b, _c, _d;
|
|
33
|
+
const email = (_a = input.urlAttributes) === null || _a === void 0 ? void 0 : _a.email;
|
|
34
|
+
if (!email) {
|
|
35
|
+
return input.user;
|
|
36
|
+
}
|
|
37
|
+
return Object.assign(Object.assign({}, input.user), { email, attributes: Object.assign(Object.assign({}, ((_b = input.attributes) !== null && _b !== void 0 ? _b : {})), ((_c = input.user.attributes) !== null && _c !== void 0 ? _c : {})), document: (_d = input.user.document) !== null && _d !== void 0 ? _d : {} });
|
|
38
|
+
}
|
|
@@ -12,6 +12,7 @@ import { collectCurrentFunnelAttribution } from './funnel-attribution.js';
|
|
|
12
12
|
import { bootstrapPostHog, getPostHog, identifyPostHog, isPostHogReady, resolveExperimentVariant, } from './posthog-flags.js';
|
|
13
13
|
import { isEditorEnabled, useRuntimeMode } from '../services/runtime-mode.service.js';
|
|
14
14
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
15
|
+
const FIRST_STEP_ENGAGEMENT_THRESHOLD_MS = 1000;
|
|
15
16
|
const buildFeatureFlagProperties = (featureFlags) => {
|
|
16
17
|
return Object.fromEntries(Object.entries(featureFlags).map(([key, value]) => [`$feature/${key}`, value]));
|
|
17
18
|
};
|
|
@@ -108,6 +109,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
108
109
|
const postHogFeatureFlagsRef = useRef({});
|
|
109
110
|
const prevStepIdRef = useRef(null);
|
|
110
111
|
const stepStartedAtByIdRef = useRef({});
|
|
112
|
+
const engagedStepIdsRef = useRef(new Set());
|
|
111
113
|
const currentUserIdRef = useRef(user.id);
|
|
112
114
|
const safeActiveStepId = useMemo(() => { var _a; return (_a = resolveRenderableStepId(activeStepId)) !== null && _a !== void 0 ? _a : safeInitialStepId; }, [activeStepId, resolveRenderableStepId, safeInitialStepId]);
|
|
113
115
|
const activeStepMeta = stepById[safeActiveStepId];
|
|
@@ -312,6 +314,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
312
314
|
choices,
|
|
313
315
|
};
|
|
314
316
|
const startedAt = stepStartedAtByIdRef.current[meta.id] || record.completedAt;
|
|
317
|
+
const durationMs = Math.max(0, new Date(record.completedAt).getTime() - new Date(startedAt).getTime());
|
|
315
318
|
dispatchWindowCustomEvent('funnel:step-completed', record);
|
|
316
319
|
if (!isPreviewRuntime) {
|
|
317
320
|
apiService.trackStepCompleted({
|
|
@@ -322,7 +325,7 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
322
325
|
endedAt: record.completedAt,
|
|
323
326
|
selected: record.choices,
|
|
324
327
|
});
|
|
325
|
-
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, step_id: record.stepId, step_name: record.stepName, started_at: startedAt, ended_at: record.completedAt, selected: record.choices }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
328
|
+
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, step_id: record.stepId, step_name: record.stepName, started_at: startedAt, ended_at: record.completedAt, duration_ms: Number.isFinite(durationMs) ? durationMs : undefined, selected: record.choices }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
326
329
|
}
|
|
327
330
|
setUser((prev) => {
|
|
328
331
|
var _a, _b;
|
|
@@ -363,22 +366,53 @@ export function useFunnelFlowController({ initialStepId, lockToInitialStep = fal
|
|
|
363
366
|
recordStepCompletion(prevId);
|
|
364
367
|
}
|
|
365
368
|
const startedAt = new Date().toISOString();
|
|
369
|
+
const stepName = (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name) || (activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title) || safeActiveStepId;
|
|
366
370
|
stepStartedAtByIdRef.current[safeActiveStepId] = startedAt;
|
|
367
371
|
if (!isPreviewRuntime) {
|
|
368
372
|
apiService.trackStepStarted({
|
|
369
373
|
userId: currentUserIdRef.current,
|
|
370
374
|
stepId: safeActiveStepId,
|
|
371
|
-
stepName
|
|
375
|
+
stepName,
|
|
372
376
|
startedAt,
|
|
373
377
|
});
|
|
374
|
-
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, step_id: safeActiveStepId, step_name:
|
|
378
|
+
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, step_id: safeActiveStepId, step_name: stepName, started_at: startedAt }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
375
379
|
}
|
|
376
380
|
attributesAtStepStart.current = Object.assign({}, attributesRef.current);
|
|
377
381
|
prevStepIdRef.current = safeActiveStepId;
|
|
382
|
+
if (isPreviewRuntime ||
|
|
383
|
+
safeActiveStepId !== defaultStepId ||
|
|
384
|
+
engagedStepIdsRef.current.has(safeActiveStepId)) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const engagedStepId = safeActiveStepId;
|
|
388
|
+
const engagedStartedAt = startedAt;
|
|
389
|
+
const engagedStepName = stepName;
|
|
390
|
+
const timer = window.setTimeout(() => {
|
|
391
|
+
if (prevStepIdRef.current !== engagedStepId || engagedStepIdsRef.current.has(engagedStepId)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
engagedStepIdsRef.current.add(engagedStepId);
|
|
395
|
+
const engagedAt = new Date().toISOString();
|
|
396
|
+
apiService.trackFunnelEvent({
|
|
397
|
+
userId: currentUserIdRef.current,
|
|
398
|
+
eventType: 'step_engaged',
|
|
399
|
+
stepId: engagedStepId,
|
|
400
|
+
stepName: engagedStepName,
|
|
401
|
+
startedAt: engagedStartedAt,
|
|
402
|
+
endedAt: engagedAt,
|
|
403
|
+
metadata: {
|
|
404
|
+
durationMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
|
|
405
|
+
engagementThresholdMs: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
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, step_id: engagedStepId, step_name: engagedStepName, started_at: engagedStartedAt, ended_at: engagedAt, duration_ms: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS, engagement_threshold_ms: FIRST_STEP_ENGAGEMENT_THRESHOLD_MS }, buildFeatureFlagProperties(postHogFeatureFlagsRef.current)));
|
|
409
|
+
}, FIRST_STEP_ENGAGEMENT_THRESHOLD_MS);
|
|
410
|
+
return () => window.clearTimeout(timer);
|
|
378
411
|
}, [
|
|
379
412
|
activeStepId,
|
|
380
413
|
activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.name,
|
|
381
414
|
activeStepMeta === null || activeStepMeta === void 0 ? void 0 : activeStepMeta.title,
|
|
415
|
+
defaultStepId,
|
|
382
416
|
isPreviewRuntime,
|
|
383
417
|
recordStepCompletion,
|
|
384
418
|
renderSuspended,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FunnelUserAnswers } from '../sdk/userAnswers.js';
|
|
2
2
|
import { type FunnelUserAttribution } from '../runtime/funnel-attribution.js';
|
|
3
|
+
import { type UrlUserAttributes } from '../runtime/url-user-attributes.js';
|
|
3
4
|
export type AppUser = {
|
|
4
5
|
id: string;
|
|
5
6
|
name: string;
|
|
@@ -74,6 +75,12 @@ declare class ApiService {
|
|
|
74
75
|
updateUser(user: AppUser, options?: {
|
|
75
76
|
attribution?: FunnelUserAttribution;
|
|
76
77
|
}): Promise<AppUser>;
|
|
78
|
+
syncUrlUserAttributes(input: {
|
|
79
|
+
user: AppUser;
|
|
80
|
+
attributes?: FunnelUserAnswers;
|
|
81
|
+
urlAttributes?: UrlUserAttributes | null;
|
|
82
|
+
attribution?: FunnelUserAttribution;
|
|
83
|
+
}): Promise<AppUser | null>;
|
|
77
84
|
uploadTempPhoto(input: {
|
|
78
85
|
file: File;
|
|
79
86
|
userId?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildMainApiUrl, buildSdkHeaders, FUNNEL_ID, FUNNEL_VERSION_ID, getFunnelSdkPublishableKey, } from './runtime-api.config.js';
|
|
2
2
|
import { mergeFunnelUserAttribution, } from '../runtime/funnel-attribution.js';
|
|
3
|
+
import { applyUrlUserAttributesToUser, hasUrlUserAttributes, } from '../runtime/url-user-attributes.js';
|
|
3
4
|
const DEFAULT_USER_ID_STORAGE_KEY = 'funnel:user-id';
|
|
4
5
|
const LOCATION_USER_ID_QUERY_KEYS = ['user_id'];
|
|
5
6
|
const LOCATION_STRIPE_CUSTOMER_ID_QUERY_KEYS = ['stripe_customer_id'];
|
|
@@ -326,6 +327,24 @@ class ApiService {
|
|
|
326
327
|
fallbackDocument: withMergedAttributionDocument(user.document, options === null || options === void 0 ? void 0 : options.attribution),
|
|
327
328
|
});
|
|
328
329
|
}
|
|
330
|
+
async syncUrlUserAttributes(input) {
|
|
331
|
+
if (!hasUrlUserAttributes(input.urlAttributes)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const nextUser = applyUrlUserAttributesToUser({
|
|
335
|
+
user: input.user,
|
|
336
|
+
attributes: input.attributes,
|
|
337
|
+
urlAttributes: input.urlAttributes,
|
|
338
|
+
});
|
|
339
|
+
const updatedUser = await this.updateUser(nextUser, {
|
|
340
|
+
attribution: input.attribution,
|
|
341
|
+
});
|
|
342
|
+
return applyUrlUserAttributesToUser({
|
|
343
|
+
user: updatedUser,
|
|
344
|
+
attributes: nextUser.attributes,
|
|
345
|
+
urlAttributes: input.urlAttributes,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
329
348
|
async uploadTempPhoto(input) {
|
|
330
349
|
const userId = persistUserId(input.userId || this.getOrCreateClientUserId(), FUNNEL_ID);
|
|
331
350
|
const publishableKey = getFunnelSdkPublishableKey();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RUNTIME_PUBLIC_DEFAULTS, runtimePublicConfig } from './public-env.js';
|
|
2
|
+
import { resolveRuntimeSdkApiBaseUrl } from '../config/env.config.js';
|
|
2
3
|
const trimTrailingSlash = (value) => {
|
|
3
4
|
return value.replace(/\/+$/, '');
|
|
4
5
|
};
|
|
@@ -16,7 +17,7 @@ const isPreviewFrameRuntime = () => {
|
|
|
16
17
|
return false;
|
|
17
18
|
}
|
|
18
19
|
};
|
|
19
|
-
export const APP_DEALS_API_BASE_URL = trimTrailingSlash(runtimePublicConfig.apiBaseUrl);
|
|
20
|
+
export const APP_DEALS_API_BASE_URL = trimTrailingSlash(resolveRuntimeSdkApiBaseUrl(runtimePublicConfig.apiBaseUrl));
|
|
20
21
|
export const FUNNEL_SDK_PUBLISHABLE_KEY = runtimePublicConfig.funnelSdkPublishableKey;
|
|
21
22
|
export const FUNNEL_ID = runtimePublicConfig.funnelId;
|
|
22
23
|
export const FUNNEL_VERSION_ID = runtimePublicConfig.funnelVersionId;
|