@funnelsgrove/cli 0.1.14 → 0.1.18
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/contracts/step-contract-v2.json +733 -0
- package/dist/analyticsOutput.d.ts +30 -15
- package/dist/analyticsOutput.js +45 -28
- package/dist/apiClient.d.ts +85 -0
- package/dist/apiClient.js +225 -8
- package/dist/cli.d.ts +149 -1
- package/dist/cli.js +1232 -71
- package/dist/diagnosticOutput.d.ts +42 -0
- package/dist/diagnosticOutput.js +83 -0
- package/dist/docsOutput.d.ts +19 -0
- package/dist/docsOutput.js +73 -0
- package/dist/docsTransaction.d.ts +81 -0
- package/dist/docsTransaction.js +1755 -0
- package/dist/funnelValidation.d.ts +14 -0
- package/dist/funnelValidation.js +297 -0
- package/dist/localSync.d.ts +79 -0
- package/dist/localSync.js +428 -53
- package/dist/projectValidator.d.ts +27 -0
- package/dist/projectValidator.js +208 -0
- package/dist/reskin.js +27 -1
- package/dist/stepContractMigration.d.ts +71 -0
- package/dist/stepContractMigration.js +1323 -0
- package/dist/templateDocs.js +8 -25
- package/docs-release-history.json +66 -0
- package/legacy-docs-catalog.json +1012 -0
- package/package.json +13 -4
- package/template_docs/.funnelsgrove-docs.json +144 -0
- package/template_docs/AGENTS.md +30 -58
- package/template_docs/CLAUDE.md +0 -2
- package/template_docs/docs/funnelsgrove/START-HERE.md +73 -0
- package/template_docs/docs/funnelsgrove/contracts/analytics-events.md +59 -0
- package/template_docs/docs/funnelsgrove/contracts/content-answers.md +44 -0
- package/template_docs/docs/funnelsgrove/contracts/flow-routing.md +41 -0
- package/template_docs/docs/funnelsgrove/contracts/payments.md +42 -0
- package/template_docs/docs/funnelsgrove/contracts/step-metadata.md +57 -0
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
- package/template_docs/docs/funnelsgrove/qa/analytics.md +43 -0
- package/template_docs/docs/funnelsgrove/qa/local.md +42 -0
- package/template_docs/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
- package/template_docs/docs/funnelsgrove/qa/publish.md +32 -0
- package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +34 -0
- package/template_docs/docs/funnelsgrove/recipes/add-step.md +33 -0
- package/template_docs/docs/funnelsgrove/recipes/edit-step.md +31 -0
- package/template_docs/docs/funnelsgrove/steps/INDEX.md +26 -0
- package/template_docs/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/checkout.md +120 -0
- package/template_docs/docs/funnelsgrove/steps/form_input.md +115 -0
- package/template_docs/docs/funnelsgrove/steps/intro_hero.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
- package/template_docs/docs/funnelsgrove/steps/paywall_offer.md +115 -0
- package/template_docs/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
- package/template_docs/docs/funnelsgrove/steps/purchase_completed.md +117 -0
- package/template_docs/docs/funnelsgrove/steps/single_step_choice.md +122 -0
- package/template_docs/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
- package/template_docs/docs/funnelsgrove/steps/social_proof.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
- package/template_docs/docs/funnelsgrove/steps/subscription_management.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
- package/template_docs/docs/funnelsgrove/steps/upsell_offer.md +112 -0
- package/template_docs/docs/funnelsgrove/steps/value_prop_story.md +110 -0
- package/template_docs/funnel-docs.config.json +43 -0
- package/template_scaffold/.env.example +41 -0
- package/template_scaffold/.funnelsgrove-contract-tools.json +19 -0
- package/template_scaffold/.funnelsgrove-docs.json +144 -0
- package/template_scaffold/.funnelsgrove-scaffold.json +1078 -0
- package/template_scaffold/AGENTS.md +42 -0
- package/template_scaffold/CLAUDE.md +1 -0
- package/template_scaffold/PLAN.md +81 -0
- package/template_scaffold/PRODUCT_SENSE.md +28 -0
- package/template_scaffold/context/build-context.md +34 -0
- package/template_scaffold/docs/AGENT_FUNNEL_IMPLEMENTATION_SPEC.md +5 -0
- package/template_scaffold/docs/ARCHITECTURE.md +5 -0
- package/template_scaffold/docs/FLOW_CONFIG_AND_ROUTING.md +5 -0
- package/template_scaffold/docs/PAYWALL_AND_PAYMENTS.md +5 -0
- package/template_scaffold/docs/README.md +5 -0
- package/template_scaffold/docs/funnelsgrove/START-HERE.md +73 -0
- package/template_scaffold/docs/funnelsgrove/contracts/analytics-events.md +59 -0
- package/template_scaffold/docs/funnelsgrove/contracts/content-answers.md +44 -0
- package/template_scaffold/docs/funnelsgrove/contracts/flow-routing.md +41 -0
- package/template_scaffold/docs/funnelsgrove/contracts/payments.md +42 -0
- package/template_scaffold/docs/funnelsgrove/contracts/step-metadata.md +57 -0
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +39 -0
- package/template_scaffold/docs/funnelsgrove/qa/analytics.md +43 -0
- package/template_scaffold/docs/funnelsgrove/qa/local.md +42 -0
- package/template_scaffold/docs/funnelsgrove/qa/paywall-checkout.md +55 -0
- package/template_scaffold/docs/funnelsgrove/qa/publish.md +32 -0
- package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +34 -0
- package/template_scaffold/docs/funnelsgrove/recipes/add-step.md +33 -0
- package/template_scaffold/docs/funnelsgrove/recipes/edit-step.md +31 -0
- package/template_scaffold/docs/funnelsgrove/steps/INDEX.md +26 -0
- package/template_scaffold/docs/funnelsgrove/steps/cancellation_offer.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/checkout.md +120 -0
- package/template_scaffold/docs/funnelsgrove/steps/form_input.md +115 -0
- package/template_scaffold/docs/funnelsgrove/steps/intro_hero.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/multi_select_choice.md +123 -0
- package/template_scaffold/docs/funnelsgrove/steps/paywall_offer.md +115 -0
- package/template_scaffold/docs/funnelsgrove/steps/progress_interstitial.md +111 -0
- package/template_scaffold/docs/funnelsgrove/steps/purchase_completed.md +117 -0
- package/template_scaffold/docs/funnelsgrove/steps/single_step_choice.md +122 -0
- package/template_scaffold/docs/funnelsgrove/steps/single_step_choice_emoji.md +122 -0
- package/template_scaffold/docs/funnelsgrove/steps/social_proof.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/subscription_handoff.md +113 -0
- package/template_scaffold/docs/funnelsgrove/steps/subscription_management.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/summary_confirmation.md +110 -0
- package/template_scaffold/docs/funnelsgrove/steps/upsell_offer.md +112 -0
- package/template_scaffold/docs/funnelsgrove/steps/value_prop_story.md +110 -0
- package/template_scaffold/docs/references/funnel-sdk-user-answers.md +5 -0
- package/template_scaffold/eslint.config.mjs +16 -0
- package/template_scaffold/funnel-agent-docs.test.ts +699 -0
- package/template_scaffold/funnel-docs.config.json +43 -0
- package/template_scaffold/funnel.config.json +5 -0
- package/template_scaffold/gitignore.template +5 -0
- package/template_scaffold/next-env.d.ts +6 -0
- package/template_scaffold/next.config.ts +53 -0
- package/template_scaffold/package-lock.json +7737 -0
- package/template_scaffold/package.json +37 -0
- package/template_scaffold/package.test.ts +23 -0
- package/template_scaffold/public/claimbee/bee.png +0 -0
- package/template_scaffold/public/claimbee/device-iphone.png +0 -0
- package/template_scaffold/public/claimbee/device-macbook.png +0 -0
- package/template_scaffold/public/claimbee/device-none.png +0 -0
- package/template_scaffold/public/claimbee/device-watch.png +0 -0
- package/template_scaffold/public/claimbee/glow.svg +3 -0
- package/template_scaffold/public/claimbee/steps-sprite-alt.png +0 -0
- package/template_scaffold/public/claimbee/steps-sprite-default.png +0 -0
- package/template_scaffold/public/claimbee/testimonial-avatar.png +0 -0
- package/template_scaffold/public/claimbee/testimonial-star.svg +3 -0
- package/template_scaffold/public/paywall/ai-claim-assistant.png +0 -0
- package/template_scaffold/public/paywall/claim-history.png +0 -0
- package/template_scaffold/public/paywall/clear-progress.png +0 -0
- package/template_scaffold/public/paywall/hero.png +0 -0
- package/template_scaffold/public/paywall/money-alerts.png +0 -0
- package/template_scaffold/public/paywall/money-back-badge.png +0 -0
- package/template_scaffold/public/paywall/paywall-image.png +0 -0
- package/template_scaffold/public/paywall/r1.png +0 -0
- package/template_scaffold/public/paywall/r2.png +0 -0
- package/template_scaffold/public/paywall/r3.png +0 -0
- package/template_scaffold/public/paywall/r4.png +0 -0
- package/template_scaffold/public/paywall/r5.png +0 -0
- package/template_scaffold/public/paywall/r6.png +0 -0
- package/template_scaffold/public/paywall/special-offer-gift.png +0 -0
- package/template_scaffold/public/please/bee.png +0 -0
- package/template_scaffold/public/please/device-iphone.png +0 -0
- package/template_scaffold/public/please/device-macbook.png +0 -0
- package/template_scaffold/public/please/device-none.png +0 -0
- package/template_scaffold/public/please/device-watch.png +0 -0
- package/template_scaffold/public/please/glow.svg +3 -0
- package/template_scaffold/public/please/steps-sprite-alt.png +0 -0
- package/template_scaffold/public/please/steps-sprite-default.png +0 -0
- package/template_scaffold/public/please/testimonial-avatar.png +0 -0
- package/template_scaffold/public/please/testimonial-star.svg +3 -0
- package/template_scaffold/public/upsell/upgrade-hero.png +0 -0
- package/template_scaffold/src/app/[stepId]/page.test.tsx +59 -0
- package/template_scaffold/src/app/[stepId]/page.tsx +62 -0
- package/template_scaffold/src/app/globals.css +49 -0
- package/template_scaffold/src/app/layout.test.ts +26 -0
- package/template_scaffold/src/app/layout.tsx +61 -0
- package/template_scaffold/src/app/page.tsx +8 -0
- package/template_scaffold/src/app/sdk/page.tsx +673 -0
- package/template_scaffold/src/app/subscription-dashboard/page.tsx +274 -0
- package/template_scaffold/src/assets/claimbee/bee.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-iphone.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-macbook.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-none.png +0 -0
- package/template_scaffold/src/assets/claimbee/device-watch.png +0 -0
- package/template_scaffold/src/assets/claimbee/glow.svg +3 -0
- package/template_scaffold/src/assets/claimbee/steps-sprite-alt.png +0 -0
- package/template_scaffold/src/assets/claimbee/steps-sprite-default.png +0 -0
- package/template_scaffold/src/assets/claimbee/testimonial-avatar.png +0 -0
- package/template_scaffold/src/assets/claimbee/testimonial-star.svg +3 -0
- package/template_scaffold/src/components/FunnelEditorPanel.tsx +194 -0
- package/template_scaffold/src/components/FunnelFlow.test.tsx +17 -0
- package/template_scaffold/src/components/FunnelFlow.tsx +114 -0
- package/template_scaffold/src/components/FunnelStepImage.test.ts +15 -0
- package/template_scaffold/src/components/FunnelStepImage.tsx +28 -0
- package/template_scaffold/src/components/step/FunnelStepHeader.tsx +35 -0
- package/template_scaffold/src/config/billing.plans.test.ts +143 -0
- package/template_scaffold/src/config/billing.plans.ts +69 -0
- package/template_scaffold/src/config/billing.test.plans.ts +32 -0
- package/template_scaffold/src/config/experiments.generated.ts +4 -0
- package/template_scaffold/src/config/experiments.test.ts +12 -0
- package/template_scaffold/src/config/experiments.ts +9 -0
- package/template_scaffold/src/config/funnel.config.ts +12 -0
- package/template_scaffold/src/config/funnel.manifest.test.ts +166 -0
- package/template_scaffold/src/config/funnel.manifest.ts +139 -0
- package/template_scaffold/src/config/offer-sets.generated.ts +4 -0
- package/template_scaffold/src/contract/funnel-project.validation.test.ts +80 -0
- package/template_scaffold/src/contract/funnel-project.validation.ts +44 -0
- package/template_scaffold/src/contract/funnel-validator.vite.config.ts +11 -0
- package/template_scaffold/src/contract/golden/cancellation_offer.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/checkout.fixture.ts +37 -0
- package/template_scaffold/src/contract/golden/form_input.fixture.ts +28 -0
- package/template_scaffold/src/contract/golden/golden-fixtures.test.ts +190 -0
- package/template_scaffold/src/contract/golden/intro_hero.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/multi_select_choice.fixture.ts +37 -0
- package/template_scaffold/src/contract/golden/paywall_offer.fixture.ts +30 -0
- package/template_scaffold/src/contract/golden/progress_interstitial.fixture.ts +28 -0
- package/template_scaffold/src/contract/golden/purchase_completed.fixture.ts +33 -0
- package/template_scaffold/src/contract/golden/single_step_choice.fixture.ts +36 -0
- package/template_scaffold/src/contract/golden/single_step_choice_emoji.fixture.ts +36 -0
- package/template_scaffold/src/contract/golden/social_proof.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/subscription_handoff.fixture.ts +30 -0
- package/template_scaffold/src/contract/golden/subscription_management.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/summary_confirmation.fixture.ts +27 -0
- package/template_scaffold/src/contract/golden/upsell_offer.fixture.ts +29 -0
- package/template_scaffold/src/contract/golden/value_prop_story.fixture.ts +27 -0
- package/template_scaffold/src/contract/validate-funnel.cli.test.ts +180 -0
- package/template_scaffold/src/contract/validate-funnel.cli.ts +199 -0
- package/template_scaffold/src/runtime/checkout-runtime-config.test.ts +60 -0
- package/template_scaffold/src/runtime/checkout-runtime-config.ts +26 -0
- package/template_scaffold/src/runtime/funnel-runtime.test.ts +25 -0
- package/template_scaffold/src/runtime/funnel-runtime.ts +118 -0
- package/template_scaffold/src/runtime/step-content-context.test.ts +38 -0
- package/template_scaffold/src/runtime/step-content-context.ts +65 -0
- package/template_scaffold/src/runtime/step-registry.ts +66 -0
- package/template_scaffold/src/runtime/step-variable-values.ts +31 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.integration.test.ts +203 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.test.ts +76 -0
- package/template_scaffold/src/runtime/use-funnel-flow-controller.ts +53 -0
- package/template_scaffold/src/steps/content/email-capture.content.ts +29 -0
- package/template_scaffold/src/steps/content/manage-subscription.content.ts +62 -0
- package/template_scaffold/src/steps/content/paywall.content.ts +191 -0
- package/template_scaffold/src/steps/content/step-01.content.ts +23 -0
- package/template_scaffold/src/steps/content/step-02.content.ts +77 -0
- package/template_scaffold/src/steps/content/step-03.content.ts +40 -0
- package/template_scaffold/src/steps/content/step-04.content.ts +49 -0
- package/template_scaffold/src/steps/content/step-32-paywall.content.ts +352 -0
- package/template_scaffold/src/steps/content/subscription-started.content.ts +68 -0
- package/template_scaffold/src/steps/editor/email-capture.editor.ts +58 -0
- package/template_scaffold/src/steps/editor/manage-subscription.editor.ts +179 -0
- package/template_scaffold/src/steps/editor/paywall.editor.ts +232 -0
- package/template_scaffold/src/steps/editor/step-01.editor.ts +22 -0
- package/template_scaffold/src/steps/editor/step-02.editor.ts +41 -0
- package/template_scaffold/src/steps/editor/step-03.editor.ts +58 -0
- package/template_scaffold/src/steps/editor/step-04.editor.ts +98 -0
- package/template_scaffold/src/steps/editor/step-32-paywall.editor.ts +478 -0
- package/template_scaffold/src/steps/editor/subscription-started.editor.ts +131 -0
- package/template_scaffold/src/steps/index.ts +25 -0
- package/template_scaffold/src/steps/paywall-checkout-options.test.ts +104 -0
- package/template_scaffold/src/steps/paywall-checkout-options.ts +130 -0
- package/template_scaffold/src/steps/pricing/paywall-b.pricing.ts +12 -0
- package/template_scaffold/src/steps/pricing/paywall.pricing.ts +12 -0
- package/template_scaffold/src/steps/pricing/shared-plan-order.ts +17 -0
- package/template_scaffold/src/steps/pricing/upsell-form.pricing.ts +16 -0
- package/template_scaffold/src/steps/shared-header-contract.test.ts +27 -0
- package/template_scaffold/src/steps/step-01.test.ts +38 -0
- package/template_scaffold/src/steps/step-01.tsx +108 -0
- package/template_scaffold/src/steps/step-02.test.ts +43 -0
- package/template_scaffold/src/steps/step-02.tsx +140 -0
- package/template_scaffold/src/steps/step-03.test.ts +15 -0
- package/template_scaffold/src/steps/step-03.tsx +262 -0
- package/template_scaffold/src/steps/step-04.test.ts +32 -0
- package/template_scaffold/src/steps/step-04.tsx +673 -0
- package/template_scaffold/src/steps/step-31-email-capture.test.ts +22 -0
- package/template_scaffold/src/steps/step-31-email-capture.tsx +195 -0
- package/template_scaffold/src/steps/step-32-paywall.test.ts +770 -0
- package/template_scaffold/src/steps/step-32-paywall.tsx +2253 -0
- package/template_scaffold/src/steps/step-32b-paywall-b.tsx +35 -0
- package/template_scaffold/src/steps/step-33-subscription-started.test.ts +26 -0
- package/template_scaffold/src/steps/step-33-subscription-started.tsx +86 -0
- package/template_scaffold/src/steps/step-34-upsell-form.test.ts +27 -0
- package/template_scaffold/src/steps/step-35-manage-subscription.tsx +46 -0
- package/template_scaffold/src/steps/step-content.registry.ts +45 -0
- package/template_scaffold/src/steps/step-image-migration.test.ts +20 -0
- package/template_scaffold/src/steps/styles/index.css +2 -0
- package/template_scaffold/src/steps/styles/shared/final-flow-shared.css +55 -0
- package/template_scaffold/src/steps/styles/shared/funnel-base.css +543 -0
- package/template_scaffold/src/steps/styles/shared/info-shared.css +139 -0
- package/template_scaffold/src/steps/styles/shared/progress-shared.css +117 -0
- package/template_scaffold/src/steps/styles/shared/question-shared.css +76 -0
- package/template_scaffold/src/steps/styles/shared/responsive-container.css +176 -0
- package/template_scaffold/src/steps/types.ts +7 -0
- package/template_scaffold/src/steps/use-claimbee-step-content.ts +15 -0
- package/template_scaffold/src/theme/theme.ts +81 -0
- package/template_scaffold/tsconfig.json +38 -0
- package/template_scaffold/vitest.config.ts +32 -0
- package/template_validation/.funnelsgrove-contract-tools.json +19 -0
- package/template_validation/src/contract/funnel-project.validation.ts +44 -0
- package/template_validation/src/contract/funnel-validator.vite.config.ts +11 -0
- package/template_validation/src/contract/validate-funnel.cli.ts +199 -0
- package/template_docs/docs/ab-experiments.md +0 -55
- package/template_docs/docs/analytics.md +0 -65
- package/template_docs/docs/editing-flow.md +0 -125
- package/template_docs/docs/editing-step.md +0 -125
- package/template_docs/docs/editor-and-content.md +0 -58
- package/template_docs/docs/funnel-runtime-architecture.md +0 -125
- package/template_docs/docs/meta-pixel-conversions-api.md +0 -50
- package/template_docs/docs/payment-plans-and-discounts.md +0 -106
- package/template_docs/docs/publishing-and-versioning.md +0 -117
- package/template_docs/docs/qa-checklist.md +0 -80
- package/template_docs/docs/sdk-api-endpoints.md +0 -78
- package/template_docs/docs/step-ui-guidelines.md +0 -110
- package/template_docs/docs/theme.md +0 -35
|
@@ -23,6 +23,26 @@ export type AnalyticsOverviewResponse = {
|
|
|
23
23
|
chargedRevenueMinor: number;
|
|
24
24
|
}>;
|
|
25
25
|
};
|
|
26
|
+
export type AnalyticsFunnelFlow = {
|
|
27
|
+
steps: Array<{
|
|
28
|
+
stepId: string;
|
|
29
|
+
stepName: string | null;
|
|
30
|
+
stepPath: string | null;
|
|
31
|
+
stepTitle: string | null;
|
|
32
|
+
stepType: string | null;
|
|
33
|
+
stepKind: string | null;
|
|
34
|
+
stepOrder: number;
|
|
35
|
+
}>;
|
|
36
|
+
experiments: unknown[];
|
|
37
|
+
branches: unknown[];
|
|
38
|
+
};
|
|
39
|
+
export type PublishedFunnelFlowResponse = {
|
|
40
|
+
funnelId: string;
|
|
41
|
+
publishedVersionId: string | null;
|
|
42
|
+
status: 'ready' | 'syncing' | 'unpublished';
|
|
43
|
+
flow: AnalyticsFunnelFlow | null;
|
|
44
|
+
updatedAt: string | null;
|
|
45
|
+
};
|
|
26
46
|
export type AnalyticsStepDropoffResponse = {
|
|
27
47
|
generatedAt: string;
|
|
28
48
|
from: string | null;
|
|
@@ -61,19 +81,7 @@ export type AnalyticsStepDropoffResponse = {
|
|
|
61
81
|
chartPoints: unknown[];
|
|
62
82
|
}>;
|
|
63
83
|
steps: AnalyticsStepTransition[];
|
|
64
|
-
flow:
|
|
65
|
-
steps: Array<{
|
|
66
|
-
stepId: string;
|
|
67
|
-
stepName: string | null;
|
|
68
|
-
stepPath: string | null;
|
|
69
|
-
stepTitle: string | null;
|
|
70
|
-
stepType: string | null;
|
|
71
|
-
stepKind: string | null;
|
|
72
|
-
stepOrder: number;
|
|
73
|
-
}>;
|
|
74
|
-
experiments: unknown[];
|
|
75
|
-
branches: unknown[];
|
|
76
|
-
} | null;
|
|
84
|
+
flow: AnalyticsFunnelFlow | null;
|
|
77
85
|
};
|
|
78
86
|
export type AnalyticsStepTransition = {
|
|
79
87
|
stepId: string;
|
|
@@ -97,6 +105,9 @@ export type MarketingCohortPerformanceRow = {
|
|
|
97
105
|
subscribersAlive: number;
|
|
98
106
|
subscribersAlivePercent: number | null;
|
|
99
107
|
revenue: number;
|
|
108
|
+
revenueDay3: number;
|
|
109
|
+
revenueDay90: number;
|
|
110
|
+
revenueDay180: number;
|
|
100
111
|
pLtv: number | null;
|
|
101
112
|
pLtvPredictedNetRevenueDay365Minor: number;
|
|
102
113
|
pLtvPredictedCustomerCount: number;
|
|
@@ -130,7 +141,7 @@ export declare const parseAnalyticsDate: (value: string) => ParsedAnalyticsDate;
|
|
|
130
141
|
export declare const normalizeAnalyticsOutputFormat: (value: string | undefined) => AnalyticsOutputFormat;
|
|
131
142
|
export declare const assertHasConversionData: (date: string, overview: AnalyticsOverviewResponse, stepDropoff: AnalyticsStepDropoffResponse) => void;
|
|
132
143
|
export declare const assertHasTransitionData: (date: string, stepDropoff: AnalyticsStepDropoffResponse) => void;
|
|
133
|
-
export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse) => void;
|
|
144
|
+
export declare const assertHasFunnelPathData: (date: string, stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => void;
|
|
134
145
|
export declare const assertHasCohortData: (date: string, report: MarketingCohortPerformanceReport) => void;
|
|
135
146
|
export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResponse) => {
|
|
136
147
|
stepId: string;
|
|
@@ -141,7 +152,7 @@ export declare const buildTransitionRows: (stepDropoff: AnalyticsStepDropoffResp
|
|
|
141
152
|
dropoffs: number;
|
|
142
153
|
dropoffRate: number;
|
|
143
154
|
}[];
|
|
144
|
-
export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse) => {
|
|
155
|
+
export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResponse, publishedFunnelFlow: PublishedFunnelFlowResponse) => {
|
|
145
156
|
stepId: string;
|
|
146
157
|
path: string;
|
|
147
158
|
label: string;
|
|
@@ -154,6 +165,7 @@ export declare const buildFunnelPathRows: (stepDropoff: AnalyticsStepDropoffResp
|
|
|
154
165
|
export declare const buildConversionReportPayload: (input: ReportContext & {
|
|
155
166
|
overview: AnalyticsOverviewResponse;
|
|
156
167
|
stepDropoff: AnalyticsStepDropoffResponse;
|
|
168
|
+
publishedFunnelFlow: PublishedFunnelFlowResponse;
|
|
157
169
|
}) => {
|
|
158
170
|
generatedAt: string;
|
|
159
171
|
date: string;
|
|
@@ -199,6 +211,7 @@ export declare const buildConversionReportPayload: (input: ReportContext & {
|
|
|
199
211
|
};
|
|
200
212
|
export declare const buildFunnelPathReportPayload: (input: ReportContext & {
|
|
201
213
|
stepDropoff: AnalyticsStepDropoffResponse;
|
|
214
|
+
publishedFunnelFlow: PublishedFunnelFlowResponse;
|
|
202
215
|
}) => {
|
|
203
216
|
generatedAt: string;
|
|
204
217
|
date: string;
|
|
@@ -263,11 +276,13 @@ export declare const formatTransitionsTable: (input: {
|
|
|
263
276
|
export declare const formatFunnelPathTable: (input: {
|
|
264
277
|
date: string;
|
|
265
278
|
stepDropoff: AnalyticsStepDropoffResponse;
|
|
279
|
+
publishedFunnelFlow: PublishedFunnelFlowResponse;
|
|
266
280
|
}) => string;
|
|
267
281
|
export declare const formatConversionsTable: (input: {
|
|
268
282
|
date: string;
|
|
269
283
|
overview: AnalyticsOverviewResponse;
|
|
270
284
|
stepDropoff: AnalyticsStepDropoffResponse;
|
|
285
|
+
publishedFunnelFlow: PublishedFunnelFlowResponse;
|
|
271
286
|
}) => string;
|
|
272
287
|
export declare const formatCohortTable: (input: {
|
|
273
288
|
date: string;
|
package/dist/analyticsOutput.js
CHANGED
|
@@ -55,8 +55,8 @@ export const assertHasTransitionData = (date, stepDropoff) => {
|
|
|
55
55
|
}
|
|
56
56
|
throw new Error(`No synced step transition data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked step traffic that day.`);
|
|
57
57
|
};
|
|
58
|
-
export const assertHasFunnelPathData = (date, stepDropoff) => {
|
|
59
|
-
if (buildFunnelPathRows(stepDropoff).length > 0
|
|
58
|
+
export const assertHasFunnelPathData = (date, stepDropoff, publishedFunnelFlow) => {
|
|
59
|
+
if (buildFunnelPathRows(stepDropoff, publishedFunnelFlow).length > 0) {
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
throw new Error(`No synced full funnel path data found for ${date}. Analytics may not have synced yet, or the funnel had no tracked path traffic that day.`);
|
|
@@ -85,13 +85,6 @@ const formatCurrency = (value) => new Intl.NumberFormat('en-US', {
|
|
|
85
85
|
}).format(value);
|
|
86
86
|
const formatMinorCurrency = (value) => formatCurrency((Number(value) || 0) / 100);
|
|
87
87
|
const stepDisplayName = (step) => step.stepName || step.stepId;
|
|
88
|
-
const flowStepDisplay = (stepDropoff, step) => {
|
|
89
|
-
const flowStep = stepDropoff.flow?.steps.find((candidate) => candidate.stepId === step.stepId);
|
|
90
|
-
return {
|
|
91
|
-
label: flowStep?.stepTitle || step.stepName || flowStep?.stepName || step.stepId,
|
|
92
|
-
path: flowStep?.stepPath || step.stepId,
|
|
93
|
-
};
|
|
94
|
-
};
|
|
95
88
|
export const buildTransitionRows = (stepDropoff) => [...stepDropoff.steps]
|
|
96
89
|
.sort((left, right) => left.stepOrder - right.stepOrder || left.stepId.localeCompare(right.stepId))
|
|
97
90
|
.map((step) => ({
|
|
@@ -103,20 +96,28 @@ export const buildTransitionRows = (stepDropoff) => [...stepDropoff.steps]
|
|
|
103
96
|
dropoffs: step.dropoffs,
|
|
104
97
|
dropoffRate: step.dropoffRate,
|
|
105
98
|
}));
|
|
106
|
-
export const buildFunnelPathRows = (stepDropoff
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
export const buildFunnelPathRows = (stepDropoff, publishedFunnelFlow) => {
|
|
100
|
+
if (publishedFunnelFlow.status !== 'ready' || !publishedFunnelFlow.flow) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const transitionsByStepId = new Map(stepDropoff.steps.map((step) => [step.stepId, step]));
|
|
104
|
+
return [...publishedFunnelFlow.flow.steps]
|
|
105
|
+
.sort((left, right) => left.stepOrder - right.stepOrder || left.stepId.localeCompare(right.stepId))
|
|
106
|
+
.map((flowStep) => {
|
|
107
|
+
const step = transitionsByStepId.get(flowStep.stepId);
|
|
108
|
+
const entrants = step?.entrants ?? 0;
|
|
109
|
+
return {
|
|
110
|
+
stepId: flowStep.stepId,
|
|
111
|
+
path: flowStep.stepPath || flowStep.stepId,
|
|
112
|
+
label: flowStep.stepTitle || flowStep.stepName || flowStep.stepId,
|
|
113
|
+
users: entrants,
|
|
114
|
+
advancedToNextStep: step?.advancedToNextStep ?? 0,
|
|
115
|
+
conversionToNextStep: entrants > 0 ? (step?.advancedToNextStep ?? 0) / entrants : 0,
|
|
116
|
+
dropoffs: step?.dropoffs ?? 0,
|
|
117
|
+
dropoffRate: step?.dropoffRate ?? 0,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
};
|
|
120
121
|
export const buildConversionReportPayload = (input) => ({
|
|
121
122
|
generatedAt: new Date().toISOString(),
|
|
122
123
|
date: input.date,
|
|
@@ -126,7 +127,7 @@ export const buildConversionReportPayload = (input) => ({
|
|
|
126
127
|
overview: input.overview,
|
|
127
128
|
totals: input.stepDropoff.totals,
|
|
128
129
|
metrics: input.stepDropoff.metrics,
|
|
129
|
-
funnelPath: buildFunnelPathRows(input.stepDropoff),
|
|
130
|
+
funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
|
|
130
131
|
transitions: buildTransitionRows(input.stepDropoff),
|
|
131
132
|
});
|
|
132
133
|
export const buildFunnelPathReportPayload = (input) => ({
|
|
@@ -135,7 +136,7 @@ export const buildFunnelPathReportPayload = (input) => ({
|
|
|
135
136
|
workspaceId: input.workspaceId,
|
|
136
137
|
project: input.project,
|
|
137
138
|
funnel: input.stepDropoff.funnel,
|
|
138
|
-
funnelPath: buildFunnelPathRows(input.stepDropoff),
|
|
139
|
+
funnelPath: buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow),
|
|
139
140
|
});
|
|
140
141
|
export const buildTransitionReportPayload = (input) => ({
|
|
141
142
|
generatedAt: new Date().toISOString(),
|
|
@@ -176,7 +177,7 @@ export const formatFunnelPathTable = (input) => {
|
|
|
176
177
|
`Full funnel path\t${input.date}\t${input.stepDropoff.funnel.name}`,
|
|
177
178
|
'step\tpath\tusers\tadvanced\tconversionToNext\tdropoffs\tdropoffRate',
|
|
178
179
|
];
|
|
179
|
-
for (const row of buildFunnelPathRows(input.stepDropoff)) {
|
|
180
|
+
for (const row of buildFunnelPathRows(input.stepDropoff, input.publishedFunnelFlow)) {
|
|
180
181
|
lines.push([
|
|
181
182
|
row.label,
|
|
182
183
|
row.path,
|
|
@@ -219,6 +220,7 @@ export const formatConversionsTable = (input) => {
|
|
|
219
220
|
lines.push('', formatFunnelPathTable({
|
|
220
221
|
date: input.date,
|
|
221
222
|
stepDropoff: input.stepDropoff,
|
|
223
|
+
publishedFunnelFlow: input.publishedFunnelFlow,
|
|
222
224
|
}).trimEnd());
|
|
223
225
|
return `${lines.join('\n')}\n`;
|
|
224
226
|
};
|
|
@@ -229,14 +231,24 @@ const predictedAverageLtv = (row) => {
|
|
|
229
231
|
}
|
|
230
232
|
return Number(row.pLtv) || 0;
|
|
231
233
|
};
|
|
234
|
+
const ratioToSpend = (value, spend) => (Number.isFinite(value) && Number.isFinite(spend) && spend !== 0 ? value / spend : null);
|
|
235
|
+
const formatRoi = (profit, spend) => {
|
|
236
|
+
const ratio = ratioToSpend(profit, spend);
|
|
237
|
+
return ratio === null ? '' : `${(ratio * 100).toFixed(2)}%`;
|
|
238
|
+
};
|
|
239
|
+
const formatRoas = (revenue, spend) => {
|
|
240
|
+
const ratio = ratioToSpend(revenue, spend);
|
|
241
|
+
return ratio === null ? '' : `${ratio.toFixed(2)}x`;
|
|
242
|
+
};
|
|
232
243
|
export const formatCohortTable = (input) => {
|
|
233
244
|
const lines = [
|
|
234
245
|
`Cohort\t${input.date}`,
|
|
235
|
-
'cohort\tmediaSource\tfunnelUrl\tspend\tsubscribers\taliveRate\trevenue\tpLtv\tcpa\tpredictedProfit',
|
|
246
|
+
'cohort\tmediaSource\tfunnelUrl\tspend\tsubscribers\taliveRate\trevenue\tpLtv\tcpa\tpredictedProfit\tpROI\tROAS\tROAS d3\tROAS d90\tROAS d180',
|
|
236
247
|
];
|
|
237
248
|
for (const row of flattenCohortRows(input.cohort.rows)) {
|
|
238
249
|
const cpa = row.subscribers > 0 ? row.spend / row.subscribers : 0;
|
|
239
250
|
const predictedRevenue = predictedAverageLtv(row) * row.subscribers;
|
|
251
|
+
const predictedProfit = predictedRevenue - row.spend;
|
|
240
252
|
lines.push([
|
|
241
253
|
row.cohortDate,
|
|
242
254
|
row.mediaSource,
|
|
@@ -247,7 +259,12 @@ export const formatCohortTable = (input) => {
|
|
|
247
259
|
formatCurrency(row.revenue),
|
|
248
260
|
row.pLtv === null ? '' : formatCurrency(row.pLtv),
|
|
249
261
|
row.subscribers > 0 ? formatCurrency(cpa) : '',
|
|
250
|
-
formatCurrency(
|
|
262
|
+
formatCurrency(predictedProfit),
|
|
263
|
+
formatRoi(predictedProfit, row.spend),
|
|
264
|
+
formatRoas(row.revenue, row.spend),
|
|
265
|
+
formatRoas(row.revenueDay3, row.spend),
|
|
266
|
+
formatRoas(row.revenueDay90, row.spend),
|
|
267
|
+
formatRoas(row.revenueDay180, row.spend),
|
|
251
268
|
].join('\t'));
|
|
252
269
|
}
|
|
253
270
|
return `${lines.join('\n')}\n`;
|
package/dist/apiClient.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type FunnelContractDiagnosticCode } from '@funnelsgrove/runtime';
|
|
2
|
+
import type { SourceCandidateOperation } from './localSync.js';
|
|
1
3
|
type FetchFn = typeof fetch;
|
|
2
4
|
type TrpcProcedureType = 'query' | 'mutation';
|
|
3
5
|
export type TrpcCallInput = {
|
|
@@ -7,7 +9,90 @@ export type TrpcCallInput = {
|
|
|
7
9
|
input?: unknown;
|
|
8
10
|
token?: string | null;
|
|
9
11
|
fetchFn?: FetchFn;
|
|
12
|
+
signal?: AbortSignal;
|
|
10
13
|
};
|
|
14
|
+
export type FunnelSourceCandidateApiContext = {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
token?: string | null;
|
|
17
|
+
fetchFn?: FetchFn;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
};
|
|
20
|
+
export type FunnelSourceCandidateStatus = 'staging' | 'validating' | 'committed' | 'aborted' | 'expired' | 'failed';
|
|
21
|
+
export type BeginFunnelSourceCandidateInput = {
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
funnelId: string;
|
|
24
|
+
baseDraftVersionId: string | null;
|
|
25
|
+
idempotencyKey: string;
|
|
26
|
+
};
|
|
27
|
+
export type StageFunnelSourceCandidateInput = {
|
|
28
|
+
workspaceId: string;
|
|
29
|
+
candidateId: string;
|
|
30
|
+
idempotencyKey: string;
|
|
31
|
+
operations: SourceCandidateOperation[];
|
|
32
|
+
};
|
|
33
|
+
export type FinishFunnelSourceCandidateInput = {
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
candidateId: string;
|
|
36
|
+
};
|
|
37
|
+
export type FinalizeFunnelSourceCandidateInput = FinishFunnelSourceCandidateInput & {
|
|
38
|
+
message?: string;
|
|
39
|
+
};
|
|
40
|
+
export type FunnelSourceCandidateSummary = {
|
|
41
|
+
candidateId: string;
|
|
42
|
+
status: FunnelSourceCandidateStatus;
|
|
43
|
+
baseDraftVersionId: string | null;
|
|
44
|
+
fileCount?: number;
|
|
45
|
+
byteCount?: number;
|
|
46
|
+
batchCount?: number;
|
|
47
|
+
};
|
|
48
|
+
export type StageFunnelSourceCandidateResult = {
|
|
49
|
+
candidateId: string;
|
|
50
|
+
stageId: string;
|
|
51
|
+
batchIndex: number;
|
|
52
|
+
operationCount: number;
|
|
53
|
+
fileCount: number;
|
|
54
|
+
byteCount: number;
|
|
55
|
+
totalOperationCount: number;
|
|
56
|
+
idempotent: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type FinalizeFunnelSourceCandidateResult = {
|
|
59
|
+
candidateId: string;
|
|
60
|
+
projectId: string;
|
|
61
|
+
funnelId: string;
|
|
62
|
+
versionId: string;
|
|
63
|
+
status: 'committed';
|
|
64
|
+
idempotent: boolean;
|
|
65
|
+
diagnostics: readonly FunnelContractApiDiagnostic[];
|
|
66
|
+
};
|
|
67
|
+
export declare const FUNNEL_CONTRACT_API_INGRESSES: readonly ["agent-transaction", "cli-sync", "github-sync", "builder-authoring", "preview-publish", "production-publish", "published-artifact-serving"];
|
|
68
|
+
export type FunnelContractApiIngress = (typeof FUNNEL_CONTRACT_API_INGRESSES)[number];
|
|
69
|
+
export type FunnelContractApiDiagnostic = {
|
|
70
|
+
code: FunnelContractDiagnosticCode;
|
|
71
|
+
severity: 'error';
|
|
72
|
+
ingress: FunnelContractApiIngress;
|
|
73
|
+
file: string | null;
|
|
74
|
+
stepId: string | null;
|
|
75
|
+
expected: unknown;
|
|
76
|
+
received: unknown;
|
|
77
|
+
reason: string;
|
|
78
|
+
guide: string;
|
|
79
|
+
repair: string;
|
|
80
|
+
};
|
|
81
|
+
export type FunnelContractApiErrorData = {
|
|
82
|
+
schemaVersion: 1;
|
|
83
|
+
contractVersion: number;
|
|
84
|
+
ingress: FunnelContractApiIngress;
|
|
85
|
+
diagnostics: readonly FunnelContractApiDiagnostic[];
|
|
86
|
+
};
|
|
87
|
+
export declare class FunnelContractApiError extends Error {
|
|
88
|
+
readonly funnelContract: FunnelContractApiErrorData;
|
|
89
|
+
constructor(message: string, funnelContract: FunnelContractApiErrorData);
|
|
90
|
+
}
|
|
91
|
+
export declare const parseFunnelContractApiErrorData: (value: unknown) => FunnelContractApiErrorData | null;
|
|
11
92
|
export declare function parseTrpcJsonResponse<T = unknown>(response: unknown): T;
|
|
12
93
|
export declare function callTrpcProcedure<T = unknown>(input: TrpcCallInput): Promise<T>;
|
|
94
|
+
export declare const beginFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: BeginFunnelSourceCandidateInput) => Promise<FunnelSourceCandidateSummary>;
|
|
95
|
+
export declare const stageFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: StageFunnelSourceCandidateInput) => Promise<StageFunnelSourceCandidateResult>;
|
|
96
|
+
export declare const finalizeFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: FinalizeFunnelSourceCandidateInput) => Promise<FinalizeFunnelSourceCandidateResult>;
|
|
97
|
+
export declare const abortFunnelSourceCandidate: (context: FunnelSourceCandidateApiContext, input: FinishFunnelSourceCandidateInput) => Promise<FunnelSourceCandidateSummary>;
|
|
13
98
|
export {};
|
package/dist/apiClient.js
CHANGED
|
@@ -1,22 +1,227 @@
|
|
|
1
|
+
import { CURRENT_STEP_CONTRACT_VERSION, FUNNEL_CONTRACT_DIAGNOSTIC_CODES, } from '@funnelsgrove/runtime';
|
|
2
|
+
export const FUNNEL_CONTRACT_API_INGRESSES = Object.freeze([
|
|
3
|
+
'agent-transaction',
|
|
4
|
+
'cli-sync',
|
|
5
|
+
'github-sync',
|
|
6
|
+
'builder-authoring',
|
|
7
|
+
'preview-publish',
|
|
8
|
+
'production-publish',
|
|
9
|
+
'published-artifact-serving',
|
|
10
|
+
]);
|
|
11
|
+
export class FunnelContractApiError extends Error {
|
|
12
|
+
funnelContract;
|
|
13
|
+
constructor(message, funnelContract) {
|
|
14
|
+
super(`tRPC request failed: ${message}`);
|
|
15
|
+
this.name = 'FunnelContractApiError';
|
|
16
|
+
this.funnelContract = funnelContract;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const ENVELOPE_KEYS = Object.freeze([
|
|
20
|
+
'schemaVersion',
|
|
21
|
+
'contractVersion',
|
|
22
|
+
'ingress',
|
|
23
|
+
'diagnostics',
|
|
24
|
+
]);
|
|
25
|
+
const DIAGNOSTIC_KEYS = Object.freeze([
|
|
26
|
+
'code',
|
|
27
|
+
'severity',
|
|
28
|
+
'ingress',
|
|
29
|
+
'file',
|
|
30
|
+
'stepId',
|
|
31
|
+
'expected',
|
|
32
|
+
'received',
|
|
33
|
+
'reason',
|
|
34
|
+
'guide',
|
|
35
|
+
'repair',
|
|
36
|
+
]);
|
|
37
|
+
const DIAGNOSTIC_CODE_SET = new Set(FUNNEL_CONTRACT_DIAGNOSTIC_CODES);
|
|
38
|
+
const INGRESS_SET = new Set(FUNNEL_CONTRACT_API_INGRESSES);
|
|
39
|
+
const MAX_DIAGNOSTICS = 256;
|
|
40
|
+
const MAX_JSON_DEPTH = 8;
|
|
41
|
+
const MAX_JSON_ITEMS = 256;
|
|
42
|
+
const MAX_TRANSPORT_STRING_LENGTH = 4_096;
|
|
43
|
+
const UNSAFE_JSON_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
1
44
|
function isObject(value) {
|
|
2
|
-
|
|
45
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
const prototype = Object.getPrototypeOf(value);
|
|
49
|
+
return prototype === Object.prototype || prototype === null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
3
54
|
}
|
|
55
|
+
const ownDataValue = (value, key) => {
|
|
56
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
57
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
58
|
+
};
|
|
59
|
+
const hasExactDataProperties = (value, keys) => {
|
|
60
|
+
if (!isObject(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
let ownKeys;
|
|
64
|
+
try {
|
|
65
|
+
ownKeys = Reflect.ownKeys(value);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (ownKeys.length !== keys.length || ownKeys.some((key) => typeof key !== 'string')) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const expectedKeys = new Set(keys);
|
|
74
|
+
return ownKeys.every((key) => {
|
|
75
|
+
if (typeof key !== 'string' || !expectedKeys.has(key)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
79
|
+
return descriptor?.enumerable === true && Object.hasOwn(descriptor, 'value');
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const readDenseDataArray = (value, maximum) => {
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
return null;
|
|
85
|
+
const descriptors = (() => {
|
|
86
|
+
try {
|
|
87
|
+
return Object.getOwnPropertyDescriptors(value);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
if (descriptors === null)
|
|
94
|
+
return null;
|
|
95
|
+
const length = descriptors.length?.value;
|
|
96
|
+
if (typeof length !== 'number' || !Number.isSafeInteger(length) || length < 0 || length > maximum) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const keys = Reflect.ownKeys(descriptors);
|
|
100
|
+
if (keys.length !== length + 1
|
|
101
|
+
|| keys.some((key) => (typeof key !== 'string'
|
|
102
|
+
|| (key !== 'length' && !/^(?:0|[1-9]\d*)$/.test(key)))))
|
|
103
|
+
return null;
|
|
104
|
+
const result = [];
|
|
105
|
+
for (let index = 0; index < length; index += 1) {
|
|
106
|
+
const descriptor = descriptors[String(index)];
|
|
107
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value'))
|
|
108
|
+
return null;
|
|
109
|
+
result.push(descriptor.value);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
};
|
|
113
|
+
const isJsonValue = (value, seen = new Set(), depth = 0) => {
|
|
114
|
+
if (value === null
|
|
115
|
+
|| typeof value === 'boolean') {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === 'string')
|
|
119
|
+
return value.length <= MAX_TRANSPORT_STRING_LENGTH;
|
|
120
|
+
if (typeof value === 'number') {
|
|
121
|
+
return Number.isFinite(value);
|
|
122
|
+
}
|
|
123
|
+
if (typeof value !== 'object' || seen.has(value) || depth >= MAX_JSON_DEPTH) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
seen.add(value);
|
|
127
|
+
try {
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
const items = readDenseDataArray(value, MAX_JSON_ITEMS);
|
|
130
|
+
return items !== null && items.every((item) => isJsonValue(item, seen, depth + 1));
|
|
131
|
+
}
|
|
132
|
+
const prototype = Object.getPrototypeOf(value);
|
|
133
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const keys = Reflect.ownKeys(value);
|
|
137
|
+
return keys.length <= MAX_JSON_ITEMS && keys.every((key) => {
|
|
138
|
+
if (typeof key !== 'string') {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (UNSAFE_JSON_KEYS.has(key))
|
|
142
|
+
return false;
|
|
143
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
144
|
+
return descriptor?.enumerable === true
|
|
145
|
+
&& Object.hasOwn(descriptor, 'value')
|
|
146
|
+
&& isJsonValue(descriptor.value, seen, depth + 1);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
seen.delete(value);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const isNullableString = (value) => (value === null
|
|
154
|
+
|| (typeof value === 'string' && value.length <= MAX_TRANSPORT_STRING_LENGTH));
|
|
155
|
+
const isBoundedString = (value) => (typeof value === 'string' && value.length <= MAX_TRANSPORT_STRING_LENGTH);
|
|
156
|
+
const isFunnelContractApiDiagnostic = (value, ingress) => {
|
|
157
|
+
if (!hasExactDataProperties(value, DIAGNOSTIC_KEYS))
|
|
158
|
+
return false;
|
|
159
|
+
const code = ownDataValue(value, 'code');
|
|
160
|
+
return typeof code === 'string'
|
|
161
|
+
&& DIAGNOSTIC_CODE_SET.has(code)
|
|
162
|
+
&& ownDataValue(value, 'severity') === 'error'
|
|
163
|
+
&& ownDataValue(value, 'ingress') === ingress
|
|
164
|
+
&& isNullableString(ownDataValue(value, 'file'))
|
|
165
|
+
&& isNullableString(ownDataValue(value, 'stepId'))
|
|
166
|
+
&& isJsonValue(ownDataValue(value, 'expected'))
|
|
167
|
+
&& isJsonValue(ownDataValue(value, 'received'))
|
|
168
|
+
&& isBoundedString(ownDataValue(value, 'reason'))
|
|
169
|
+
&& isBoundedString(ownDataValue(value, 'guide'))
|
|
170
|
+
&& isBoundedString(ownDataValue(value, 'repair'));
|
|
171
|
+
};
|
|
172
|
+
export const parseFunnelContractApiErrorData = (value) => {
|
|
173
|
+
if (!hasExactDataProperties(value, ENVELOPE_KEYS)
|
|
174
|
+
|| ownDataValue(value, 'schemaVersion') !== 1
|
|
175
|
+
|| ownDataValue(value, 'contractVersion') !== CURRENT_STEP_CONTRACT_VERSION) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const ingressValue = ownDataValue(value, 'ingress');
|
|
179
|
+
if (typeof ingressValue !== 'string' || !INGRESS_SET.has(ingressValue))
|
|
180
|
+
return null;
|
|
181
|
+
const diagnostics = readDenseDataArray(ownDataValue(value, 'diagnostics'), MAX_DIAGNOSTICS);
|
|
182
|
+
if (diagnostics === null)
|
|
183
|
+
return null;
|
|
184
|
+
const ingress = ingressValue;
|
|
185
|
+
return diagnostics.every((diagnostic) => (isFunnelContractApiDiagnostic(diagnostic, ingress))) ? value : null;
|
|
186
|
+
};
|
|
187
|
+
const readFunnelContractApiErrorData = (error) => {
|
|
188
|
+
const data = ownDataValue(error, 'data');
|
|
189
|
+
if (!isObject(data)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const descriptor = Object.getOwnPropertyDescriptor(data, 'funnelContract');
|
|
193
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
194
|
+
? parseFunnelContractApiErrorData(descriptor.value)
|
|
195
|
+
: null;
|
|
196
|
+
};
|
|
4
197
|
export function parseTrpcJsonResponse(response) {
|
|
5
198
|
if (!isObject(response)) {
|
|
6
199
|
throw new Error('Invalid tRPC response');
|
|
7
200
|
}
|
|
8
|
-
|
|
9
|
-
|
|
201
|
+
const errorValue = ownDataValue(response, 'error');
|
|
202
|
+
if (isObject(errorValue)) {
|
|
203
|
+
const messageValue = ownDataValue(errorValue, 'message');
|
|
204
|
+
const message = typeof messageValue === 'string' ? messageValue : 'Unknown error';
|
|
205
|
+
const funnelContract = readFunnelContractApiErrorData(errorValue);
|
|
206
|
+
if (funnelContract) {
|
|
207
|
+
throw new FunnelContractApiError(message, funnelContract);
|
|
208
|
+
}
|
|
10
209
|
throw new Error(`tRPC request failed: ${message}`);
|
|
11
210
|
}
|
|
12
|
-
const result = response
|
|
13
|
-
if (!isObject(result)
|
|
211
|
+
const result = ownDataValue(response, 'result');
|
|
212
|
+
if (!isObject(result)) {
|
|
14
213
|
throw new Error('Invalid tRPC response');
|
|
15
214
|
}
|
|
16
|
-
|
|
17
|
-
|
|
215
|
+
const dataDescriptor = Object.getOwnPropertyDescriptor(result, 'data');
|
|
216
|
+
if (!dataDescriptor || !Object.hasOwn(dataDescriptor, 'value'))
|
|
217
|
+
throw new Error('Invalid tRPC response');
|
|
218
|
+
const data = dataDescriptor.value;
|
|
219
|
+
if (isObject(data)) {
|
|
220
|
+
const jsonDescriptor = Object.getOwnPropertyDescriptor(data, 'json');
|
|
221
|
+
if (jsonDescriptor && Object.hasOwn(jsonDescriptor, 'value'))
|
|
222
|
+
return jsonDescriptor.value;
|
|
18
223
|
}
|
|
19
|
-
return
|
|
224
|
+
return data;
|
|
20
225
|
}
|
|
21
226
|
const trimTrailingSlash = (value) => value.trim().replace(/\/+$/, '');
|
|
22
227
|
const buildHeaders = (input) => {
|
|
@@ -36,11 +241,13 @@ export async function callTrpcProcedure(input) {
|
|
|
36
241
|
? {
|
|
37
242
|
method: 'GET',
|
|
38
243
|
headers: buildHeaders({ token: input.token }),
|
|
244
|
+
signal: input.signal,
|
|
39
245
|
}
|
|
40
246
|
: {
|
|
41
247
|
method: 'POST',
|
|
42
248
|
headers: buildHeaders({ token: input.token, contentType: 'application/json' }),
|
|
43
249
|
body: JSON.stringify(input.input ?? {}),
|
|
250
|
+
signal: input.signal,
|
|
44
251
|
};
|
|
45
252
|
const url = input.type === 'query' && input.input !== undefined
|
|
46
253
|
? `${baseUrl}?input=${encodeURIComponent(JSON.stringify(input.input))}`
|
|
@@ -59,3 +266,13 @@ export async function callTrpcProcedure(input) {
|
|
|
59
266
|
}
|
|
60
267
|
return parseTrpcJsonResponse(json);
|
|
61
268
|
}
|
|
269
|
+
const callFunnelSourceCandidateMutation = (context, path, input) => callTrpcProcedure({
|
|
270
|
+
...context,
|
|
271
|
+
path,
|
|
272
|
+
type: 'mutation',
|
|
273
|
+
input,
|
|
274
|
+
});
|
|
275
|
+
export const beginFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.beginSourceCandidate', input);
|
|
276
|
+
export const stageFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.stageSourceCandidate', input);
|
|
277
|
+
export const finalizeFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.finalizeSourceCandidate', input);
|
|
278
|
+
export const abortFunnelSourceCandidate = (context, input) => callFunnelSourceCandidateMutation(context, 'funnels.abortSourceCandidate', input);
|