@funnelsgrove/cli 0.1.14 → 0.1.15
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/localSync.js
CHANGED
|
@@ -198,12 +198,12 @@ function formatByteSize(byteCount) {
|
|
|
198
198
|
async function readSourceFiles(rootDir, manifestFiles) {
|
|
199
199
|
const files = await Promise.all(manifestFiles.map(async (file) => {
|
|
200
200
|
const absolutePath = path.join(rootDir, assertSafeSyncPath(file.path));
|
|
201
|
-
const
|
|
201
|
+
const binaryContentType = inferBinaryContentType(file.path);
|
|
202
202
|
const buffer = await readFile(absolutePath);
|
|
203
203
|
return {
|
|
204
204
|
path: file.path,
|
|
205
|
-
content:
|
|
206
|
-
contentType:
|
|
205
|
+
content: binaryContentType ? `data:${binaryContentType};base64,${buffer.toString('base64')}` : buffer.toString('utf8'),
|
|
206
|
+
contentType: binaryContentType || 'text/plain',
|
|
207
207
|
};
|
|
208
208
|
}));
|
|
209
209
|
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
@@ -242,27 +242,31 @@ async function collectSyncFiles(rootDir, dir) {
|
|
|
242
242
|
async function hashFile(filePath) {
|
|
243
243
|
return createHash('sha256').update(await readFile(filePath)).digest('hex');
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
245
|
+
const BINARY_CONTENT_TYPE_BY_EXTENSION = {
|
|
246
|
+
'.aac': 'audio/aac',
|
|
247
|
+
'.avif': 'image/avif',
|
|
248
|
+
'.flac': 'audio/flac',
|
|
249
|
+
'.gif': 'image/gif',
|
|
250
|
+
'.jpeg': 'image/jpeg',
|
|
251
|
+
'.jpg': 'image/jpeg',
|
|
252
|
+
'.m4a': 'audio/mp4',
|
|
253
|
+
'.m4v': 'video/mp4',
|
|
254
|
+
'.mov': 'video/quicktime',
|
|
255
|
+
'.mp3': 'audio/mpeg',
|
|
256
|
+
'.mp4': 'video/mp4',
|
|
257
|
+
'.oga': 'audio/ogg',
|
|
258
|
+
'.ogg': 'audio/ogg',
|
|
259
|
+
'.ogv': 'video/ogg',
|
|
260
|
+
'.opus': 'audio/ogg',
|
|
261
|
+
'.png': 'image/png',
|
|
262
|
+
'.svg': 'image/svg+xml',
|
|
263
|
+
'.wav': 'audio/wav',
|
|
264
|
+
'.weba': 'audio/webm',
|
|
265
|
+
'.webm': 'video/webm',
|
|
266
|
+
'.webp': 'image/webp',
|
|
267
|
+
};
|
|
268
|
+
function inferBinaryContentType(filePath) {
|
|
269
|
+
return BINARY_CONTENT_TYPE_BY_EXTENSION[path.extname(filePath).toLowerCase()] || null;
|
|
266
270
|
}
|
|
267
271
|
function decodeDataUrl(content) {
|
|
268
272
|
const match = content.trim().match(/^data:([^;,]+);base64,([A-Za-z0-9+/=\s_-]+)$/);
|
package/package.json
CHANGED
package/template_docs/AGENTS.md
CHANGED
|
@@ -8,7 +8,7 @@ Source-of-truth guide for editing this synced FunnelsGrove funnel. Read the matc
|
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
9
|
| Add or edit a step | [docs/editing-step.md](docs/editing-step.md) + [docs/step-ui-guidelines.md](docs/step-ui-guidelines.md) | `src/steps/*` + registries + manifest | < 3 min to first preview |
|
|
10
10
|
| Change step order / branching | [docs/editing-flow.md](docs/editing-flow.md) | `src/config/funnel.manifest.ts` | < 2 min |
|
|
11
|
-
| Set up an A/B experiment | [docs/ab-experiments.md](docs/ab-experiments.md) | `src/config/experiments.ts
|
|
11
|
+
| Set up an A/B experiment | [docs/ab-experiments.md](docs/ab-experiments.md) | FunnelsGrove UI/API + `src/config/experiments.generated.ts`; manifest steps/edges | < 1 min when variant step exists |
|
|
12
12
|
| Edit copy or images | [docs/editor-and-content.md](docs/editor-and-content.md) | `src/steps/content/*.content.ts` | < 2 min |
|
|
13
13
|
| Edit image loading/performance | [docs/editing-flow.md](docs/editing-flow.md) + [docs/step-ui-guidelines.md](docs/step-ui-guidelines.md) + [docs/publishing-and-versioning.md](docs/publishing-and-versioning.md) | `src/config/funnel.manifest.ts`, `src/components/FunnelFlow.tsx`, image assets | careful, QA required |
|
|
14
14
|
| Change plans, prices, discounts | [docs/payment-plans-and-discounts.md](docs/payment-plans-and-discounts.md) | `src/config/billing.plans.ts` | careful, read doc fully |
|
|
@@ -63,7 +63,7 @@ with the same local source diff.
|
|
|
63
63
|
4. **Goal-driven execution.** Define the success check before editing ("step renders at all default breakpoints and Continue advances to step-X"), then loop until it passes.
|
|
64
64
|
5. **Image performance locked.** Keep build-time raster compression plus AVIF/WebP variants enabled. For funnel step images, use `funnelManifest.assets` + step `assetIds`, priority/preload only for first-viewport images, and low-priority next-step warming from the shell. Do not preload the whole funnel image set.
|
|
65
65
|
6. **Meaningful URLs.** New step `path` values are public product routes, so use readable slugs like `/motivation`, `/fitness-goal`, or `/email-capture`. Sequential ids and `step-NN-*` filenames are okay for ordering, but do not create public routes like `/step-1`.
|
|
66
|
-
7. **Flow labeling hygiene.** When changing `edgesByStepId`, keep manifest `branches` current for conditional paths that own steps before reconverging. Keep builder metadata ClaimBee-style and source-readable: `edgesByStepId` keys/targets and `branches: [...]` must use inline string literals, not `someStep.id` variables or
|
|
66
|
+
7. **Flow labeling hygiene.** When changing `edgesByStepId`, keep manifest `branches` current for conditional paths that own steps before reconverging. Keep builder metadata ClaimBee-style and source-readable: `edgesByStepId` keys/targets and `branches: [...]` must use inline string literals, not `someStep.id` variables or indirection like `branches: flowBranches`. Each branch needs a readable `name`, answer-derived `label` such as `yes-branch`, useful `tags`, and the owned `stepIds`. Every experiment control or variant route target must be a real manifest step with normal outgoing edges. Prefer creating/updating experiments through the FunnelsGrove UI/API so the platform owns the database row, PostHog flag, and generated `src/config/experiments.generated.ts`; keep `src/config/experiments.ts` as the generated compatibility wrapper when possible. If a code-authored experiment is necessary, keep `sourceStepId`/`stepId`, variant labels, traffic percentages, and route target ids as explicit literals so deploy sync and analytics can import it. Running experiment variants need labels/tags like `paywall-test-control` and `paywall-test-variant-b`; stopped A/B variants or other inactive screens should keep stable tags and remain unreachable from the default flow so builder marks them `unused`.
|
|
67
67
|
|
|
68
68
|
Architecture docs are part of the change: if you change routing, runtime state, SDK contracts, URL handoff parameters, checkout behavior, analytics events, or shared package boundaries, update the matching doc in the same change.
|
|
69
69
|
|
|
@@ -1,10 +1,53 @@
|
|
|
1
1
|
# A/B Experiments
|
|
2
2
|
|
|
3
|
-
Experiments are config, not code
|
|
3
|
+
Experiments are config, not component code. Prefer creating or updating the
|
|
4
|
+
experiment through the FunnelsGrove UI/API, which owns the database row,
|
|
5
|
+
PostHog flag, and generated `src/config/experiments.generated.ts` file. Keep
|
|
6
|
+
`src/config/experiments.ts` as the generated compatibility wrapper when
|
|
7
|
+
possible. Make sure every control or variant route target is a real manifest
|
|
8
|
+
step with normal outgoing edges. Never write variant conditionals inside step
|
|
9
|
+
components. If the variant step already exists, setting up the experiment is
|
|
10
|
+
mostly config plus publish sync.
|
|
4
11
|
|
|
5
12
|
## Recipe: Step or Paywall Test
|
|
6
13
|
|
|
7
|
-
Open `src/config/
|
|
14
|
+
Open `src/config/funnel.manifest.ts` and make sure every control and variant
|
|
15
|
+
screen exists in `steps` with readable tags:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
steps: [
|
|
19
|
+
{ id: 'paywall', path: '/offer', tags: ['paywall-ab-control'], /* ... */ },
|
|
20
|
+
{ id: 'paywall-b', path: '/offer-b', tags: ['paywall-ab-variant-b'], /* ... */ },
|
|
21
|
+
]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add normal outgoing edges for both routes:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
edgesByStepId: {
|
|
28
|
+
paywall: [{ toStepId: 'checkout' }],
|
|
29
|
+
'paywall-b': [{ toStepId: 'checkout' }],
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then create the experiment in the FunnelsGrove UI/API and pull the generated
|
|
34
|
+
config. The synced source should export it from `src/config/experiments.generated.ts`
|
|
35
|
+
and keep `src/config/experiments.ts` as a wrapper:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import {
|
|
39
|
+
defineFunnelExperiments,
|
|
40
|
+
toManifestExperiments,
|
|
41
|
+
} from '@funnelsgrove/runtime';
|
|
42
|
+
import { generatedExperiments } from './experiments.generated';
|
|
43
|
+
|
|
44
|
+
export const experiments = defineFunnelExperiments(generatedExperiments);
|
|
45
|
+
|
|
46
|
+
export const manifestExperiments = toManifestExperiments(experiments);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If a code-authored experiment is necessary, add one explicit object inside the
|
|
50
|
+
local experiment array or `defineFunnelExperiments([...])` call:
|
|
8
51
|
|
|
9
52
|
```ts
|
|
10
53
|
export const experiments = defineFunnelExperiments([
|
|
@@ -12,18 +55,30 @@ export const experiments = defineFunnelExperiments([
|
|
|
12
55
|
id: 'paywall-ab', // durable, readable, never reused
|
|
13
56
|
name: 'Paywall copy test',
|
|
14
57
|
type: 'paywall', // 'step' for quiz steps, 'paywall' for paywall tests
|
|
15
|
-
status: 'running', //
|
|
58
|
+
status: 'running', // non-running entries stay out of active routing
|
|
16
59
|
launchDate: '2026-06-12T00:00:00.000Z',
|
|
17
|
-
|
|
18
|
-
|
|
60
|
+
stepId: 'paywall',
|
|
61
|
+
variants: [
|
|
62
|
+
{ variantKey: 'control', stepId: 'paywall', label: 'paywall-ab-control', trafficPercent: 50 },
|
|
63
|
+
{ variantKey: 'variant_b', stepId: 'paywall-b', label: 'paywall-ab-variant-b', trafficPercent: 50 },
|
|
64
|
+
],
|
|
19
65
|
},
|
|
20
66
|
] as const);
|
|
21
67
|
```
|
|
22
68
|
|
|
23
|
-
|
|
69
|
+
Preconditions:
|
|
24
70
|
|
|
25
|
-
1.
|
|
71
|
+
1. Every control or variant `stepId` exists in the manifest `steps` array.
|
|
26
72
|
2. Both steps route to the same next step, unless the experiment is explicitly about the flow.
|
|
73
|
+
3. `steps`, `edgesByStepId`, experiment `stepId`, and experiment `label` values use explicit string literals that analytics can read.
|
|
74
|
+
|
|
75
|
+
The manifest may keep `experiments: manifestExperiments` and import the
|
|
76
|
+
runnable experiment shape from `src/config/experiments.ts`. That is fine as
|
|
77
|
+
long as the separate file exports a generated wrapper or a source-readable
|
|
78
|
+
`experiments` array. Do not hide step ids or labels behind computed variables.
|
|
79
|
+
For route experiments where the source step differs from the rendered control
|
|
80
|
+
step, set `sourceStepId` or `stepId` to the source step and set each variant's
|
|
81
|
+
`stepId` to its rendered route.
|
|
27
82
|
|
|
28
83
|
## How the Runtime Behaves
|
|
29
84
|
|
|
@@ -48,7 +103,8 @@ Check: opening the source URL promotes to the assigned route, each variant rende
|
|
|
48
103
|
|
|
49
104
|
- One deliberate change per experiment. Don't bundle unrelated edits into a variant.
|
|
50
105
|
- `control` stays stable; never edit the control step as part of launching a variant.
|
|
51
|
-
- Add stable
|
|
106
|
+
- Add stable variant `label` values in the `<experiment-id>-control` / `<experiment-id>-variant-b` format so builder labels and analytics views stay readable.
|
|
107
|
+
- Add matching `steps[].tags` to every control and variant step.
|
|
52
108
|
- Variant keys and experiment ids are durable — they flow into analytics. Never recycle an id for a different hypothesis.
|
|
53
109
|
- Keep every variant step's outgoing edge in `edgesByStepId`, or assigned visitors strand.
|
|
54
110
|
- If a paywall variant changes plans or pricing, follow [payment-plans-and-discounts.md](payment-plans-and-discounts.md) for the plan/discount sync rules and QA both variants' checkout ([qa-checklist.md](qa-checklist.md)).
|
|
@@ -15,10 +15,10 @@ The manifest defines:
|
|
|
15
15
|
variants. Funnel shells may use this data to warm likely next-step images,
|
|
16
16
|
but first-viewport images should still use the framework's normal
|
|
17
17
|
priority/preload mechanism.
|
|
18
|
-
- `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, optional `tags`, and optional `assetIds`. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
|
|
18
|
+
- `steps`: every routable step with `id`, `path`, `filePath`, `componentKey`, `type`, optional `kind`, optional `tags`, and optional `assetIds`. This includes experiment variant screens and intentionally inactive screens that should appear as `unused` in builder. New `path` values must be meaningful public route slugs, not `/step-1` style URLs. Sequential ids are acceptable when the funnel uses them internally.
|
|
19
19
|
- `edgesByStepId`: graph edges between steps.
|
|
20
20
|
- `branches`: builder metadata for conditional paths that own one or more steps before reconverging.
|
|
21
|
-
- `experiments`: optional variant routing.
|
|
21
|
+
- `experiments`: optional variant routing. This may be imported from `src/config/experiments.ts` when that file keeps source-readable experiment definitions.
|
|
22
22
|
|
|
23
23
|
Keep `steps[].id`, `path`, and answer keys stable unless the request is a migration.
|
|
24
24
|
When adding a step, prefer a semantic path like `/motivation`, `/fitness-goal`,
|
|
@@ -101,6 +101,13 @@ Experiments attach to a step and route to variant steps:
|
|
|
101
101
|
}
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
Keep every control and variant route target as a real step in
|
|
105
|
+
`funnel.manifest.ts`. The runnable experiment entry may live in
|
|
106
|
+
`src/config/experiments.ts` and be imported into the manifest as
|
|
107
|
+
`experiments: manifestExperiments`; if so, keep the experiment `stepId`, labels,
|
|
108
|
+
and traffic as explicit string/number literals so analytics can parse them.
|
|
109
|
+
Do not hide step ids or labels behind computed variables.
|
|
110
|
+
|
|
104
111
|
The runtime resolves assignments through the shared analytics/runtime integration outside preview and uses editor overrides inside preview/editor mode. Opening the source step waits for the assignment, then opens the assigned route and keeps the URL in sync. Continue then advances from the assigned route's normal graph edge, with the already-applied experiment ignored for that continuation.
|
|
105
112
|
|
|
106
113
|
Keep experiment redirects out of step components. Add normal `edgesByStepId` entries for the source/control step and every variant step, then let `goNext()` or the shell Continue button use the shared runtime.
|
|
@@ -112,6 +119,10 @@ Keep the control variant stable and do not remove a running variant until analyt
|
|
|
112
119
|
- Add or update the manifest step.
|
|
113
120
|
- Register the component in `src/runtime/step-registry.ts`.
|
|
114
121
|
- Update `edgesByStepId`, `entryPoints`, `branches`, step `tags`, and `assetIds` if needed.
|
|
122
|
+
- Add every experiment control and variant route target to `steps`.
|
|
123
|
+
- Add normal outgoing `edgesByStepId` entries for every experiment route target.
|
|
124
|
+
- Add each running experiment to `src/config/experiments.ts` or the manifest's inline `experiments` array, matching the local funnel pattern.
|
|
125
|
+
- Keep experiment config synchronized with manifest steps, tags, and edges.
|
|
115
126
|
- Label every branch-owned path with `branches[].name`, answer-derived `label`, useful `tags`, and owned `stepIds`.
|
|
116
127
|
- Keep builder metadata inline/literal so groups, labels, and `unused` badges are visible in builder.
|
|
117
128
|
- Label every running experiment variant with stable tags/labels like `<experiment-id>-control` and `<experiment-id>-variant-b`.
|