@apex-inc/sdk 0.8.0 → 0.10.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.
package/README.md CHANGED
@@ -30,6 +30,21 @@ identify("user_123", { email: "jane@example.com" });
30
30
  track("page_viewed", { path: "/pricing" });
31
31
  ```
32
32
 
33
+ ### Profile photos (`avatar_url`)
34
+
35
+ Pass `avatar_url` in traits to show end-user photos across Apex — the
36
+ Customers list, customer detail pages, and the Live Customers widget. It's a
37
+ [canonical attribute](https://app.apex.inc/docs/concepts/schema), so
38
+ `photoUrl` / `image` / `profile_image` map to it too. Apex validates on write
39
+ (https only) and falls back to colored initials when absent.
40
+
41
+ ```typescript
42
+ identify("user_123", {
43
+ email: "jane@example.com",
44
+ avatar_url: user.photoURL, // OIDC `picture` claim works the same way
45
+ });
46
+ ```
47
+
33
48
  ## PLG primitives
34
49
 
35
50
  In addition to `track` / `identify` the SDK exposes three PLG-specific
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GENERATED FILE — DO NOT EDIT BY HAND.
3
+ *
4
+ * Typed Apex event payloads, generated from the Apex Spec registries by
5
+ * `app/scripts/generate-spec-types.ts`. Regenerate with:
6
+ *
7
+ * npx tsx app/scripts/generate-spec-types.ts
8
+ *
9
+ * The `codegen.test.ts` stale gate fails CI if this drifts from the spec.
10
+ */
11
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GENERATED FILE — DO NOT EDIT BY HAND.
3
+ *
4
+ * Typed Apex identify() traits, generated from the Apex Spec attribute
5
+ * registry by `app/scripts/generate-spec-types.ts`. Regenerate with:
6
+ *
7
+ * npx tsx app/scripts/generate-spec-types.ts
8
+ *
9
+ * The `codegen.test.ts` stale gate fails CI if this drifts from the spec.
10
+ */
11
+ export {};
@@ -28,6 +28,136 @@ export function createManagementClient(cfg) {
28
28
  }
29
29
  return res.json();
30
30
  },
31
+ /**
32
+ * "Is our growth real?" — per-goal cumulative lift (journeys + experiments)
33
+ * vs the do-nothing global holdout, with 95% CIs. Forward-only; goals still
34
+ * collecting are flagged `isDecisive: false`. (Is Our Growth Real, Phase 4.)
35
+ */
36
+ async getGrowthReality(opts) {
37
+ const days = opts?.days ?? "all";
38
+ const res = await fetch(`${root}/api/growth/reality?days=${days}`, { headers });
39
+ if (!res.ok) {
40
+ const text = await res.text().catch(() => "");
41
+ throw new Error(`Apex getGrowthReality failed (${res.status}): ${text}`);
42
+ }
43
+ return res.json();
44
+ },
45
+ /**
46
+ * Channel Economics Cockpit (Layer 1) — gross-margin-adjusted LTV:CAC,
47
+ * payback, and per-channel cohort curves by acquisition channel (raw
48
+ * first-touch), plus a directional multi-touch lens. Reads real contacts +
49
+ * spend + revenue; metrics with missing inputs come back null, never
50
+ * fabricated. `days` defaults to all-time. Wraps GET /api/channels/economics.
51
+ */
52
+ async getChannelEconomics(opts) {
53
+ const days = opts?.days ?? "all";
54
+ const res = await fetch(`${root}/api/channels/economics?days=${days}`, { headers });
55
+ if (!res.ok) {
56
+ const text = await res.text().catch(() => "");
57
+ throw new Error(`Apex getChannelEconomics failed (${res.status}): ${text}`);
58
+ }
59
+ return res.json();
60
+ },
61
+ // ─── Vertical Widget Packs read surface (2026-07-02) ─────────────────
62
+ // Thin wrappers over the pack read APIs. Param names match the API
63
+ // query params exactly (shipped params ARE the SDK signature). All
64
+ // endpoints require the `analytics:read` API-key scope; responses
65
+ // apply the same small-n suppression as the dashboard widgets.
66
+ /**
67
+ * SaaS pack — MRR / ARR / monthly waterfall (new / expansion /
68
+ * reactivation / contraction / churn) + NRR / GRR for the last
69
+ * complete month, from materialized monthly snapshots (UTC calendar
70
+ * months; the current month is flagged `inProgress`). Wraps
71
+ * GET /api/saas/recurring-revenue.
72
+ */
73
+ async getSaasRecurringRevenue(opts) {
74
+ const q = opts?.months ? `?months=${opts.months}` : "";
75
+ const res = await fetch(`${root}/api/saas/recurring-revenue${q}`, { headers });
76
+ if (!res.ok) {
77
+ const text = await res.text().catch(() => "");
78
+ throw new Error(`Apex getSaasRecurringRevenue failed (${res.status}): ${text}`);
79
+ }
80
+ return res.json();
81
+ },
82
+ /**
83
+ * SaaS pack — expansion vs contraction MRR, expanded-account counts,
84
+ * and seat growth. Wraps GET /api/saas/expansion.
85
+ */
86
+ async getSaasExpansion(opts) {
87
+ const q = opts?.months ? `?months=${opts.months}` : "";
88
+ const res = await fetch(`${root}/api/saas/expansion${q}`, { headers });
89
+ if (!res.ok) {
90
+ const text = await res.text().catch(() => "");
91
+ throw new Error(`Apex getSaasExpansion failed (${res.status}): ${text}`);
92
+ }
93
+ return res.json();
94
+ },
95
+ /**
96
+ * DTC pack — per-product funnel (views → adds → purchases → refunds)
97
+ * via the shared item aggregator. Wraps GET /api/dtc/merchandising.
98
+ */
99
+ async getDtcMerchandising(opts) {
100
+ const q = opts?.days ? `?days=${opts.days}` : "";
101
+ const res = await fetch(`${root}/api/dtc/merchandising${q}`, { headers });
102
+ if (!res.ok) {
103
+ const text = await res.text().catch(() => "");
104
+ throw new Error(`Apex getDtcMerchandising failed (${res.status}): ${text}`);
105
+ }
106
+ return res.json();
107
+ },
108
+ /**
109
+ * DTC pack — refund rate (money back) and return rate (goods back —
110
+ * separate metrics, never summed), reasons, refund cycle time, AOV,
111
+ * repeat purchase. Wraps GET /api/dtc/returns.
112
+ */
113
+ async getDtcReturns(opts) {
114
+ const q = opts?.days ? `?days=${opts.days}` : "";
115
+ const res = await fetch(`${root}/api/dtc/returns${q}`, { headers });
116
+ if (!res.ok) {
117
+ const text = await res.text().catch(() => "");
118
+ throw new Error(`Apex getDtcReturns failed (${res.status}): ${text}`);
119
+ }
120
+ return res.json();
121
+ },
122
+ /**
123
+ * Marketplace pack — the six pillars (liquidity, supply & demand,
124
+ * economics GMV-vs-take, retention, trust, concentration), sliceable
125
+ * by category / geo / price_band. Low-volume slices are suppressed
126
+ * (k-anonymity floor). Wraps GET /api/marketplace/metrics.
127
+ */
128
+ async getMarketplaceMetrics(opts) {
129
+ const params = new URLSearchParams();
130
+ if (opts?.days)
131
+ params.set("days", String(opts.days));
132
+ if (opts?.category)
133
+ params.set("category", opts.category);
134
+ if (opts?.geo)
135
+ params.set("geo", opts.geo);
136
+ if (opts?.price_band)
137
+ params.set("price_band", opts.price_band);
138
+ const qs = params.toString();
139
+ const res = await fetch(`${root}/api/marketplace/metrics${qs ? `?${qs}` : ""}`, { headers });
140
+ if (!res.ok) {
141
+ const text = await res.text().catch(() => "");
142
+ throw new Error(`Apex getMarketplaceMetrics failed (${res.status}): ${text}`);
143
+ }
144
+ return res.json();
145
+ },
146
+ /**
147
+ * Agency pack — per-client (= linked workspace) spend, revenue, ROI,
148
+ * MoM series, and the transparent at-risk rule. NOTE: this endpoint is
149
+ * a FINANCIAL boundary gated to org owner/admin SESSIONS (no API-key
150
+ * lane yet — CISO decision 2026-07-02); calling it with an API key
151
+ * returns 401/403. Wraps GET /api/agency/clients.
152
+ */
153
+ async getClientPortfolio(opts) {
154
+ const res = await fetch(`${root}/api/agency/clients?orgId=${encodeURIComponent(opts.orgId)}`, { headers });
155
+ if (!res.ok) {
156
+ const text = await res.text().catch(() => "");
157
+ throw new Error(`Apex getClientPortfolio failed (${res.status}): ${text}`);
158
+ }
159
+ return res.json();
160
+ },
31
161
  async assignVariant(experimentId, visitorId) {
32
162
  const cookie = [
33
163
  `apex_vid=${encodeURIComponent(visitorId)}`,
@@ -70,6 +200,99 @@ export function createManagementClient(cfg) {
70
200
  throw new Error(`Apex promoteExperiment failed (${res.status}): ${await res.text().catch(() => "")}`);
71
201
  return res.json();
72
202
  },
203
+ // ─── Conversions (Goals & Conversions Convergence) ────────────────
204
+ //
205
+ // A Conversion is the merchant's owned-event definition of a valued
206
+ // action ("Purchase", "Signup"). One workspace registry, one
207
+ // `isPrimary`. Experiments and journeys reference a Conversion's
208
+ // owned-event source as their objective; the headline funnel and
209
+ // scoring use the primary one. These wrap the same `/api/conversion-goals`
210
+ // endpoints the dashboard + MCP server use.
211
+ conversions: {
212
+ /** List the workspace's owned-event Conversions (archived excluded by default). */
213
+ async list(opts) {
214
+ const res = await fetch(`${root}/api/conversion-goals`, { headers });
215
+ if (!res.ok)
216
+ throw new Error(`Apex conversions.list failed (${res.status}): ${await res.text().catch(() => "")}`);
217
+ const json = (await res.json());
218
+ const all = Array.isArray(json) ? json : [];
219
+ return opts?.includeArchived ? all : all.filter((c) => !c.archivedAt);
220
+ },
221
+ /**
222
+ * Define a new owned-event Conversion. `custom_event` (with `eventName`)
223
+ * is the most common shape and matches an Apex Spec event sent via track.
224
+ * Set `isPrimary` to make it the default experiment objective + headline metric.
225
+ */
226
+ async create(params) {
227
+ const body = {
228
+ name: params.name,
229
+ type: params.type,
230
+ metricType: "binary",
231
+ isPrimary: params.isPrimary ?? false,
232
+ ...(params.eventName ? { eventName: params.eventName } : {}),
233
+ ...(params.selector ? { selector: params.selector } : {}),
234
+ ...(params.targetUrl ? { targetUrl: params.targetUrl } : {}),
235
+ };
236
+ const res = await fetch(`${root}/api/conversion-goals`, {
237
+ method: "POST", headers, body: JSON.stringify(body),
238
+ });
239
+ if (!res.ok)
240
+ throw new Error(`Apex conversions.create failed (${res.status}): ${await res.text().catch(() => "")}`);
241
+ return res.json();
242
+ },
243
+ /** Promote a Conversion to primary (only one can be primary at a time). */
244
+ async setPrimary(conversionId) {
245
+ const res = await fetch(`${root}/api/conversion-goals`, {
246
+ method: "PATCH", headers, body: JSON.stringify({ id: conversionId, isPrimary: true }),
247
+ });
248
+ if (!res.ok)
249
+ throw new Error(`Apex conversions.setPrimary failed (${res.status}): ${await res.text().catch(() => "")}`);
250
+ return res.json();
251
+ },
252
+ },
253
+ // ─── Targets (Goals & Conversions Convergence) ────────────────────
254
+ //
255
+ // A Target is an aggregate KPI number to hit by a date (MRR, pipeline,
256
+ // "1,000 signups by Q3"). Bind one to a Conversion via
257
+ // `sourceConversionId` to auto-count progress from that conversion's
258
+ // owned-event completions; otherwise `current` is a manual number.
259
+ // `currentIsAuto` flags an auto-computed value; `sourceMissing` flags a
260
+ // bound Conversion that has since been archived. Wraps `/api/targets`.
261
+ targets: {
262
+ async list() {
263
+ const res = await fetch(`${root}/api/targets`, { headers });
264
+ if (!res.ok)
265
+ throw new Error(`Apex targets.list failed (${res.status}): ${await res.text().catch(() => "")}`);
266
+ const json = (await res.json());
267
+ if (Array.isArray(json))
268
+ return json;
269
+ return json.data ?? [];
270
+ },
271
+ async create(params) {
272
+ const res = await fetch(`${root}/api/targets`, {
273
+ method: "POST", headers, body: JSON.stringify(params),
274
+ });
275
+ if (!res.ok)
276
+ throw new Error(`Apex targets.create failed (${res.status}): ${await res.text().catch(() => "")}`);
277
+ return res.json();
278
+ },
279
+ /** Patch a Target. Pass `sourceConversionId: null` to unbind (back to manual). */
280
+ async update(id, patch) {
281
+ const res = await fetch(`${root}/api/targets`, {
282
+ method: "PATCH", headers, body: JSON.stringify({ id, ...patch }),
283
+ });
284
+ if (!res.ok)
285
+ throw new Error(`Apex targets.update failed (${res.status}): ${await res.text().catch(() => "")}`);
286
+ return res.json();
287
+ },
288
+ async delete(id) {
289
+ const res = await fetch(`${root}/api/targets?id=${encodeURIComponent(id)}`, {
290
+ method: "DELETE", headers,
291
+ });
292
+ if (!res.ok)
293
+ throw new Error(`Apex targets.delete failed (${res.status}): ${await res.text().catch(() => "")}`);
294
+ },
295
+ },
73
296
  // ─── Communications ───────────────────────────────────────────────
74
297
  /** Append a content variant to a communication (composer-columns model; an experiment arm). */
75
298
  async createCommVariant(commId, opts) {