@apex-inc/sdk 0.1.1 → 0.6.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 +70 -2
- package/dist/esm/index.js +175 -7
- package/dist/esm/management.js +395 -3
- package/dist/esm/plg-events.js +45 -0
- package/dist/index.d.ts +254 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +182 -8
- package/dist/index.js.map +1 -1
- package/dist/management.d.ts +285 -2
- package/dist/management.d.ts.map +1 -1
- package/dist/management.js +395 -3
- package/dist/management.js.map +1 -1
- package/dist/plg-events.d.ts +131 -0
- package/dist/plg-events.d.ts.map +1 -0
- package/dist/plg-events.js +50 -0
- package/dist/plg-events.js.map +1 -0
- package/package.json +6 -1
- package/skills/apex-communications/SKILL.md +217 -1
- package/skills/apex-experimentation/SKILL.md +2 -2
- package/skills/apex-growth-tracking/SKILL.md +93 -2
- package/skills/apex-integration-cookbook/SKILL.md +141 -26
- package/skills/apex-partner-network/SKILL.md +191 -0
package/dist/esm/management.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Programmatic access to Apex APIs using an API key (same as MCP server).
|
|
3
|
-
* Intended for Node.js backends; sends x-api-key and x-apex-
|
|
3
|
+
* Intended for Node.js backends; sends x-api-key and x-apex-workspace on each request.
|
|
4
4
|
*/
|
|
5
5
|
function baseUrl(cfg) {
|
|
6
6
|
return (cfg.apiUrl ?? "http://localhost:3000").replace(/\/$/, "");
|
|
@@ -9,7 +9,7 @@ function keyHeaders(cfg) {
|
|
|
9
9
|
return {
|
|
10
10
|
"Content-Type": "application/json",
|
|
11
11
|
"x-api-key": cfg.apiKey,
|
|
12
|
-
"x-apex-
|
|
12
|
+
"x-apex-workspace": cfg.workspaceKey,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -31,7 +31,7 @@ export function createManagementClient(cfg) {
|
|
|
31
31
|
async assignVariant(experimentId, visitorId) {
|
|
32
32
|
const cookie = [
|
|
33
33
|
`apex_vid=${encodeURIComponent(visitorId)}`,
|
|
34
|
-
`apex_project=${encodeURIComponent(cfg.
|
|
34
|
+
`apex_project=${encodeURIComponent(cfg.workspaceKey)}`,
|
|
35
35
|
].join("; ");
|
|
36
36
|
const res = await fetch(`${root}/api/experiments/${encodeURIComponent(experimentId)}/assign`, {
|
|
37
37
|
headers: { ...headers, Cookie: cookie },
|
|
@@ -42,7 +42,49 @@ export function createManagementClient(cfg) {
|
|
|
42
42
|
}
|
|
43
43
|
return res.json();
|
|
44
44
|
},
|
|
45
|
+
/** Fetch a single experiment (unified shape). */
|
|
46
|
+
async getExperiment(id) {
|
|
47
|
+
const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}?unified=true`, { headers });
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`Apex getExperiment failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
50
|
+
return res.json();
|
|
51
|
+
},
|
|
52
|
+
/**
|
|
53
|
+
* Computed results + the shared readiness verdict
|
|
54
|
+
* (collecting | decisive | inconclusive | guardrail) for any surface.
|
|
55
|
+
*/
|
|
56
|
+
async getExperimentResults(id) {
|
|
57
|
+
const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}/results`, { headers });
|
|
58
|
+
if (!res.ok)
|
|
59
|
+
throw new Error(`Apex getExperimentResults failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
60
|
+
return res.json();
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* Promote the experiment's winner (re-verifies the readiness gate
|
|
64
|
+
* server-side; pass `force` to override). Winner content is written into
|
|
65
|
+
* the canonical communication as a new version.
|
|
66
|
+
*/
|
|
67
|
+
async promoteExperiment(id, opts) {
|
|
68
|
+
const res = await fetch(`${root}/api/experiments/${encodeURIComponent(id)}/promote`, { method: "POST", headers, body: JSON.stringify(opts ?? {}) });
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
throw new Error(`Apex promoteExperiment failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
71
|
+
return res.json();
|
|
72
|
+
},
|
|
45
73
|
// ─── Communications ───────────────────────────────────────────────
|
|
74
|
+
/** Append a content variant to a communication (composer-columns model; an experiment arm). */
|
|
75
|
+
async createCommVariant(commId, opts) {
|
|
76
|
+
const res = await fetch(`${root}/api/communications/${encodeURIComponent(commId)}/variants`, { method: "POST", headers, body: JSON.stringify(opts ?? {}) });
|
|
77
|
+
if (!res.ok)
|
|
78
|
+
throw new Error(`Apex createCommVariant failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
79
|
+
return res.json();
|
|
80
|
+
},
|
|
81
|
+
/** List the content variants (`content.variants[]`) of a communication. */
|
|
82
|
+
async listCommVariants(commId) {
|
|
83
|
+
const res = await fetch(`${root}/api/communications/${encodeURIComponent(commId)}/variants`, { headers });
|
|
84
|
+
if (!res.ok)
|
|
85
|
+
throw new Error(`Apex listCommVariants failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
86
|
+
return res.json();
|
|
87
|
+
},
|
|
46
88
|
async recommendCommunications() {
|
|
47
89
|
const res = await fetch(`${root}/api/communications/recommend`, { method: "POST", headers });
|
|
48
90
|
if (!res.ok)
|
|
@@ -148,5 +190,355 @@ export function createManagementClient(cfg) {
|
|
|
148
190
|
throw new Error(`Apex getEventIntegrationStatus failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
149
191
|
return res.json();
|
|
150
192
|
},
|
|
193
|
+
// ─── Partner Network — Programs (MMP-161) ─────────────────────────
|
|
194
|
+
//
|
|
195
|
+
// Every method below runs under the merchant's x-apex-workspace scope.
|
|
196
|
+
// Partner-facing flows (self-signup, dashboard, Stripe Connect OAuth)
|
|
197
|
+
// are cookie-authed in the portal and not wrapped in this SDK.
|
|
198
|
+
async listPrograms() {
|
|
199
|
+
const res = await fetch(`${root}/api/mobile/affiliates/programs`, { headers });
|
|
200
|
+
if (!res.ok)
|
|
201
|
+
throw new Error(`Apex listPrograms failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
202
|
+
return res.json();
|
|
203
|
+
},
|
|
204
|
+
async createProgram(params) {
|
|
205
|
+
const res = await fetch(`${root}/api/mobile/affiliates/programs`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers,
|
|
208
|
+
body: JSON.stringify(params),
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok)
|
|
211
|
+
throw new Error(`Apex createProgram failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
212
|
+
return res.json();
|
|
213
|
+
},
|
|
214
|
+
async updateProgram(programId, updates) {
|
|
215
|
+
const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}`, {
|
|
216
|
+
method: "PATCH",
|
|
217
|
+
headers,
|
|
218
|
+
body: JSON.stringify(updates),
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok)
|
|
221
|
+
throw new Error(`Apex updateProgram failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
222
|
+
return res.json();
|
|
223
|
+
},
|
|
224
|
+
async listProgramMembers(programId) {
|
|
225
|
+
const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}/members`, { headers });
|
|
226
|
+
if (!res.ok)
|
|
227
|
+
throw new Error(`Apex listProgramMembers failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
228
|
+
return res.json();
|
|
229
|
+
},
|
|
230
|
+
/** Apply program default commission to every existing member (respects overrides). */
|
|
231
|
+
async applyProgramDefaultToMembers(programId) {
|
|
232
|
+
const res = await fetch(`${root}/api/mobile/affiliates/programs/${encodeURIComponent(programId)}/apply-to-members`, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers,
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok)
|
|
237
|
+
throw new Error(`Apex applyProgramDefaultToMembers failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
238
|
+
return res.json();
|
|
239
|
+
},
|
|
240
|
+
// ─── Partner Network — Rate overrides ─────────────────────────────
|
|
241
|
+
/** Override a single membership's commission (sticky — survives program default updates). */
|
|
242
|
+
async updateMembershipRate(profileId, params) {
|
|
243
|
+
const res = await fetch(`${root}/api/mobile/affiliates/memberships/${encodeURIComponent(profileId)}/rate`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers,
|
|
246
|
+
body: JSON.stringify(params),
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok)
|
|
249
|
+
throw new Error(`Apex updateMembershipRate failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
250
|
+
return res.json();
|
|
251
|
+
},
|
|
252
|
+
/** Reset a membership back to its program's default commission. */
|
|
253
|
+
async resetMembershipRate(profileId, reason) {
|
|
254
|
+
const res = await fetch(`${root}/api/mobile/affiliates/memberships/${encodeURIComponent(profileId)}/rate`, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers,
|
|
257
|
+
body: JSON.stringify({ reset: true, reason }),
|
|
258
|
+
});
|
|
259
|
+
if (!res.ok)
|
|
260
|
+
throw new Error(`Apex resetMembershipRate failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
261
|
+
return res.json();
|
|
262
|
+
},
|
|
263
|
+
// ─── Partner Network — Invitations ────────────────────────────────
|
|
264
|
+
/** Send a magic-link invite to a partner (email resolves to existing profile or creates pending). */
|
|
265
|
+
async invitePartner(params) {
|
|
266
|
+
const res = await fetch(`${root}/api/mobile/affiliates/invites`, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers,
|
|
269
|
+
body: JSON.stringify(params),
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok)
|
|
272
|
+
throw new Error(`Apex invitePartner failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
273
|
+
return res.json();
|
|
274
|
+
},
|
|
275
|
+
// ─── Partner Network — Payouts (MMP-145 + MMP-151) ────────────────
|
|
276
|
+
/** Pending payouts grouped into aging buckets (0-30, 30-60, 60+). */
|
|
277
|
+
async listPendingPayouts() {
|
|
278
|
+
const res = await fetch(`${root}/api/mobile/affiliates/payouts/pending`, { headers });
|
|
279
|
+
if (!res.ok)
|
|
280
|
+
throw new Error(`Apex listPendingPayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
281
|
+
return res.json();
|
|
282
|
+
},
|
|
283
|
+
/** Approve one or more pending payouts (creates HoldbackEntry per approved conversion). */
|
|
284
|
+
async approvePayouts(conversionIds) {
|
|
285
|
+
const res = await fetch(`${root}/api/mobile/affiliates/payouts/approve`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers,
|
|
288
|
+
body: JSON.stringify({ conversionIds }),
|
|
289
|
+
});
|
|
290
|
+
if (!res.ok)
|
|
291
|
+
throw new Error(`Apex approvePayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
292
|
+
return res.json();
|
|
293
|
+
},
|
|
294
|
+
/** Reject pending payouts (opens an audit FraudCase severity=low). */
|
|
295
|
+
async rejectPayouts(conversionIds, reason) {
|
|
296
|
+
const res = await fetch(`${root}/api/mobile/affiliates/payouts/reject`, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers,
|
|
299
|
+
body: JSON.stringify({ conversionIds, reason }),
|
|
300
|
+
});
|
|
301
|
+
if (!res.ok)
|
|
302
|
+
throw new Error(`Apex rejectPayouts failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
303
|
+
return res.json();
|
|
304
|
+
},
|
|
305
|
+
// ─── Partner Network — Fees (MMP-158) ─────────────────────────────
|
|
306
|
+
/** Merchant-facing fee disclosure. Returns every fee kind Apex charges this merchant. */
|
|
307
|
+
async getEffectiveFees() {
|
|
308
|
+
const res = await fetch(`${root}/api/mobile/affiliates/fees`, { headers });
|
|
309
|
+
if (!res.ok)
|
|
310
|
+
throw new Error(`Apex getEffectiveFees failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
311
|
+
return res.json();
|
|
312
|
+
},
|
|
313
|
+
// ─── Partner Network — Vouch (MMP-156) ────────────────────────────
|
|
314
|
+
/**
|
|
315
|
+
* Vouch for a partner — one-tier bump for 60 days. Requires indemnity
|
|
316
|
+
* acknowledgment. Fails if the partner is already verified.
|
|
317
|
+
*/
|
|
318
|
+
async vouchForPartner(profileId, params) {
|
|
319
|
+
const res = await fetch(`${root}/api/mobile/affiliates/${encodeURIComponent(profileId)}/vouch`, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers,
|
|
322
|
+
body: JSON.stringify(params),
|
|
323
|
+
});
|
|
324
|
+
if (!res.ok)
|
|
325
|
+
throw new Error(`Apex vouchForPartner failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
326
|
+
return res.json();
|
|
327
|
+
},
|
|
328
|
+
// ─── Communications — end-user preferences (DFC-016) ─────────────────
|
|
329
|
+
//
|
|
330
|
+
// Two API-key methods for managing an end-user's communication
|
|
331
|
+
// preferences from the customer's backend. `preferences.get` and
|
|
332
|
+
// `preferences.update` proxy `/api/communications/preferences/[id]`
|
|
333
|
+
// with the org-scoped key — the route's dual-auth layer treats the
|
|
334
|
+
// API-key caller as a trusted server.
|
|
335
|
+
preferences: {
|
|
336
|
+
async get(endUserId) {
|
|
337
|
+
const res = await fetch(`${root}/api/communications/preferences/${encodeURIComponent(endUserId)}?workspaceKey=${encodeURIComponent(cfg.workspaceKey)}`, { headers });
|
|
338
|
+
if (!res.ok) {
|
|
339
|
+
throw new Error(`Apex preferences.get failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
340
|
+
}
|
|
341
|
+
return res.json();
|
|
342
|
+
},
|
|
343
|
+
async update(endUserId, patch) {
|
|
344
|
+
const res = await fetch(`${root}/api/communications/preferences/${encodeURIComponent(endUserId)}?workspaceKey=${encodeURIComponent(cfg.workspaceKey)}`, {
|
|
345
|
+
method: "PATCH",
|
|
346
|
+
headers,
|
|
347
|
+
body: JSON.stringify(patch),
|
|
348
|
+
});
|
|
349
|
+
if (!res.ok) {
|
|
350
|
+
throw new Error(`Apex preferences.update failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
351
|
+
}
|
|
352
|
+
return res.json();
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
// ─── Communications — inbox notifications (DFC-016) ──────────────────
|
|
356
|
+
//
|
|
357
|
+
// Read + write helpers for the in-app inbox. The `mark-read` and
|
|
358
|
+
// `mark-all-read` paths target the existing PATCH route on the
|
|
359
|
+
// workspace-admin gate (the SDK's API key satisfies that gate).
|
|
360
|
+
notifications: {
|
|
361
|
+
async list(endUserId, opts) {
|
|
362
|
+
const qs = new URLSearchParams({ userId: endUserId });
|
|
363
|
+
if (opts?.limit)
|
|
364
|
+
qs.set("limit", String(opts.limit));
|
|
365
|
+
if (opts?.unreadOnly)
|
|
366
|
+
qs.set("unreadOnly", "true");
|
|
367
|
+
const res = await fetch(`${root}/api/communications/notifications?${qs.toString()}`, { headers });
|
|
368
|
+
if (!res.ok) {
|
|
369
|
+
throw new Error(`Apex notifications.list failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
370
|
+
}
|
|
371
|
+
return res.json();
|
|
372
|
+
},
|
|
373
|
+
async markRead(notificationId) {
|
|
374
|
+
const res = await fetch(`${root}/api/communications/notifications`, {
|
|
375
|
+
method: "PATCH",
|
|
376
|
+
headers,
|
|
377
|
+
body: JSON.stringify({ notificationId }),
|
|
378
|
+
});
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
throw new Error(`Apex notifications.markRead failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
async markAllRead(endUserId) {
|
|
384
|
+
const res = await fetch(`${root}/api/communications/notifications`, {
|
|
385
|
+
method: "PATCH",
|
|
386
|
+
headers,
|
|
387
|
+
body: JSON.stringify({ markAll: true, endUserId }),
|
|
388
|
+
});
|
|
389
|
+
if (!res.ok) {
|
|
390
|
+
throw new Error(`Apex notifications.markAllRead failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
// ─── Communications — end-user token mint (DFC-018) ──────────────────
|
|
395
|
+
//
|
|
396
|
+
// Server-side helper that asks Apex to sign an HMAC end-user token
|
|
397
|
+
// for the customer's client. The signing key lives on Apex servers
|
|
398
|
+
// only, so this is the canonical path. Customers call this from
|
|
399
|
+
// their backend, then hand the token to <ApexPreferenceCenter>.
|
|
400
|
+
/**
|
|
401
|
+
* Set up Apex — programmatic access to per-workspace setup state.
|
|
402
|
+
*
|
|
403
|
+
* `get()` returns the current hub signal + per-step overrides
|
|
404
|
+
* (read-only). `complete(stepId)` / `skip(stepId, reason)` mirror
|
|
405
|
+
* the dashboard's write contract at
|
|
406
|
+
* POST /api/workspaces/<workspaceKey>/setup-state/<stepId>.
|
|
407
|
+
*
|
|
408
|
+
* Allowed step IDs are enforced server-side via the SetupRegistry
|
|
409
|
+
* allowlist. Unknown IDs return 400.
|
|
410
|
+
*/
|
|
411
|
+
workspace: {
|
|
412
|
+
setupState: {
|
|
413
|
+
async get() {
|
|
414
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/setup-state`, { headers });
|
|
415
|
+
if (!res.ok) {
|
|
416
|
+
throw new Error(`Apex workspace.setupState.get failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
417
|
+
}
|
|
418
|
+
const json = (await res.json());
|
|
419
|
+
return {
|
|
420
|
+
overrides: json.overrides ?? [],
|
|
421
|
+
signal: json.signal ?? {},
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
async complete(stepId) {
|
|
425
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/setup-state/${encodeURIComponent(stepId)}/complete`, { method: "POST", headers });
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
throw new Error(`Apex workspace.setupState.complete failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
async skip(stepId, reason) {
|
|
431
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/setup-state/${encodeURIComponent(stepId)}/skip`, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers,
|
|
434
|
+
body: JSON.stringify({ reason: reason ?? null }),
|
|
435
|
+
});
|
|
436
|
+
if (!res.ok) {
|
|
437
|
+
throw new Error(`Apex workspace.setupState.skip failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
/**
|
|
443
|
+
* Data Sources registry — the producers that send events into this
|
|
444
|
+
* workspace (Website, iOS / Android apps, Backend). Mirrors
|
|
445
|
+
* /api/workspaces/<workspaceKey>/data-sources. `dataSourceId` is
|
|
446
|
+
* reporting-only; these methods manage the registry, not access control.
|
|
447
|
+
*/
|
|
448
|
+
dataSources: {
|
|
449
|
+
async list(opts) {
|
|
450
|
+
const qs = opts?.includeArchived ? "?includeArchived=true" : "";
|
|
451
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources${qs}`, { headers });
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
throw new Error(`Apex dataSources.list failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
454
|
+
}
|
|
455
|
+
const json = (await res.json());
|
|
456
|
+
return json.data ?? [];
|
|
457
|
+
},
|
|
458
|
+
async get(id) {
|
|
459
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources/${encodeURIComponent(id)}`, { headers });
|
|
460
|
+
if (!res.ok) {
|
|
461
|
+
throw new Error(`Apex dataSources.get failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
462
|
+
}
|
|
463
|
+
const json = (await res.json());
|
|
464
|
+
return json.data;
|
|
465
|
+
},
|
|
466
|
+
async create(params) {
|
|
467
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources`, { method: "POST", headers, body: JSON.stringify(params) });
|
|
468
|
+
if (!res.ok) {
|
|
469
|
+
throw new Error(`Apex dataSources.create failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
470
|
+
}
|
|
471
|
+
const json = (await res.json());
|
|
472
|
+
return json.data;
|
|
473
|
+
},
|
|
474
|
+
async update(id, patch) {
|
|
475
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources/${encodeURIComponent(id)}`, { method: "PATCH", headers, body: JSON.stringify(patch) });
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
throw new Error(`Apex dataSources.update failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
478
|
+
}
|
|
479
|
+
const json = (await res.json());
|
|
480
|
+
return json.data;
|
|
481
|
+
},
|
|
482
|
+
async archive(id) {
|
|
483
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources/${encodeURIComponent(id)}`, { method: "DELETE", headers });
|
|
484
|
+
if (!res.ok) {
|
|
485
|
+
throw new Error(`Apex dataSources.archive failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
async health(id) {
|
|
489
|
+
const res = await fetch(`${root}/api/workspaces/${encodeURIComponent(cfg.workspaceKey)}/data-sources/${encodeURIComponent(id)}/health`, { headers });
|
|
490
|
+
if (!res.ok) {
|
|
491
|
+
throw new Error(`Apex dataSources.health failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
492
|
+
}
|
|
493
|
+
const json = (await res.json());
|
|
494
|
+
return json.data;
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
/**
|
|
498
|
+
* Unified analytics reads — per-source / per-platform / per-hostname
|
|
499
|
+
* breakdowns from the ingest-maintained rollups. Honors the source/
|
|
500
|
+
* platform/hostname/range filters the dashboard exposes.
|
|
501
|
+
*/
|
|
502
|
+
analytics: {
|
|
503
|
+
async sources(opts) {
|
|
504
|
+
const qs = new URLSearchParams();
|
|
505
|
+
if (opts?.days)
|
|
506
|
+
qs.set("days", String(opts.days));
|
|
507
|
+
if (opts?.source)
|
|
508
|
+
qs.set("source", opts.source);
|
|
509
|
+
const suffix = qs.toString() ? `?${qs}` : "";
|
|
510
|
+
const res = await fetch(`${root}/api/analytics/sources${suffix}`, {
|
|
511
|
+
headers,
|
|
512
|
+
});
|
|
513
|
+
if (!res.ok) {
|
|
514
|
+
throw new Error(`Apex analytics.sources failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
515
|
+
}
|
|
516
|
+
const json = (await res.json());
|
|
517
|
+
return json.data;
|
|
518
|
+
},
|
|
519
|
+
async hostnames(opts) {
|
|
520
|
+
const qs = opts?.limit ? `?limit=${opts.limit}` : "";
|
|
521
|
+
const res = await fetch(`${root}/api/analytics/hostnames${qs}`, {
|
|
522
|
+
headers,
|
|
523
|
+
});
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
throw new Error(`Apex analytics.hostnames failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
526
|
+
}
|
|
527
|
+
const json = (await res.json());
|
|
528
|
+
return json.data ?? [];
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
async signEndUserToken(params) {
|
|
532
|
+
const res = await fetch(`${root}/api/v1/tokens/end-user`, {
|
|
533
|
+
method: "POST",
|
|
534
|
+
headers,
|
|
535
|
+
body: JSON.stringify(params),
|
|
536
|
+
});
|
|
537
|
+
if (!res.ok) {
|
|
538
|
+
throw new Error(`Apex signEndUserToken failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
539
|
+
}
|
|
540
|
+
const json = (await res.json());
|
|
541
|
+
return json.data;
|
|
542
|
+
},
|
|
151
543
|
};
|
|
152
544
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MWO-009 — Apex PLG Event Catalog.
|
|
3
|
+
*
|
|
4
|
+
* Curated, typed event schemas for the eight most common PLG
|
|
5
|
+
* lifecycle moments. Customers import these interfaces directly so
|
|
6
|
+
* their `apex.track()` calls type-check end-to-end, and the Apex
|
|
7
|
+
* dashboard uses them to derive activation / expansion / retention
|
|
8
|
+
* rollups without bespoke queries per customer.
|
|
9
|
+
*
|
|
10
|
+
* import { EVENTS, type ActivatedEvent } from "@apex-inc/sdk/plg-events";
|
|
11
|
+
* apex.track(EVENTS.ACTIVATED, { daysToActivation: 2 } satisfies ActivatedEvent);
|
|
12
|
+
*
|
|
13
|
+
* Adding a new event:
|
|
14
|
+
* 1. Add its name to `EVENTS`.
|
|
15
|
+
* 2. Add its typed interface below.
|
|
16
|
+
* 3. Update the doc page at /docs/plg-event-catalog (Phase 10).
|
|
17
|
+
*
|
|
18
|
+
* Every event is optional. There's no requirement that a customer
|
|
19
|
+
* emit any of these — the catalog is a convention, not a gate.
|
|
20
|
+
* Archived event definitions (Phase 8 follow-up) will stop deriving
|
|
21
|
+
* new rollups but preserve their historical data.
|
|
22
|
+
*/
|
|
23
|
+
export const EVENTS = {
|
|
24
|
+
SIGNUP: "signup",
|
|
25
|
+
ACTIVATED: "activated",
|
|
26
|
+
FEATURE_FIRST_USE: "feature_first_use",
|
|
27
|
+
AHA_MOMENT: "aha_moment",
|
|
28
|
+
EXPANSION: "expansion",
|
|
29
|
+
CONTRACTION: "contraction",
|
|
30
|
+
REACTIVATION: "reactivation",
|
|
31
|
+
CHURN: "churn",
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Typed `apex.track` wrapper specific to PLG events. Not a hard gate —
|
|
35
|
+
* `apex.track()` still accepts any event name — but gives customers
|
|
36
|
+
* compile-time safety on the curated set.
|
|
37
|
+
*/
|
|
38
|
+
export function trackPlgEvent(_apex, name, payload) {
|
|
39
|
+
_apex.track(name, payload);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read-only exhaustive list of the catalog. Handy for UI surfaces
|
|
43
|
+
* that need to render "all PLG events we know about."
|
|
44
|
+
*/
|
|
45
|
+
export const ALL_PLG_EVENTS = Object.values(EVENTS);
|