@apex-inc/sdk 0.20.2 → 0.25.0

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.
@@ -1,13 +1,19 @@
1
1
  ---
2
2
  name: apex-adoption
3
- description: Build on Apex's Adoption Engine from the SDK (createManagementClient) — define per-feature adoption milestones over your own events/traits, list/update them, and read aggregated metrics (funnel + lift vs holdout). Use when integrating adoption tracking or activation nudges programmatically. A milestone is NOT a goal.
3
+ description: Build on Apex Adoption from the SDK (createManagementClient) — define Milestones, compose one Adaptive Journey (Nudge and/or Celebration), hours or days, unique Communications. A Milestone is NOT a Target.
4
4
  ---
5
5
 
6
6
  # Apex Adoption (SDK)
7
7
 
8
8
  Name live hosts with `apex.workspace.environments` (or Foundations) before you trust Production adoption numbers. Unmatched hosts stay Unclassified.
9
9
 
10
- The Adoption Engine tracks whether each end-user has adopted each feature, nudges the ones who haven't (one next-best nudge per user, through your journeys), and measures lift against a deterministic holdout.
10
+ Adoption tracks whether each person has finished a Milestone. A Milestone is **This person** unless they set **Anyone here** (`finishWhen: "anyone_here"`). Anyone here = first person on the same Account (`identify({ account: { id } })`) finishes it for the company. Only that person gets the Celebration. Trait finishes stay This person. Company facts (snippet, SSO, billing) → Anyone here. Person skills (ran a report, invited a teammate) → This person. `apex.adoption.update(id, { finishWhen: "anyone_here" })` can flip a live event row; that backfills leftover Nudges.
11
+
12
+ `compose` stamps **one Adaptive Journey** when they want letters — Trigger → Wait until they finish → Celebration and/or Nudge. Just-track is `scaffold: "none"`. Do not clone two templates.
13
+
14
+ ## Words
15
+
16
+ Milestone, Nudge, Celebration, Adaptive Journey, Communication, Segment. A Milestone is not a Target.
11
17
 
12
18
  ## Bootstrap
13
19
 
@@ -20,137 +26,83 @@ const apex = createManagementClient({
20
26
  });
21
27
  ```
22
28
 
23
- ## Define a milestone
24
-
25
- A milestone is "adopted feature X", defined by an event or a trait. It starts inactive.
29
+ ## Define a Milestone
26
30
 
27
31
  ```ts
28
32
  const milestone = await apex.adoption.create({
29
33
  name: "Ran first report",
30
34
  featureKey: "reporting",
31
35
  adoptedWhen: { kind: "event", eventName: "report_run" },
32
- gapPolicy: { kind: "days_since_signup", days: 3 },
36
+ gapPolicy: { kind: "days_since_signup", days: 0, hours: 1 },
37
+ enrollMode: "future",
38
+ finishWhen: "this_person",
33
39
  priority: 10,
34
- // active defaults to false — turn it on when you're ready to nudge.
35
40
  });
36
41
 
37
42
  await apex.adoption.update(milestone.id, { active: true });
38
43
  ```
39
44
 
40
- ### Compose: milestone + celebration journey in one call
41
-
42
- A milestone has two possible journeys, and they're opposites: a **nudge**
43
- (fires when a user *hasn't* adopted) and a **celebration** (fires once the
44
- moment they *do* adopt — congratulate them + tell them what's next).
45
- `compose` creates the milestone and scaffolds its journey legs in a single
46
- call. Use `scaffold: "both"` to produce the whole milestone unit — exactly what
47
- the dashboard sentence on-ramp does:
45
+ ## Compose: one Adaptive Journey
48
46
 
49
47
  ```ts
50
- const { milestone, nudgeJourneyId, celebrationJourneyId } =
51
- await apex.adoption.compose({
52
- name: "Started first experiment",
53
- featureKey: "experiments",
54
- adoptedWhen: { kind: "event", eventName: "experiment_started" },
55
- gapPolicy: { kind: "days_since_signup", days: 7 },
56
- scaffold: "both", // nudge + celebration. Also: "celebration" (default), "nudge", "none".
57
- });
58
- // Both ids are DRAFTS — edit copy in the canvas, then publish each.
59
- ```
60
-
61
- For a single leg, pass `scaffold: "celebration"` (default) or `"nudge"` and an
62
- optional `journeyName`:
63
-
64
- ```ts
65
- const { milestone, journeyId } = await apex.adoption.compose({
48
+ const { milestone, journeyId, sends } = await apex.adoption.compose({
66
49
  name: "Started first experiment",
67
50
  featureKey: "experiments",
68
51
  adoptedWhen: { kind: "event", eventName: "experiment_started" },
69
- scaffold: "celebration",
70
- journeyName: "Congrats on your first experiment",
52
+ gapPolicy: { kind: "days_since_signup", days: 0, hours: 1 },
53
+ enrollMode: "future",
54
+ scaffold: "both", // also: "nudge" | "celebration" | "none"
71
55
  });
72
- // journeyId is a DRAFT — edit the congratulatory message in the canvas, then publish.
56
+ // journeyId is a DRAFT. Preview each send's Communication, then publish.
73
57
  ```
74
58
 
75
- ### Order milestones (unlocks the account-view Path funnel)
59
+ `sends` is `{ role, stepId, commId }[]`. `nudgeJourneyId` is the same id. `celebrationJourneyId` is null on new rows.
76
60
 
77
- ```ts
78
- await apex.adoption.reorder([
79
- activationMilestone.id, // index 0 — reached first
80
- depthMilestone.id, // index 1
81
- ]);
82
- ```
61
+ Each Send has its own Communication. Never reuse `comm-adoption-nudge` / `comm-adoption-celebration`.
62
+
63
+ Extra Nudges: `nudgeClocks: [{ hours: 1 }, { hours: 3 }]`. First is the Wait deadline; later are more hours or days on that arm. One lift for the protocol.
83
64
 
84
- Ordering the full set turns on the "Path" funnel in the account view
85
- (step-by-step drop-off). Unordered milestones stay on the order-agnostic
86
- "Coverage" view — a funnel over an unordered set would imply a false sequence.
65
+ Save as a Segment: `saveNotYetSegment` / `saveDoneSegment`.
87
66
 
88
- Trait-based milestone:
67
+ ## Order
89
68
 
90
69
  ```ts
91
- await apex.adoption.create({
92
- name: "Upgraded to Pro",
93
- featureKey: "pro_plan",
94
- adoptedWhen: { kind: "trait", traitPath: "plan", traitOp: "equals", traitValue: "pro" },
95
- });
70
+ await apex.adoption.reorder([
71
+ activationMilestone.id,
72
+ depthMilestone.id,
73
+ ]);
96
74
  ```
97
75
 
98
76
  ## List + read metrics
99
77
 
100
78
  ```ts
101
79
  const milestones = await apex.adoption.list();
102
-
103
80
  const { metrics, health } = await apex.adoption.metrics();
104
- // metrics: per-milestone { pending, nudged, adopted, holdout, liftPct, ... }
105
- // health: { activeMilestones, totalTracked, totalStuck, totalAdopted, avgLiftPct, stuckByMilestone }
81
+ const person = await apex.adoption.progress(endUserId);
82
+ const company = await apex.adoption.accountProgress(accountId);
106
83
  ```
107
84
 
108
- `liftPct` is nudged-adoption-rate minus holdout-adoption-rate (percentage points) — the causal lift, not attribution. Treat it as directional until the holdout is large enough. **Trait (state) milestones don't report lift** (`liftMeasurable: false`) — a durable trait has no "became true after exposure" moment, so we show raw adoption until a baseline exists.
109
-
110
- ## Feature groupings
111
-
112
- Milestones group under tenant-owned "features" (how the builder organizes your own events). Manage them from the IDE too:
113
-
114
- ```ts
115
- await apex.adoption.features.create({ label: "Subscriptions", eventNames: ["subscription_started"] });
116
- const features = await apex.adoption.features.list();
117
- ```
85
+ Lift is one number for the protocol. Trait Milestones do not report lift (`liftMeasurable: false`).
118
86
 
119
- ## Segments — one grammar for "people who…"
87
+ ## Stills
120
88
 
121
- A segment is a saved group of customers over your own events + traits. Author it with the SAME friendly grammar the dashboard chip builder uses (`conditions`), or drop to a raw `predicate`.
89
+ If they already have the picture, `attach_adoption_milestone_still` (MCP) or `POST /api/adoption/milestones/:id/assets`.
122
90
 
123
- ```ts
124
- // Friendly conditions (compiled + validated server-side):
125
- await apex.segments.create({
126
- name: "Power users who haven't upgraded",
127
- conditions: {
128
- match: "all",
129
- groups: [
130
- { match: "all", conditions: [
131
- { verb: "has", event: "report_run", window_days: 30 },
132
- { attribute: "plan", op: "is_not", value: "pro" },
133
- ] },
134
- ],
135
- },
136
- });
91
+ Signed-in product pages: Playwright on their machine, then upload. Apex cannot sign in for them.
137
92
 
138
- const segments = await apex.segments.list();
93
+ ```
94
+ npx @apex-inc/mcp-server screenshot --milestone <id> --state not_yet --url https://app.theirproduct.com/reports --storage-state .apex/auth.json
95
+ npx @apex-inc/mcp-server screenshot --milestone <id> --state done --url https://app.theirproduct.com/reports --storage-state .apex/auth.json
139
96
  ```
140
97
 
141
- ## Define an event before it fires (define ≠ fire)
142
-
143
- Built a feature but haven't wired Apex yet? Register the event's name so milestones/segments can reference it. **Defining doesn't make it happen** — a developer still calls `apex.track("...")` from the app.
98
+ Save the login file with `npx playwright codegen --save-storage .apex/auth.json <url>` (or reuse their test login). Apex never receives that file.
144
99
 
145
- ```ts
146
- await apex.events.define({ name: "report_exported", description: "User exported a report to CSV" });
147
- // then, in your app code: apex.track("report_exported"); // define → wire → verify
148
- ```
100
+ Public pages: Capture on the card / hosted recapture. A login wall writes nothing.
149
101
 
150
102
  ## Notes
151
103
 
152
- - Milestones are per-user, per-feature — not goals (which are workspace/experiment/journey success).
153
- - `adoptedWhen.eventName` / `traitPath` must match data you already send Apex (track/identify).
154
- - Nudges are marketing-class: they respect unsubscribe, opt-out, and frequency caps, and never target users in an active experiment.
155
- - A scaffolded celebration journey is a **draft**: publish it (and turn the milestone on) for the congratulation to actually send.
156
- - `order` is a display/sequence hint only — it never changes nudge ranking (that's `priority`).
104
+ - A Milestone is per person, per feature — not a Target.
105
+ - New rows default to `enrollMode: "future"`. Do not publish a short clock on `"all"`.
106
+ - `gapPolicy.hours` wins over `days`. `0` is right away. 1 hour is not right away.
107
+ - Nudge honors holdout, opt-out, and the Nudge cap. Celebration is holdout-exempt.
108
+ - The Adaptive Journey is a draft until they publish it.
@@ -154,7 +154,7 @@ did: publish opened a filled-in draft; `activateExperiment` starts the test.
154
154
 
155
155
  `in_experiment` is a hard stop, not an intent question — the content is a
156
156
  treatment mid-measurement. `{ confirmLiveExperiment: true }` overrides it and
157
- **marks that experiment invalid, excluding its result from the belief graph**.
157
+ **marks the numbers untrustworthy: Apex will not call a winner and will not learn from mixed counts**.
158
158
  Offer the alternatives first: wait for it to finish, or conclude it so the
159
159
  learning is captured.
160
160
 
@@ -69,7 +69,7 @@ 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.** 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.
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 })` or `npx @apex-inc/mcp-server screenshot --experiment … --storage-state <login file>` for a signed-in page (Apex never receives that file). 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
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.
74
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.
75
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.
@@ -154,6 +154,7 @@ Match Apex UI terminology in user-facing copy: e.g. **Adaptive traffic allocatio
154
154
  - Hiding the page until `/assign` returns (worse LCP; not the React-hook promise).
155
155
  - Changing variant implementation mid-flight without versioning (invalidates analysis).
156
156
  - Only using p-values without effect size or business context.
157
+ - Shipping onto a live test. Before you edit a page, commit, or push — even if they never said experiment — call `check_live_tests` (or `checkLiveTests` on the management client) with the **full file contents**, not the diff hunk. Before you change a letter, Adaptive Journey, or Milestone, pass those ids too. If something live is on those files or ids, **stop and ask**. Three outs: end the test then ship; wait; or they say this change is not the thing under test (they mark that on the experiment page). Do not edit. Do not commit.
157
158
 
158
159
  ## Related
159
160