@apex-inc/mcp-server 0.26.0 → 0.27.1
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/activation-wiring.d.ts +12 -0
- package/dist/activation-wiring.d.ts.map +1 -0
- package/dist/activation-wiring.js +18 -0
- package/dist/activation-wiring.js.map +1 -0
- package/dist/control-gate-guidance.d.ts +1 -0
- package/dist/control-gate-guidance.d.ts.map +1 -1
- package/dist/control-gate-guidance.js +13 -5
- package/dist/control-gate-guidance.js.map +1 -1
- package/dist/prompts.js +3 -3
- package/dist/publish-communication-copy.d.ts +11 -0
- package/dist/publish-communication-copy.d.ts.map +1 -0
- package/dist/publish-communication-copy.js +29 -0
- package/dist/publish-communication-copy.js.map +1 -0
- package/dist/tools.d.ts +30 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +135 -45
- package/dist/tools.js.map +1 -1
- package/dist/workspace-target-url.d.ts +23 -0
- package/dist/workspace-target-url.d.ts.map +1 -0
- package/dist/workspace-target-url.js +62 -0
- package/dist/workspace-target-url.js.map +1 -0
- package/package.json +1 -1
- package/skills/apex-communications/SKILL.md +25 -21
- package/skills/apex-experimentation/SKILL.md +20 -6
- package/skills/apex-growth-tracking/SKILL.md +1 -1
- package/skills/apex-integration-cookbook/SKILL.md +1 -1
- package/skills/apex-spec/SKILL.md +6 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind experiment target_url to hosts the ACTIVE workspace actually owns.
|
|
3
|
+
*
|
|
4
|
+
* A merchant with two workspaces (e.g. apex-staging vs the stealth apex.inc
|
|
5
|
+
* property) must not silently land a draft on the sibling's site. Agents
|
|
6
|
+
* guess brand domains; the catalog is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hostnameOf(raw: string): string | null;
|
|
9
|
+
export declare function collectHosts(input: {
|
|
10
|
+
analyticsHosts?: Array<{
|
|
11
|
+
hostname?: string;
|
|
12
|
+
} | string>;
|
|
13
|
+
namedSites?: string[];
|
|
14
|
+
workspaceUrl?: string | null;
|
|
15
|
+
apexUrl?: string | null;
|
|
16
|
+
}): string[];
|
|
17
|
+
export declare function evaluateTargetUrl(targetUrl: string, allowedHosts: string[]): {
|
|
18
|
+
ok: true;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=workspace-target-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-target-url.d.ts","sourceRoot":"","sources":["../src/workspace-target-url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOrD;AAUD,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,cAAc,CAAC,EAAE,KAAK,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,CAAC;IACvD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,GAAG,MAAM,EAAE,CASX;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,GACrB;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAwB/C"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind experiment target_url to hosts the ACTIVE workspace actually owns.
|
|
3
|
+
*
|
|
4
|
+
* A merchant with two workspaces (e.g. apex-staging vs the stealth apex.inc
|
|
5
|
+
* property) must not silently land a draft on the sibling's site. Agents
|
|
6
|
+
* guess brand domains; the catalog is the source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export function hostnameOf(raw) {
|
|
9
|
+
try {
|
|
10
|
+
const withScheme = /:\/\//.test(raw) ? raw : `https://${raw}`;
|
|
11
|
+
return new URL(withScheme).hostname.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function addHost(hosts, raw) {
|
|
18
|
+
if (!raw)
|
|
19
|
+
return;
|
|
20
|
+
const host = hostnameOf(raw);
|
|
21
|
+
if (!host)
|
|
22
|
+
return;
|
|
23
|
+
if (host === "localhost" || host === "127.0.0.1")
|
|
24
|
+
return;
|
|
25
|
+
hosts.add(host);
|
|
26
|
+
}
|
|
27
|
+
export function collectHosts(input) {
|
|
28
|
+
const hosts = new Set();
|
|
29
|
+
for (const item of input.analyticsHosts ?? []) {
|
|
30
|
+
addHost(hosts, typeof item === "string" ? item : item.hostname);
|
|
31
|
+
}
|
|
32
|
+
for (const site of input.namedSites ?? [])
|
|
33
|
+
addHost(hosts, site);
|
|
34
|
+
addHost(hosts, input.workspaceUrl);
|
|
35
|
+
addHost(hosts, input.apexUrl);
|
|
36
|
+
return [...hosts];
|
|
37
|
+
}
|
|
38
|
+
export function evaluateTargetUrl(targetUrl, allowedHosts) {
|
|
39
|
+
const host = hostnameOf(targetUrl);
|
|
40
|
+
if (!host) {
|
|
41
|
+
return { ok: false, message: `target_url is not a valid URL: ${targetUrl}` };
|
|
42
|
+
}
|
|
43
|
+
if (host === "localhost" || host === "127.0.0.1") {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
message: "target_url must be a named site on the active workspace, not localhost. Capture localhost screenshots with attach_experiment_asset; the experiment itself runs on a hosted site.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (allowedHosts.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: "No sites are named on the active workspace yet. Call list_workspace_environments, then set_workspace_environments with the real host (or use the workspace's own site). Do not guess a brand domain or a sibling workspace's site.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (allowedHosts.includes(host))
|
|
56
|
+
return { ok: true };
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `target_url host "${host}" is not a site on the active workspace. Known hosts: ${allowedHosts.join(", ")}. Do not guess a brand domain or a sibling workspace's site.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=workspace-target-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-target-url.js","sourceRoot":"","sources":["../src/workspace-target-url.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC;QAC9D,OAAO,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,OAAO,CAAC,KAAkB,EAAE,GAAmB;IACtD,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW;QAAE,OAAO;IACzD,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAK5B;IACC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC;QAC9C,OAAO,CAAC,KAAK,EAAE,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE;QAAE,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACnC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,SAAiB,EACjB,YAAsB;IAEtB,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,kCAAkC,SAAS,EAAE,EAAE,CAAC;IAC/E,CAAC;IACD,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;QACjD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,kLAAkL;SACrL,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,oOAAoO;SACvO,CAAC;IACJ,CAAC;IACD,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,OAAO,EAAE,oBAAoB,IAAI,yDAAyD,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,8DAA8D;KAChL,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -45,30 +45,32 @@ experience, so Apex won't let you do it by accident.
|
|
|
45
45
|
|
|
46
46
|
| `controlState` | What it means | Editing it |
|
|
47
47
|
|---|---|---|
|
|
48
|
-
| `none` | Never experimented on |
|
|
49
|
-
| `no_winner` | An experiment ran and settled without a winner |
|
|
50
|
-
| `winner` | Won an experiment, unchanged since |
|
|
51
|
-
| `winner_edited` | Won, then was changed |
|
|
48
|
+
| `none` | Never experimented on | Editable. If a journey is sending it, Publish still asks replace vs compete |
|
|
49
|
+
| `no_winner` | An experiment ran and settled without a winner | Editable. Same publish question if it's sending |
|
|
50
|
+
| `winner` | Won an experiment, unchanged since | Champion. Control is read-only. Add a variant to compete; Replace overwrites |
|
|
51
|
+
| `winner_edited` | Won, then was changed | Still a champion. Same as winner |
|
|
52
52
|
| `in_experiment` | Frozen — an experiment is measuring it right now | Blocked; see below |
|
|
53
53
|
|
|
54
54
|
A comm that's live in a published journey also needs `intent`, whatever its
|
|
55
|
-
`controlState`.
|
|
55
|
+
`controlState`. Do not rewrite a champion to start a test. Use
|
|
56
|
+
`add_communication_variant`, then `publish_communication` with `intent: "compete"`.
|
|
57
|
+
Rewriting what's sending is `replace`.
|
|
56
58
|
|
|
57
59
|
### The two intents
|
|
58
60
|
|
|
59
|
-
- **`intent: "compete"`** — *
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **`intent: "replace"`** — *
|
|
63
|
-
|
|
61
|
+
- **`intent: "compete"`** — *Start an experiment.* What's sending now stays
|
|
62
|
+
live. Publish creates a filled-in draft. Then `activate_experiment` after
|
|
63
|
+
the user confirms.
|
|
64
|
+
- **`intent: "replace"`** — *Replace what's sending.* Everyone gets this
|
|
65
|
+
version on the next send. No experiment row.
|
|
64
66
|
|
|
65
67
|
### When in doubt, compete
|
|
66
68
|
|
|
67
69
|
This is the default, and it isn't a coin flip. The two mistakes are not
|
|
68
70
|
symmetric:
|
|
69
71
|
|
|
70
|
-
- Guessing `compete` when the user wanted `replace` costs **time** — they
|
|
71
|
-
|
|
72
|
+
- Guessing `compete` when the user wanted `replace` costs **time** — they have
|
|
73
|
+
a draft to activate, or they can publish again with replace.
|
|
72
74
|
- Guessing `replace` when the user wanted `compete` **destroys a measured
|
|
73
75
|
result permanently**. There's no undo for evidence.
|
|
74
76
|
|
|
@@ -76,9 +78,9 @@ So: if the user explicitly said "just change it for everyone" / "replace the
|
|
|
76
78
|
copy" / "roll this out," send `replace`. Otherwise send `compete`, and say what
|
|
77
79
|
you did:
|
|
78
80
|
|
|
79
|
-
> "That comm's control won an experiment, so I
|
|
80
|
-
> and
|
|
81
|
-
>
|
|
81
|
+
> "That comm's control won an experiment, so I published your copy as a
|
|
82
|
+
> competing version and opened a filled-in draft. Want me to activate it, or
|
|
83
|
+
> make this the letter everyone gets instead?"
|
|
82
84
|
|
|
83
85
|
### Don't force through a running experiment
|
|
84
86
|
|
|
@@ -97,9 +99,10 @@ re-issue. Do not retry the identical call.
|
|
|
97
99
|
### Running experiments
|
|
98
100
|
|
|
99
101
|
1. **`list_communications`** — find the communication to test.
|
|
100
|
-
2. **`
|
|
101
|
-
3.
|
|
102
|
-
4.
|
|
102
|
+
2. **`edit_communication` / `add_communication_variant`** — write the versions.
|
|
103
|
+
3. **`publish_communication`** with `intent: "compete"` — creates a prefilled draft. Does not start the test.
|
|
104
|
+
4. **`activate_experiment`** — begins the test on the next send.
|
|
105
|
+
5. Check results via `get_results` / `list_experiments`.
|
|
103
106
|
|
|
104
107
|
### Manual sends
|
|
105
108
|
|
|
@@ -157,15 +160,16 @@ hard-block**. For those journeys, filter to `pipeline="marketing"`.
|
|
|
157
160
|
```
|
|
158
161
|
1. recommend_communications → look for retention/winback entries
|
|
159
162
|
2. generate_communications for churn-related comms
|
|
160
|
-
3.
|
|
163
|
+
3. add_communication_variant, publish_communication (compete), then activate_experiment
|
|
161
164
|
4. get_event_taxonomy to see what churn signals to track
|
|
162
165
|
```
|
|
163
166
|
|
|
164
167
|
### "How do I A/B test my emails?"
|
|
165
168
|
```
|
|
166
169
|
1. list_communications → find the target
|
|
167
|
-
2.
|
|
168
|
-
3.
|
|
170
|
+
2. add_communication_variant (or edit the second column)
|
|
171
|
+
3. publish_communication intent=compete → filled-in draft
|
|
172
|
+
4. activate_experiment after the user confirms
|
|
169
173
|
```
|
|
170
174
|
|
|
171
175
|
### "Set up cart abandonment recovery"
|
|
@@ -69,10 +69,12 @@ Interactive flows: MCP prompts `new-experiment` and `experiment-review` orchestr
|
|
|
69
69
|
|
|
70
70
|
## Screenshots, exposure & the wiring gate
|
|
71
71
|
|
|
72
|
-
- **Screenshots are first-class.**
|
|
72
|
+
- **Screenshots are first-class.** SDK / Cursor experiments author the variant in local code. Apex does **not** auto-capture a public URL on create — that page does not have the unpublished variant, and a guessed host (a sibling workspace's brand domain) shows the wrong site on both arms. Capture both arms on localhost with `?_apex_preview=control|variant_a&_apex_exp=<id>`, then `attach_experiment_asset({ experimentId, variantKey, imageBase64 })`. After deploy, `recapture_experiment_screenshots` refreshes the live host. Snippet/DOM experiments created in the dashboard still auto-capture a public URL whose variant already lives there.
|
|
73
|
+
- **Target URL is this workspace.** `target_url` must be a host named on the **active** workspace (`list_workspace_environments` + the workspace site). If the merchant already named a workspace, switch to it — do not ask which workspace again. If they have multiple hosts on that workspace, ask which URL. Never guess `apex.inc` or any other workspace's domain.
|
|
73
74
|
- **Mobile/Capacitor experiments capture on-device.** When the experiment runs on an authed in-app screen (servers can't reach it), tell the dev to call `Apex.captureVariantScreenshot({ experimentId, variantKey })` on the variant's screen, keyed to the resolved variant in a `useEffect`, in a debug build (`Apex.initialize({ ..., debug: true })`). It no-ops in production and lands the shot on the dashboard card + gallery like web/agent captures.
|
|
74
75
|
- **Exposure auto-fires.** When a variant resolves via `useApexVariant` (web) or `Apex.getVariant()` (mobile), the SDK fires the canonical `experiment_exposure` event — the denominator for results. You don't fire it manually.
|
|
75
|
-
- **
|
|
76
|
+
- **Preview on Next.js.** `useApexVariant` cannot read `window` during SSR. Pass the request search string (`useApexVariant(id, { search })` from `searchParams`, or `<ApexProvider search>`) so the first HTML matches `_apex_preview`. Without it the server renders control and the arm only appears after hydrate.
|
|
77
|
+
- **Don't launch a dead experiment.** `activate_experiment` is gated on `verify_experiment_wiring`. Code-wired (SDK-hook) web experiments are safe to activate before their code ships: the server holds them in `pending_deployment` ("Waiting on deploy"). The 14-day clock starts at the first **recorded** visit (a tester on a named Beta site, or a live visit) — not merge, not activate. Testers on a named Beta site count in the winner. Localhost/dev is recorded as Dev and does not start the window or decide the winner. Assign with no Origin is Unclassified (no enrollment). Backend assign must send `environment` or `x-apex-web-environment`. Non-SDK experiments with unwired arms are refused (pass `force: true` to override). Run `verify_experiment_wiring({ experimentId })` after the deploy lands to confirm both arms are live.
|
|
76
78
|
|
|
77
79
|
### Journey-arm experiments (any surface)
|
|
78
80
|
|
|
@@ -95,18 +97,28 @@ Do NOT describe this as champion versus challenger, and do not try to run "the o
|
|
|
95
97
|
|
|
96
98
|
### Starting an experiment from the communication side
|
|
97
99
|
|
|
98
|
-
|
|
100
|
+
Write the letter with `edit_communication` / `add_communication_variant`. Then:
|
|
101
|
+
|
|
102
|
+
1. `publish_communication` with `intent: "compete"` (or when variants already exist). This creates a **prefilled draft** on the same experiment page as web tests. It does **not** start the test.
|
|
103
|
+
2. Show the user the draft. They can edit window, hypothesis, belief.
|
|
104
|
+
3. `activate_experiment` starts it. People on that journey get control or the variant on the next send. Screenshots are captured on activate — do not call `attach_experiment_asset` for a letter unless capture failed.
|
|
105
|
+
|
|
106
|
+
`intent: "replace"` publishes the letter for everyone. Leftover variant columns leave. No experiment row.
|
|
107
|
+
|
|
108
|
+
Preconditions for a draft, each reported rather than guessed:
|
|
99
109
|
|
|
100
110
|
- **A host** — some published journey must send the communication. `no_host` means there is no traffic to measure.
|
|
101
111
|
- **A goal** — the host step's own goal event, or the journey's. `no_goal` means there is nothing to optimize toward; set the journey's goal event first.
|
|
102
112
|
- **One host** — 409 `ambiguous_experiment_host` lists the candidates when several journeys send it. Ask the user which, then re-issue with `host_journey_id`.
|
|
103
113
|
|
|
114
|
+
When in doubt, compete. The cheap mistake is now "they have a draft to activate," not "they already have a live test."
|
|
115
|
+
|
|
104
116
|
### Editing a control that won
|
|
105
117
|
|
|
106
118
|
`edit_communication` and `publish_communication` both take `intent` when the control is protected (it won an experiment, or a published journey is sending it):
|
|
107
119
|
|
|
108
|
-
- `"compete"` — the current content keeps sending
|
|
109
|
-
- `"replace"` — your change becomes what everyone receives, superseding the prior win.
|
|
120
|
+
- `"compete"` — the current content keeps sending. Publish opens a filled-in draft. Then `activate_experiment`. **This is the default when the user hasn't said otherwise**; it is recoverable.
|
|
121
|
+
- `"replace"` — your change becomes what everyone receives, superseding the prior win. No experiment row.
|
|
110
122
|
|
|
111
123
|
Omitting `intent` when it's required returns 409 `intent_required` naming both. Do not pick `replace` to clear the error.
|
|
112
124
|
|
|
@@ -136,6 +148,8 @@ Match Apex UI terminology in user-facing copy: e.g. **Adaptive traffic allocatio
|
|
|
136
148
|
|
|
137
149
|
## Common mistakes
|
|
138
150
|
|
|
151
|
+
- Guessing `target_url` from the brand name or a sibling workspace (e.g. `apex.inc` while the active workspace is `apex-staging`). Use `list_workspace_environments` + the workspace site.
|
|
152
|
+
- Declaring an SDK / Cursor draft ready from hosted screenshots of a public URL. The variant is local until deploy — attach localhost shots with `attach_experiment_asset`.
|
|
139
153
|
- Starting an experiment without a **clear primary metric** tied to the belief.
|
|
140
154
|
- Using different visitor ids for the same user across channels (breaks consistent assignment).
|
|
141
155
|
- Changing variant implementation mid-flight without versioning (invalidates analysis).
|
|
@@ -160,7 +174,7 @@ Editability is gated by lifecycle, keyed to DATA:
|
|
|
160
174
|
|
|
161
175
|
- **Draft, no exposures yet:** fully editable. Use `update_experiment({ experimentId, ... })` to fix a mislabeled surface, rename, or correct the hypothesis/metric/guardrail. This is where typo and surface fixes belong.
|
|
162
176
|
- **Running, or any exposures recorded:** the pre-registration (hypothesis, metric, guardrails) and bucketing (surface, variants) FREEZE. `update_experiment` returns `experiment_locked` with the reason — that's intentional, so results stay valid. Do not fight it.
|
|
163
|
-
- **After freeze:** call `fork_experiment({ experimentId })`. It duplicates the design into a fresh draft (no data carries over, lineage recorded)
|
|
177
|
+
- **After freeze:** call `fork_experiment({ experimentId })`. It duplicates the design into a fresh draft (no data carries over, lineage recorded). A copy gets a new id — replace `useApexVariant("<old id>")` with the new id, deploy, then activate. Preview still works via `_apex_exp`; a normal visit shows control until the new id is shipped.
|
|
164
178
|
|
|
165
179
|
Surface specifically: `create_experiment` auto-detects mobile vs web from repo signals (capacitor.config.* / @apex-inc/capacitor-plugin) and the workspace's registered data sources when you omit `surface`; if a Capacitor app got mislabeled `web`, fix it on the draft with `update_experiment({ experiment_id, surface: "mobile" })` before the first exposure, or fork if it's already running.
|
|
166
180
|
|
|
@@ -20,7 +20,7 @@ Apex ingests **events** and **identities** so experiments, attribution, and inte
|
|
|
20
20
|
## Event naming
|
|
21
21
|
|
|
22
22
|
- Use **snake_case** or **dot-separated** consistent verbs: `page_viewed`, `signup_started`, `trial_started`, `notification_clicked`.
|
|
23
|
-
- Include **context** in properties, not in the event name explosion: `track("
|
|
23
|
+
- Include **context** in properties, not in the event name explosion: `track("ui_action", { action: "click", name: "Start trial", location: "pricing" })` vs dozens of `pricing_start_trial_clicked` events.
|
|
24
24
|
- For experiments, include **`experimentId`** and **`variant`** when the event is relevant to that exposure.
|
|
25
25
|
|
|
26
26
|
## Canonical attributes (the Schema)
|
|
@@ -118,7 +118,7 @@ For agent-driven testing through MCP, use the **`send_server_event`** tool — s
|
|
|
118
118
|
## Event naming conventions
|
|
119
119
|
|
|
120
120
|
- **snake_case**: `signup_completed`, `feature_used`, `invoice_paid`
|
|
121
|
-
- Context in **properties**: `track("
|
|
121
|
+
- Context in **properties**: `track("ui_action", { action: "click", name: "Start trial", location: "pricing" })`
|
|
122
122
|
- For experiments, include `experimentId` and `variant` in properties
|
|
123
123
|
- One primary metric per experiment; secondary metrics as properties
|
|
124
124
|
|
|
@@ -94,6 +94,12 @@ find where each business truth already lives and instrument it there.
|
|
|
94
94
|
- Revenue events MUST carry `value` + `currency`, and a stable
|
|
95
95
|
external id when one exists (order id, invoice id) — dedupe depends
|
|
96
96
|
on it.
|
|
97
|
+
- Do **not** invent one event per button. In-product chrome (named
|
|
98
|
+
buttons, widget close/minimize/move, nav picks) is `ui_action` with
|
|
99
|
+
`action` + `name` + optional `location`. Outcomes stay semantic:
|
|
100
|
+
`integration_selected`, `setup_step_continued`, `integration_connected`.
|
|
101
|
+
`click` is the snippet's outbound-link auto-event; do not use it for
|
|
102
|
+
dashboard buttons.
|
|
97
103
|
|
|
98
104
|
## Vertical event sets (Vertical Widget Packs, 2026-07)
|
|
99
105
|
|