@funnelsgrove/cli 0.1.20 → 0.1.26
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/README.md +16 -0
- package/dist/analyticsOutput.d.ts +2 -1
- package/dist/analyticsOutput.js +35 -10
- package/dist/apiClient.d.ts +9 -1
- package/dist/apiClient.js +16 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +40 -15
- package/dist/cliIdentity.d.ts +13 -0
- package/dist/cliIdentity.js +43 -0
- package/dist/contractCompatibility.d.ts +30 -0
- package/dist/contractCompatibility.js +250 -0
- package/dist/experimentCreate.d.ts +100 -0
- package/dist/experimentCreate.js +887 -0
- package/dist/localSync.d.ts +8 -0
- package/dist/localSync.js +71 -11
- package/funnel-contract-compatibility.json +1036 -0
- package/package.json +2 -1
- package/template_docs/.funnelsgrove-docs.json +4 -4
- package/template_docs/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_docs/docs/funnelsgrove/recipes/add-experiment.md +156 -7
- package/template_docs/funnel-docs.config.json +1 -1
- package/template_scaffold/.funnelsgrove-docs.json +4 -4
- package/template_scaffold/.funnelsgrove-scaffold.json +9 -9
- package/template_scaffold/docs/funnelsgrove/migrations/step-contract-v3.md +1 -1
- package/template_scaffold/docs/funnelsgrove/recipes/add-experiment.md +156 -7
- package/template_scaffold/funnel-agent-docs.test.ts +32 -3
- package/template_scaffold/funnel-docs.config.json +1 -1
- package/template_scaffold/package-lock.json +4 -4
- package/template_scaffold/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "FunnelsGrove command-line tools for editing, syncing, and publishing funnels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"template_scaffold",
|
|
16
16
|
"!template_scaffold/node_modules",
|
|
17
17
|
"template_validation",
|
|
18
|
+
"funnel-contract-compatibility.json",
|
|
18
19
|
"docs-release-history.json",
|
|
19
20
|
"legacy-docs-catalog.json"
|
|
20
21
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion": "2.0.
|
|
3
|
+
"bundleVersion": "2.0.14",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
5
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
6
6
|
"managedFiles": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "b9d539ce9e5330d267afbf694529cf5b7e7be7fc2d28a9d9deceba88f85d5044"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"path": "docs/funnelsgrove/recipes/add-experiment.md",
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "9918b577f47a9577c3c98d9a6a7a8762ddfeedb667efe13000900d1a1daee1f9"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "docs/funnelsgrove/recipes/add-step.md",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"path": "funnel-docs.config.json",
|
|
141
|
-
"sha256": "
|
|
141
|
+
"sha256": "1ac242e7ed583c7688fc8939536f9cd212c88db0fcafe34a4abee1280de52c1c"
|
|
142
142
|
}
|
|
143
143
|
]
|
|
144
144
|
}
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/runtime` `0.1.60` first, then `@funnelsgrove/analytics` `0.1.37`, then `@funnelsgrove/
|
|
20
|
+
Release `@funnelsgrove/runtime` `0.1.60` first, then `@funnelsgrove/analytics` `0.1.37`, then `@funnelsgrove/payments` `0.1.55`. Deploy the API and funnel template, then confirm production `/health` reports the new docs identity. Only then publish `@funnelsgrove/cli` `0.1.26`. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|
|
@@ -19,13 +19,162 @@ Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf
|
|
|
19
19
|
|
|
20
20
|
## Procedure
|
|
21
21
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
1. Start in a synced funnel checkout and run `fgrove status` and `git status --short`.
|
|
23
|
+
2. Build the control and variant steps or offer sets in the same local change.
|
|
24
|
+
3. Save one of the strict JSON specs below as `experiment.json`.
|
|
25
|
+
4. Create the database draft, hosted snapshot, and matching local generated files:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
fgrove experiments create --spec experiment.json --dir .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For machine-readable output, run `fgrove experiments create --spec experiment.json --dir . --json`. It emits only `experimentId`, `experimentKey`, `draftVersionId`, and `writtenPaths`, with generated paths in canonical order.
|
|
32
|
+
5. Resolve assignment and any redirect before creating the destination step visit. Keep the default flow valid when assignment is stopped, missing, or invalid.
|
|
33
|
+
6. Run experiment-focused tests and `fgrove validate`, then follow the delivery path below.
|
|
34
|
+
|
|
35
|
+
## JSON contract
|
|
36
|
+
|
|
37
|
+
Use only the documented fields. Unknown top-level and variant fields are rejected.
|
|
38
|
+
|
|
39
|
+
All experiment types require non-empty `id`, `name`, and `stepId`; one `primaryMetric`; at least one unique `trackedMetrics` entry containing the primary metric; and two to five variants. Each variant requires a unique non-empty `variantKey`, `label`, and `routeToStepId`, an integer `trafficPercent` from 0 through 100, and `isControl`. Exactly one variant is the control and traffic must total 100.
|
|
40
|
+
|
|
41
|
+
Length limits are enforced after surrounding whitespace is trimmed: `variantKey` is at most 120 characters. `id`, `name`, `stepId`, `label`, `routeToStepId`, and `offerSetKey` are each at most 200 characters after trimming. `offerSetKey` applies only to pricing variants.
|
|
42
|
+
|
|
43
|
+
Metrics are exactly `step_completion`, `next_step_reached`, `checkout_opened`, `funnel_completed`, or `paying_customer`.
|
|
44
|
+
|
|
45
|
+
### Step experiment
|
|
46
|
+
|
|
47
|
+
Every variant routes to the step that implements that experience.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"id": "claim-headline-v1",
|
|
52
|
+
"name": "Claim headline experiment",
|
|
53
|
+
"type": "step",
|
|
54
|
+
"stepId": "claim",
|
|
55
|
+
"primaryMetric": "next_step_reached",
|
|
56
|
+
"trackedMetrics": [
|
|
57
|
+
"next_step_reached",
|
|
58
|
+
"funnel_completed"
|
|
59
|
+
],
|
|
60
|
+
"variants": [
|
|
61
|
+
{
|
|
62
|
+
"variantKey": "control",
|
|
63
|
+
"label": "Control",
|
|
64
|
+
"routeToStepId": "claim",
|
|
65
|
+
"trafficPercent": 50,
|
|
66
|
+
"isControl": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"variantKey": "variant_b",
|
|
70
|
+
"label": "Variant B",
|
|
71
|
+
"routeToStepId": "claim-b",
|
|
72
|
+
"trafficPercent": 50,
|
|
73
|
+
"isControl": false
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Paywall experiment
|
|
80
|
+
|
|
81
|
+
Like `step`, every variant has a route target. Use `type: "paywall"` when the routes select complete paywall step variants.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"id": "paywall-layout-v1",
|
|
86
|
+
"name": "Paywall layout experiment",
|
|
87
|
+
"type": "paywall",
|
|
88
|
+
"stepId": "paywall-entry",
|
|
89
|
+
"primaryMetric": "checkout_opened",
|
|
90
|
+
"trackedMetrics": [
|
|
91
|
+
"checkout_opened",
|
|
92
|
+
"paying_customer"
|
|
93
|
+
],
|
|
94
|
+
"variants": [
|
|
95
|
+
{
|
|
96
|
+
"variantKey": "control",
|
|
97
|
+
"label": "Control",
|
|
98
|
+
"routeToStepId": "paywall-control",
|
|
99
|
+
"trafficPercent": 50,
|
|
100
|
+
"isControl": true
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"variantKey": "compact",
|
|
104
|
+
"label": "Compact",
|
|
105
|
+
"routeToStepId": "paywall-compact",
|
|
106
|
+
"trafficPercent": 50,
|
|
107
|
+
"isControl": false
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Pricing experiment
|
|
114
|
+
|
|
115
|
+
Each pricing variant also requires a non-empty `offerSetKey`, and every `routeToStepId` must exactly equal the top-level `stepId`.
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"id": "paywall-price-v1",
|
|
120
|
+
"name": "Paywall pricing experiment",
|
|
121
|
+
"type": "pricing",
|
|
122
|
+
"stepId": "paywall",
|
|
123
|
+
"primaryMetric": "paying_customer",
|
|
124
|
+
"trackedMetrics": [
|
|
125
|
+
"checkout_opened",
|
|
126
|
+
"paying_customer"
|
|
127
|
+
],
|
|
128
|
+
"variants": [
|
|
129
|
+
{
|
|
130
|
+
"variantKey": "control",
|
|
131
|
+
"label": "Control",
|
|
132
|
+
"routeToStepId": "paywall",
|
|
133
|
+
"trafficPercent": 50,
|
|
134
|
+
"isControl": true,
|
|
135
|
+
"offerSetKey": "default-paywall"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"variantKey": "annual_focus",
|
|
139
|
+
"label": "Annual focus",
|
|
140
|
+
"routeToStepId": "paywall",
|
|
141
|
+
"trafficPercent": 50,
|
|
142
|
+
"isControl": false,
|
|
143
|
+
"offerSetKey": "annual-focus"
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Creation guarantees
|
|
150
|
+
|
|
151
|
+
The stable `id` becomes the experiment key. Keep it unchanged for an exact retry: an identical same-key draft is reused, while a different definition or non-draft experiment is rejected rather than overwritten.
|
|
152
|
+
|
|
153
|
+
Creation always produces a draft. It does not create or activate a PostHog flag, start traffic, or make unfinished variant steps or offer sets runnable. Activation remains a separate pre-activation UI/API action.
|
|
154
|
+
|
|
155
|
+
The API creates the experiment and variants atomically, commits the matching generated snapshot to the hosted draft, and only then returns. The draft is immediately visible in the FunnelsGrove UI and hosted previews use the same snapshot. The CLI installs the exact returned bytes at `src/config/experiments.generated.ts` and `src/config/experiments.ts`, then updates the sync manifest last. `.funnelsgrove-sync.json` stays local and ignored; never commit or push it.
|
|
156
|
+
|
|
157
|
+
## Recovery and errors
|
|
158
|
+
|
|
159
|
+
- Missing `.funnelsgrove-sync.json`: run `fgrove sync down` into a clean directory first.
|
|
160
|
+
- Invalid local JSON or `[FG-EXPERIMENT-SPEC]`: fix the reported field and retry with the same stable `id`.
|
|
161
|
+
- `[FG-EXPERIMENT-STALE-DRAFT]`: do not overwrite remote work. Download the latest draft into a clean temporary directory, merge deliberately, rerun validation, and retry the same exact spec.
|
|
162
|
+
- `[FG-EXPERIMENT-DRAFT-CONFLICT]`: the stable key already belongs to a different definition, owner, or lifecycle state. Reuse the original exact definition for a retry, or choose a new stable `id` only for a genuinely new experiment.
|
|
163
|
+
- Locally changed generated experiment files are never overwritten. Resolve those edits before retrying.
|
|
164
|
+
- If the API succeeded but the process stopped, retry safely with the same `id`. A ready local journal is automatically recovered before the next create or sync command; an interruption before the journal simply repeats the exact API request.
|
|
165
|
+
|
|
166
|
+
## Validate and deliver
|
|
167
|
+
|
|
168
|
+
After implementing all referenced variant steps or offer sets, run:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
fgrove validate
|
|
172
|
+
git status --short
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For a funnel without GitHub source sync, deliver all local source changes with `fgrove sync up`.
|
|
176
|
+
|
|
177
|
+
For a GitHub-connected funnel, commit and push the generated source files with the rest of the source change, then run `fgrove github pull`. Never use `fgrove sync up` for the same GitHub-connected diff, and never add `.funnelsgrove-sync.json` to git.
|
|
29
178
|
|
|
30
179
|
## Pre-activation QA
|
|
31
180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"bundleVersion": "2.0.
|
|
3
|
+
"bundleVersion": "2.0.14",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
5
|
"contractHash": "d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf",
|
|
6
6
|
"managedFiles": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "b9d539ce9e5330d267afbf694529cf5b7e7be7fc2d28a9d9deceba88f85d5044"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"path": "docs/funnelsgrove/qa/analytics.md",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
},
|
|
59
59
|
{
|
|
60
60
|
"path": "docs/funnelsgrove/recipes/add-experiment.md",
|
|
61
|
-
"sha256": "
|
|
61
|
+
"sha256": "9918b577f47a9577c3c98d9a6a7a8762ddfeedb667efe13000900d1a1daee1f9"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"path": "docs/funnelsgrove/recipes/add-step.md",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"path": "funnel-docs.config.json",
|
|
141
|
-
"sha256": "
|
|
141
|
+
"sha256": "1ac242e7ed583c7688fc8939536f9cd212c88db0fcafe34a4abee1280de52c1c"
|
|
142
142
|
}
|
|
143
143
|
]
|
|
144
144
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sourceTreeHash": "
|
|
3
|
+
"sourceTreeHash": "a271052a1c008ebf769480051d47c0f941cb4987a2a9aa5f3475c01892e23b78",
|
|
4
4
|
"stepContractVersion": 3,
|
|
5
|
-
"docsBundleVersion": "2.0.
|
|
5
|
+
"docsBundleVersion": "2.0.14",
|
|
6
6
|
"files": [
|
|
7
7
|
{
|
|
8
8
|
"path": ".env.example",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": ".funnelsgrove-docs.json",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "fafd9bbf52e7eb1a5dd3c4f429b6b004d8b8afa3ce652f40b55ea8743e26ce84",
|
|
20
20
|
"mode": "100644"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"path": "docs/funnelsgrove/migrations/step-contract-v3.md",
|
|
104
|
-
"sha256": "
|
|
104
|
+
"sha256": "b9d539ce9e5330d267afbf694529cf5b7e7be7fc2d28a9d9deceba88f85d5044",
|
|
105
105
|
"mode": "100644"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
"path": "docs/funnelsgrove/recipes/add-experiment.md",
|
|
129
|
-
"sha256": "
|
|
129
|
+
"sha256": "9918b577f47a9577c3c98d9a6a7a8762ddfeedb667efe13000900d1a1daee1f9",
|
|
130
130
|
"mode": "100644"
|
|
131
131
|
},
|
|
132
132
|
{
|
|
@@ -236,12 +236,12 @@
|
|
|
236
236
|
},
|
|
237
237
|
{
|
|
238
238
|
"path": "funnel-agent-docs.test.ts",
|
|
239
|
-
"sha256": "
|
|
239
|
+
"sha256": "0a44b1c46be244ba95237c34ed157306932f82ea2e30654cb0e35b9dfa2f0a84",
|
|
240
240
|
"mode": "100644"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
"path": "funnel-docs.config.json",
|
|
244
|
-
"sha256": "
|
|
244
|
+
"sha256": "1ac242e7ed583c7688fc8939536f9cd212c88db0fcafe34a4abee1280de52c1c",
|
|
245
245
|
"mode": "100644"
|
|
246
246
|
},
|
|
247
247
|
{
|
|
@@ -266,12 +266,12 @@
|
|
|
266
266
|
},
|
|
267
267
|
{
|
|
268
268
|
"path": "package-lock.json",
|
|
269
|
-
"sha256": "
|
|
269
|
+
"sha256": "e3d6430705410764cb7a012befd1582a78f8e8269848fad9be44c3c66c4b58e7",
|
|
270
270
|
"mode": "100644"
|
|
271
271
|
},
|
|
272
272
|
{
|
|
273
273
|
"path": "package.json",
|
|
274
|
-
"sha256": "
|
|
274
|
+
"sha256": "f7fe244b5bc7b6449d27d048761c973f2825b0b4dba3dd931b4aa31bd523df77",
|
|
275
275
|
"mode": "100644"
|
|
276
276
|
},
|
|
277
277
|
{
|
|
@@ -17,7 +17,7 @@ Supported read versions: `1`, `2`, `3`. Authoring and publish target version `3`
|
|
|
17
17
|
|
|
18
18
|
### Package release order
|
|
19
19
|
|
|
20
|
-
Release `@funnelsgrove/runtime` `0.1.60` first, then `@funnelsgrove/analytics` `0.1.37`, then `@funnelsgrove/
|
|
20
|
+
Release `@funnelsgrove/runtime` `0.1.60` first, then `@funnelsgrove/analytics` `0.1.37`, then `@funnelsgrove/payments` `0.1.55`. Deploy the API and funnel template, then confirm production `/health` reports the new docs identity. Only then publish `@funnelsgrove/cli` `0.1.26`. Publishing packages and deploying production remain separately approved operational actions.
|
|
21
21
|
<!-- funnelsgrove:generated:end contract-v3/migration/step-contract-v3 -->
|
|
22
22
|
|
|
23
23
|
## Version-last policy
|
|
@@ -19,13 +19,162 @@ Contract hash: `d761e91d5ac6ff9e72c6d49c5bcd014270912473a66f99c49d726c65998085cf
|
|
|
19
19
|
|
|
20
20
|
## Procedure
|
|
21
21
|
|
|
22
|
-
1.
|
|
23
|
-
2.
|
|
24
|
-
3.
|
|
25
|
-
4.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
1. Start in a synced funnel checkout and run `fgrove status` and `git status --short`.
|
|
23
|
+
2. Build the control and variant steps or offer sets in the same local change.
|
|
24
|
+
3. Save one of the strict JSON specs below as `experiment.json`.
|
|
25
|
+
4. Create the database draft, hosted snapshot, and matching local generated files:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
fgrove experiments create --spec experiment.json --dir .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For machine-readable output, run `fgrove experiments create --spec experiment.json --dir . --json`. It emits only `experimentId`, `experimentKey`, `draftVersionId`, and `writtenPaths`, with generated paths in canonical order.
|
|
32
|
+
5. Resolve assignment and any redirect before creating the destination step visit. Keep the default flow valid when assignment is stopped, missing, or invalid.
|
|
33
|
+
6. Run experiment-focused tests and `fgrove validate`, then follow the delivery path below.
|
|
34
|
+
|
|
35
|
+
## JSON contract
|
|
36
|
+
|
|
37
|
+
Use only the documented fields. Unknown top-level and variant fields are rejected.
|
|
38
|
+
|
|
39
|
+
All experiment types require non-empty `id`, `name`, and `stepId`; one `primaryMetric`; at least one unique `trackedMetrics` entry containing the primary metric; and two to five variants. Each variant requires a unique non-empty `variantKey`, `label`, and `routeToStepId`, an integer `trafficPercent` from 0 through 100, and `isControl`. Exactly one variant is the control and traffic must total 100.
|
|
40
|
+
|
|
41
|
+
Length limits are enforced after surrounding whitespace is trimmed: `variantKey` is at most 120 characters. `id`, `name`, `stepId`, `label`, `routeToStepId`, and `offerSetKey` are each at most 200 characters after trimming. `offerSetKey` applies only to pricing variants.
|
|
42
|
+
|
|
43
|
+
Metrics are exactly `step_completion`, `next_step_reached`, `checkout_opened`, `funnel_completed`, or `paying_customer`.
|
|
44
|
+
|
|
45
|
+
### Step experiment
|
|
46
|
+
|
|
47
|
+
Every variant routes to the step that implements that experience.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"id": "claim-headline-v1",
|
|
52
|
+
"name": "Claim headline experiment",
|
|
53
|
+
"type": "step",
|
|
54
|
+
"stepId": "claim",
|
|
55
|
+
"primaryMetric": "next_step_reached",
|
|
56
|
+
"trackedMetrics": [
|
|
57
|
+
"next_step_reached",
|
|
58
|
+
"funnel_completed"
|
|
59
|
+
],
|
|
60
|
+
"variants": [
|
|
61
|
+
{
|
|
62
|
+
"variantKey": "control",
|
|
63
|
+
"label": "Control",
|
|
64
|
+
"routeToStepId": "claim",
|
|
65
|
+
"trafficPercent": 50,
|
|
66
|
+
"isControl": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"variantKey": "variant_b",
|
|
70
|
+
"label": "Variant B",
|
|
71
|
+
"routeToStepId": "claim-b",
|
|
72
|
+
"trafficPercent": 50,
|
|
73
|
+
"isControl": false
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Paywall experiment
|
|
80
|
+
|
|
81
|
+
Like `step`, every variant has a route target. Use `type: "paywall"` when the routes select complete paywall step variants.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"id": "paywall-layout-v1",
|
|
86
|
+
"name": "Paywall layout experiment",
|
|
87
|
+
"type": "paywall",
|
|
88
|
+
"stepId": "paywall-entry",
|
|
89
|
+
"primaryMetric": "checkout_opened",
|
|
90
|
+
"trackedMetrics": [
|
|
91
|
+
"checkout_opened",
|
|
92
|
+
"paying_customer"
|
|
93
|
+
],
|
|
94
|
+
"variants": [
|
|
95
|
+
{
|
|
96
|
+
"variantKey": "control",
|
|
97
|
+
"label": "Control",
|
|
98
|
+
"routeToStepId": "paywall-control",
|
|
99
|
+
"trafficPercent": 50,
|
|
100
|
+
"isControl": true
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"variantKey": "compact",
|
|
104
|
+
"label": "Compact",
|
|
105
|
+
"routeToStepId": "paywall-compact",
|
|
106
|
+
"trafficPercent": 50,
|
|
107
|
+
"isControl": false
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Pricing experiment
|
|
114
|
+
|
|
115
|
+
Each pricing variant also requires a non-empty `offerSetKey`, and every `routeToStepId` must exactly equal the top-level `stepId`.
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"id": "paywall-price-v1",
|
|
120
|
+
"name": "Paywall pricing experiment",
|
|
121
|
+
"type": "pricing",
|
|
122
|
+
"stepId": "paywall",
|
|
123
|
+
"primaryMetric": "paying_customer",
|
|
124
|
+
"trackedMetrics": [
|
|
125
|
+
"checkout_opened",
|
|
126
|
+
"paying_customer"
|
|
127
|
+
],
|
|
128
|
+
"variants": [
|
|
129
|
+
{
|
|
130
|
+
"variantKey": "control",
|
|
131
|
+
"label": "Control",
|
|
132
|
+
"routeToStepId": "paywall",
|
|
133
|
+
"trafficPercent": 50,
|
|
134
|
+
"isControl": true,
|
|
135
|
+
"offerSetKey": "default-paywall"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"variantKey": "annual_focus",
|
|
139
|
+
"label": "Annual focus",
|
|
140
|
+
"routeToStepId": "paywall",
|
|
141
|
+
"trafficPercent": 50,
|
|
142
|
+
"isControl": false,
|
|
143
|
+
"offerSetKey": "annual-focus"
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Creation guarantees
|
|
150
|
+
|
|
151
|
+
The stable `id` becomes the experiment key. Keep it unchanged for an exact retry: an identical same-key draft is reused, while a different definition or non-draft experiment is rejected rather than overwritten.
|
|
152
|
+
|
|
153
|
+
Creation always produces a draft. It does not create or activate a PostHog flag, start traffic, or make unfinished variant steps or offer sets runnable. Activation remains a separate pre-activation UI/API action.
|
|
154
|
+
|
|
155
|
+
The API creates the experiment and variants atomically, commits the matching generated snapshot to the hosted draft, and only then returns. The draft is immediately visible in the FunnelsGrove UI and hosted previews use the same snapshot. The CLI installs the exact returned bytes at `src/config/experiments.generated.ts` and `src/config/experiments.ts`, then updates the sync manifest last. `.funnelsgrove-sync.json` stays local and ignored; never commit or push it.
|
|
156
|
+
|
|
157
|
+
## Recovery and errors
|
|
158
|
+
|
|
159
|
+
- Missing `.funnelsgrove-sync.json`: run `fgrove sync down` into a clean directory first.
|
|
160
|
+
- Invalid local JSON or `[FG-EXPERIMENT-SPEC]`: fix the reported field and retry with the same stable `id`.
|
|
161
|
+
- `[FG-EXPERIMENT-STALE-DRAFT]`: do not overwrite remote work. Download the latest draft into a clean temporary directory, merge deliberately, rerun validation, and retry the same exact spec.
|
|
162
|
+
- `[FG-EXPERIMENT-DRAFT-CONFLICT]`: the stable key already belongs to a different definition, owner, or lifecycle state. Reuse the original exact definition for a retry, or choose a new stable `id` only for a genuinely new experiment.
|
|
163
|
+
- Locally changed generated experiment files are never overwritten. Resolve those edits before retrying.
|
|
164
|
+
- If the API succeeded but the process stopped, retry safely with the same `id`. A ready local journal is automatically recovered before the next create or sync command; an interruption before the journal simply repeats the exact API request.
|
|
165
|
+
|
|
166
|
+
## Validate and deliver
|
|
167
|
+
|
|
168
|
+
After implementing all referenced variant steps or offer sets, run:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
fgrove validate
|
|
172
|
+
git status --short
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For a funnel without GitHub source sync, deliver all local source changes with `fgrove sync up`.
|
|
176
|
+
|
|
177
|
+
For a GitHub-connected funnel, commit and push the generated source files with the rest of the source change, then run `fgrove github pull`. Never use `fgrove sync up` for the same GitHub-connected diff, and never add `.funnelsgrove-sync.json` to git.
|
|
29
178
|
|
|
30
179
|
## Pre-activation QA
|
|
31
180
|
|
|
@@ -138,9 +138,10 @@ describe('funnel agent documentation supply', () => {
|
|
|
138
138
|
|
|
139
139
|
it('renders package release order from the current package manifests', async () => {
|
|
140
140
|
const { buildBundle, parseGeneratedBlock } = await loadGenerator();
|
|
141
|
-
const [runtimePackage, analyticsPackage, cliPackage] = await Promise.all([
|
|
141
|
+
const [runtimePackage, analyticsPackage, paymentsPackage, cliPackage] = await Promise.all([
|
|
142
142
|
readFile(join(repoRoot, 'apps/funnel-runtime/package.json'), 'utf8').then(JSON.parse),
|
|
143
143
|
readFile(join(repoRoot, 'apps/funnel-analytics/package.json'), 'utf8').then(JSON.parse),
|
|
144
|
+
readFile(join(repoRoot, 'apps/funnel-payments/package.json'), 'utf8').then(JSON.parse),
|
|
144
145
|
readFile(join(repoRoot, 'apps/cli/package.json'), 'utf8').then(JSON.parse),
|
|
145
146
|
]);
|
|
146
147
|
const migration = (await buildBundle()).files.get(
|
|
@@ -152,7 +153,9 @@ describe('funnel agent documentation supply', () => {
|
|
|
152
153
|
expect(generated).toContain(
|
|
153
154
|
`Release \`@funnelsgrove/runtime\` \`${runtimePackage.version}\` first, `
|
|
154
155
|
+ `then \`@funnelsgrove/analytics\` \`${analyticsPackage.version}\`, `
|
|
155
|
-
+ `then \`@funnelsgrove/
|
|
156
|
+
+ `then \`@funnelsgrove/payments\` \`${paymentsPackage.version}\`. `
|
|
157
|
+
+ 'Deploy the API and funnel template, then confirm production `/health` reports the new docs identity. '
|
|
158
|
+
+ `Only then publish \`@funnelsgrove/cli\` \`${cliPackage.version}\`.`,
|
|
156
159
|
);
|
|
157
160
|
});
|
|
158
161
|
|
|
@@ -226,6 +229,32 @@ describe('funnel agent documentation supply', () => {
|
|
|
226
229
|
);
|
|
227
230
|
});
|
|
228
231
|
|
|
232
|
+
it('documents the complete JSON-driven experiment creation workflow', async () => {
|
|
233
|
+
const content = await readFile(
|
|
234
|
+
join(templateRoot, 'docs/funnelsgrove/recipes/add-experiment.md'),
|
|
235
|
+
'utf8',
|
|
236
|
+
);
|
|
237
|
+
const examples = [...content.matchAll(/```json\n([\s\S]*?)\n```/g)]
|
|
238
|
+
.map((match) => JSON.parse(match[1]) as { type?: unknown });
|
|
239
|
+
|
|
240
|
+
expect(examples.map(({ type }) => type)).toEqual(['step', 'paywall', 'pricing']);
|
|
241
|
+
expect(content).toContain('fgrove experiments create --spec experiment.json --dir .');
|
|
242
|
+
expect(content).toContain('fgrove experiments create --spec experiment.json --dir . --json');
|
|
243
|
+
expect(content).toContain('`experimentId`, `experimentKey`, `draftVersionId`, and `writtenPaths`');
|
|
244
|
+
expect(content).toContain(
|
|
245
|
+
'`variantKey` is at most 120 characters. `id`, `name`, `stepId`, `label`, `routeToStepId`, and `offerSetKey` are each at most 200 characters after trimming.',
|
|
246
|
+
);
|
|
247
|
+
expect(content).toContain('does not create or activate a PostHog flag');
|
|
248
|
+
expect(content).toContain('immediately visible in the FunnelsGrove UI');
|
|
249
|
+
expect(content).toContain('src/config/experiments.generated.ts');
|
|
250
|
+
expect(content).toContain('[FG-EXPERIMENT-STALE-DRAFT]');
|
|
251
|
+
expect(content).toContain('[FG-EXPERIMENT-DRAFT-CONFLICT]');
|
|
252
|
+
expect(content).toContain('[FG-EXPERIMENT-SPEC]');
|
|
253
|
+
expect(content).toContain('fgrove sync up');
|
|
254
|
+
expect(content).toContain('fgrove github pull');
|
|
255
|
+
expect(content).toContain('`.funnelsgrove-sync.json` stays local and ignored');
|
|
256
|
+
});
|
|
257
|
+
|
|
229
258
|
it('keeps intro engagement explicitly non-automatic', async () => {
|
|
230
259
|
const content = await readFile(
|
|
231
260
|
join(templateRoot, 'docs/funnelsgrove/steps/intro_hero.md'),
|
|
@@ -311,7 +340,7 @@ describe('funnel agent documentation supply', () => {
|
|
|
311
340
|
|
|
312
341
|
expect(manifest).toMatchObject({
|
|
313
342
|
schemaVersion: 1,
|
|
314
|
-
bundleVersion: '2.0.
|
|
343
|
+
bundleVersion: '2.0.14',
|
|
315
344
|
stepContractVersion: contract.stepContractVersion,
|
|
316
345
|
contractHash: contract.contractHash,
|
|
317
346
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@funnelsgrove/analytics": "^0.1.37",
|
|
12
|
-
"@funnelsgrove/payments": "^0.1.
|
|
12
|
+
"@funnelsgrove/payments": "^0.1.55",
|
|
13
13
|
"@funnelsgrove/runtime": "^0.1.60",
|
|
14
14
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
15
15
|
"@stripe/stripe-js": "^8.7.0",
|
|
@@ -899,9 +899,9 @@
|
|
|
899
899
|
}
|
|
900
900
|
},
|
|
901
901
|
"node_modules/@funnelsgrove/payments": {
|
|
902
|
-
"version": "0.1.
|
|
903
|
-
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.1.
|
|
904
|
-
"integrity": "sha512-
|
|
902
|
+
"version": "0.1.55",
|
|
903
|
+
"resolved": "https://registry.npmjs.org/@funnelsgrove/payments/-/payments-0.1.55.tgz",
|
|
904
|
+
"integrity": "sha512-zrlnyPwUsqSYkaDYASBkB3bzNVSzS0+4glKC2+ai/+fIi4SZqEOAEo51WxR3jt0s5dAX2nFdX08r/YlvY7XqyA==",
|
|
905
905
|
"dependencies": {
|
|
906
906
|
"@funnelsgrove/analytics": "^0.1.28",
|
|
907
907
|
"@funnelsgrove/runtime": "^0.1.54",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@funnelsgrove/analytics": "^0.1.37",
|
|
16
|
-
"@funnelsgrove/payments": "^0.1.
|
|
16
|
+
"@funnelsgrove/payments": "^0.1.55",
|
|
17
17
|
"@funnelsgrove/runtime": "^0.1.60",
|
|
18
18
|
"@stripe/react-stripe-js": "^5.6.0",
|
|
19
19
|
"@stripe/stripe-js": "^8.7.0",
|