@apex-inc/sdk 0.1.1 → 0.5.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 +68 -0
- package/dist/esm/index.js +165 -2
- package/dist/esm/management.js +392 -0
- package/dist/esm/plg-events.js +45 -0
- package/dist/index.d.ts +242 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -3
- package/dist/index.js.map +1 -1
- package/dist/management.d.ts +283 -0
- package/dist/management.d.ts.map +1 -1
- package/dist/management.js +392 -0
- 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 +216 -0
- package/skills/apex-growth-tracking/SKILL.md +92 -1
- package/skills/apex-integration-cookbook/SKILL.md +140 -25
- package/skills/apex-partner-network/SKILL.md +191 -0
package/README.md
CHANGED
|
@@ -30,6 +30,74 @@ identify("user_123", { email: "jane@example.com" });
|
|
|
30
30
|
track("page_viewed", { path: "/pricing" });
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## PLG primitives
|
|
34
|
+
|
|
35
|
+
In addition to `track` / `identify` the SDK exposes three PLG-specific
|
|
36
|
+
primitives. Use them when you want Apex to derive activation,
|
|
37
|
+
expansion, and retention rollups automatically:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { init, identify, group, feature, track } from "@apex-inc/sdk";
|
|
41
|
+
import { EVENTS, type ActivatedEvent } from "@apex-inc/sdk/plg-events";
|
|
42
|
+
|
|
43
|
+
init({ projectKey: "your-project-key", apiUrl: "https://app.apex.inc" });
|
|
44
|
+
|
|
45
|
+
// Identify the authenticated user.
|
|
46
|
+
identify("chris@acme.com", { visitorId: "apex_vid_from_cookie" });
|
|
47
|
+
|
|
48
|
+
// Group them into their account / team (B2B).
|
|
49
|
+
group("acct_acme", { plan: "growth", arr: 120_000 });
|
|
50
|
+
|
|
51
|
+
// Track feature usage. Include experiment variant if you have one.
|
|
52
|
+
feature("cohort-builder", { variant: "new-editor", enabled: true });
|
|
53
|
+
|
|
54
|
+
// Emit curated PLG lifecycle events with compile-time-typed payloads.
|
|
55
|
+
track(EVENTS.ACTIVATED, {
|
|
56
|
+
daysToActivation: 3,
|
|
57
|
+
milestone: "first-experiment-shipped",
|
|
58
|
+
} satisfies ActivatedEvent);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The PLG catalog ships eight curated events: `signup`, `activated`,
|
|
62
|
+
`feature_first_use`, `aha_moment`, `expansion`, `contraction`,
|
|
63
|
+
`reactivation`, `churn`. Each has a typed interface so your IDE
|
|
64
|
+
catches missing fields at build time. See the [PLG event catalog
|
|
65
|
+
docs](https://app.apex.inc/docs/plg-event-catalog) for the full
|
|
66
|
+
schema.
|
|
67
|
+
|
|
68
|
+
## Server Events API (server-to-server events)
|
|
69
|
+
|
|
70
|
+
For backend workflows that don't have a browser session — Stripe webhooks,
|
|
71
|
+
CRM events, scheduled jobs — use `sendServerEvent` / `sendServerEvents`.
|
|
72
|
+
These hit the `/api/v1/events` endpoint with API-key auth and
|
|
73
|
+
idempotency-key support.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { sendServerEvent, newIdempotencyKey } from "@apex-inc/sdk";
|
|
77
|
+
|
|
78
|
+
await sendServerEvent(
|
|
79
|
+
{ apiKey: process.env.APEX_API_KEY! }, // apex_sk_…
|
|
80
|
+
{
|
|
81
|
+
type: "purchase_completed",
|
|
82
|
+
email: customer.email,
|
|
83
|
+
data: { value: invoice.amount_paid / 100, currency: "USD", order_id: invoice.id },
|
|
84
|
+
},
|
|
85
|
+
{ idempotencyKey: stripeEvent.id }, // or newIdempotencyKey()
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Batches of up to 100 events go through `sendServerEvents`. Use the same
|
|
90
|
+
visitorId / email you'd use in the browser snippet so cross-channel
|
|
91
|
+
stitching just works.
|
|
92
|
+
|
|
93
|
+
## Multi-workspace orgs
|
|
94
|
+
|
|
95
|
+
If your organization runs more than one workspace (e.g. a SaaS product
|
|
96
|
+
plus a partner marketplace), each workspace has its own `projectKey`.
|
|
97
|
+
Initialize one SDK instance per workspace; event identity stitches at
|
|
98
|
+
the org level via `OrgPerson` so a single human tracked in both shows
|
|
99
|
+
up as one person in cross-workspace cohorts and reports.
|
|
100
|
+
|
|
33
101
|
## License
|
|
34
102
|
|
|
35
103
|
MIT — see [LICENSE](./LICENSE)
|
package/dist/esm/index.js
CHANGED
|
@@ -17,6 +17,11 @@ async function sendBatch(events) {
|
|
|
17
17
|
projectKey: config.projectKey,
|
|
18
18
|
userId,
|
|
19
19
|
events,
|
|
20
|
+
// MMP-070 — propagate the SDK-level environment override (when
|
|
21
|
+
// set by `apex.init({ environment })`). The server prefers this
|
|
22
|
+
// over auto-detection from the Referer/Origin host. Mobile SDKs
|
|
23
|
+
// don't send this field; they set `mobile.releaseChannel` instead.
|
|
24
|
+
...(config.environment ? { webEnvironment: config.environment } : {}),
|
|
20
25
|
});
|
|
21
26
|
try {
|
|
22
27
|
const fetchFn = typeof fetch !== "undefined" ? fetch : await loadNodeFetch();
|
|
@@ -89,6 +94,9 @@ export function track(event, properties) {
|
|
|
89
94
|
* Identify a user with traits (email, name, plan, etc.).
|
|
90
95
|
* Also performs identity stitching if the userId looks like an email.
|
|
91
96
|
*
|
|
97
|
+
* Prefer canonical snake_case trait names (see `CanonicalUserTraits`) so
|
|
98
|
+
* templates resolve them cleanly.
|
|
99
|
+
*
|
|
92
100
|
* Pass `traits.visitorId` (the browser's `apex_vid` cookie value) to stitch
|
|
93
101
|
* server-side identify calls to the real browser session. Without it, the SDK
|
|
94
102
|
* creates a synthetic visitor ID that is disconnected from marketing touchpoints.
|
|
@@ -111,10 +119,16 @@ export function identify(userId_, traits) {
|
|
|
111
119
|
const email = userId_.includes("@") ? userId_ : traits?.email;
|
|
112
120
|
if (email) {
|
|
113
121
|
const browserVisitorId = traits?.visitorId;
|
|
114
|
-
|
|
122
|
+
// Forward the caller's stable user id as `userId` whenever it is NOT
|
|
123
|
+
// itself an email — this becomes the highest-precedence
|
|
124
|
+
// `internal_user_id` identifier server-side, which is what makes
|
|
125
|
+
// anonymous→known reconciliation deterministic. Always identify with
|
|
126
|
+
// your own user id (e.g. Cognito sub / DB id), not just an email.
|
|
127
|
+
const stableUserId = userId_.includes("@") ? undefined : userId_;
|
|
128
|
+
stitchIdentity(email, browserVisitorId, traits, stableUserId);
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
|
-
async function stitchIdentity(email, browserVisitorId, metadata) {
|
|
131
|
+
async function stitchIdentity(email, browserVisitorId, metadata, stableUserId) {
|
|
118
132
|
if (!config)
|
|
119
133
|
return;
|
|
120
134
|
const apiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
@@ -129,6 +143,7 @@ async function stitchIdentity(email, browserVisitorId, metadata) {
|
|
|
129
143
|
visitorId: effectiveVisitorId,
|
|
130
144
|
email,
|
|
131
145
|
projectKey: config.projectKey,
|
|
146
|
+
...(stableUserId ? { userId: stableUserId } : {}),
|
|
132
147
|
metadata: metadata ?? {},
|
|
133
148
|
}),
|
|
134
149
|
});
|
|
@@ -138,6 +153,56 @@ async function stitchIdentity(email, browserVisitorId, metadata) {
|
|
|
138
153
|
log("Identity stitch failed:", err);
|
|
139
154
|
}
|
|
140
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* MWO-009 — Group a user into an account / company / team so B2B
|
|
158
|
+
* PQA rollups can be derived. Calling `group("acme-inc")` stamps
|
|
159
|
+
* every subsequent `track`/`feature` event with `groupId`.
|
|
160
|
+
*/
|
|
161
|
+
let groupId = null;
|
|
162
|
+
export function group(groupId_, traits) {
|
|
163
|
+
if (!config) {
|
|
164
|
+
console.warn("[apex-sdk] Call init() before group()");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
groupId = groupId_;
|
|
168
|
+
log("Group:", groupId_, traits);
|
|
169
|
+
enqueue({
|
|
170
|
+
type: "group",
|
|
171
|
+
payload: { groupId: groupId_, traits },
|
|
172
|
+
timestamp: new Date().toISOString(),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* MWO-009 — Feature usage. Fire from your app whenever a user
|
|
177
|
+
* touches a named feature. Variant + enabled support wiring to
|
|
178
|
+
* Apex experiment payloads — if Phase 8's experiment engine served
|
|
179
|
+
* a variant, pass it through here to close the loop between
|
|
180
|
+
* assignment and adoption.
|
|
181
|
+
*/
|
|
182
|
+
export function feature(featureKey, opts = {}) {
|
|
183
|
+
if (!config) {
|
|
184
|
+
console.warn("[apex-sdk] Call init() before feature()");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
log("Feature:", featureKey, opts);
|
|
188
|
+
enqueue({
|
|
189
|
+
type: "feature",
|
|
190
|
+
payload: {
|
|
191
|
+
featureKey,
|
|
192
|
+
enabled: opts.enabled,
|
|
193
|
+
variant: opts.variant,
|
|
194
|
+
properties: opts.properties,
|
|
195
|
+
},
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the most recently set groupId (if any). Callers can use
|
|
201
|
+
* this to decorate their own payloads or assert in tests.
|
|
202
|
+
*/
|
|
203
|
+
export function currentGroupId() {
|
|
204
|
+
return groupId;
|
|
205
|
+
}
|
|
141
206
|
/**
|
|
142
207
|
* Track a form submission event with the email for identity stitching.
|
|
143
208
|
*/
|
|
@@ -165,6 +230,34 @@ export function flush() {
|
|
|
165
230
|
const batch = queue.splice(0, queue.length);
|
|
166
231
|
sendBatch(batch);
|
|
167
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Helpers grouped under `trackCart.*` so call sites read naturally
|
|
235
|
+
* (`apex.trackCart.add({...})`). The implementation just delegates to
|
|
236
|
+
* `track()` with the canonical event names.
|
|
237
|
+
*/
|
|
238
|
+
export const trackCart = {
|
|
239
|
+
/** Fired when a user adds an item to their cart. */
|
|
240
|
+
add(line) {
|
|
241
|
+
track("add_to_cart", { ...line });
|
|
242
|
+
},
|
|
243
|
+
/** Fired when a user removes an item from their cart. */
|
|
244
|
+
remove(opts) {
|
|
245
|
+
track("remove_from_cart", { ...opts });
|
|
246
|
+
},
|
|
247
|
+
/**
|
|
248
|
+
* Full-cart reconciliation snapshot. Use whenever the canonical
|
|
249
|
+
* cart state is loaded (login restore, app foreground, cart screen
|
|
250
|
+
* mount). The server-side rollup replaces all stored lines wholesale
|
|
251
|
+
* — this is the "source of truth" event when deltas drift.
|
|
252
|
+
*/
|
|
253
|
+
snapshot(opts) {
|
|
254
|
+
track("cart_snapshot", { ...opts });
|
|
255
|
+
},
|
|
256
|
+
/** Fired on checkout success; clears the rolled-up cart. */
|
|
257
|
+
checkout(opts) {
|
|
258
|
+
track("checkout_completed", { ...opts });
|
|
259
|
+
},
|
|
260
|
+
};
|
|
168
261
|
/**
|
|
169
262
|
* Shut down the SDK — flush remaining events and stop the timer.
|
|
170
263
|
*/
|
|
@@ -178,3 +271,73 @@ export function shutdown() {
|
|
|
178
271
|
log("Shut down");
|
|
179
272
|
}
|
|
180
273
|
export { createManagementClient, } from "./management";
|
|
274
|
+
/**
|
|
275
|
+
* Generate a stable idempotency key. Prefers `crypto.randomUUID()`
|
|
276
|
+
* when available (Node 19+, modern browsers). Falls back to a
|
|
277
|
+
* timestamp + random suffix that's good enough to avoid collisions
|
|
278
|
+
* within a single process.
|
|
279
|
+
*/
|
|
280
|
+
export function newIdempotencyKey() {
|
|
281
|
+
const c = globalThis.crypto;
|
|
282
|
+
if (c?.randomUUID)
|
|
283
|
+
return c.randomUUID();
|
|
284
|
+
return `idk_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Send a single event server-to-server. Useful for Stripe webhooks,
|
|
288
|
+
* CRM-driven conversions, and any backend signal that should land in
|
|
289
|
+
* Apex without going through the browser snippet.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* await sendServerEvent(
|
|
293
|
+
* { apiKey: process.env.APEX_API_KEY! },
|
|
294
|
+
* {
|
|
295
|
+
* type: "purchase_completed",
|
|
296
|
+
* email: "user@example.com",
|
|
297
|
+
* data: { value: 99, currency: "USD", order_id: "order_42" },
|
|
298
|
+
* },
|
|
299
|
+
* { idempotencyKey: stripeEvent.id }
|
|
300
|
+
* );
|
|
301
|
+
*/
|
|
302
|
+
export async function sendServerEvent(config, event, options = {}) {
|
|
303
|
+
return sendServerEvents(config, [event], options);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Send up to 100 events server-to-server in one request. Larger
|
|
307
|
+
* batches must be sharded by the caller (the API returns 400
|
|
308
|
+
* `batch_too_large` otherwise).
|
|
309
|
+
*/
|
|
310
|
+
export async function sendServerEvents(config, events, options = {}) {
|
|
311
|
+
if (!config.apiKey) {
|
|
312
|
+
throw new Error("apex-sdk: sendServerEvent requires apiKey");
|
|
313
|
+
}
|
|
314
|
+
if (events.length === 0) {
|
|
315
|
+
throw new Error("apex-sdk: sendServerEvent requires at least one event");
|
|
316
|
+
}
|
|
317
|
+
if (events.length > 100) {
|
|
318
|
+
throw new Error("apex-sdk: sendServerEvents batch_too_large — max 100 events; shard yourself");
|
|
319
|
+
}
|
|
320
|
+
const apiUrl = (config.apiUrl ?? "https://app.apex.inc").replace(/\/$/, "");
|
|
321
|
+
const url = `${apiUrl}/api/v1/events`;
|
|
322
|
+
const idempotencyKey = options.idempotencyKey ?? newIdempotencyKey();
|
|
323
|
+
const headers = {
|
|
324
|
+
"content-type": "application/json",
|
|
325
|
+
"x-api-key": config.apiKey,
|
|
326
|
+
"idempotency-key": idempotencyKey,
|
|
327
|
+
};
|
|
328
|
+
if (config.projectKey)
|
|
329
|
+
headers["x-apex-project"] = config.projectKey;
|
|
330
|
+
const fetchFn = typeof fetch !== "undefined" ? fetch : await loadNodeFetch();
|
|
331
|
+
const res = await fetchFn(url, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers,
|
|
334
|
+
body: JSON.stringify({ events }),
|
|
335
|
+
});
|
|
336
|
+
const json = (await res.json().catch(() => ({})));
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
const code = json.error?.code ?? `http_${res.status}`;
|
|
339
|
+
const message = json.error?.message ?? `HTTP ${res.status}`;
|
|
340
|
+
throw new Error(`apex-sdk: sendServerEvent failed (${code}): ${message}`);
|
|
341
|
+
}
|
|
342
|
+
return json;
|
|
343
|
+
}
|