@apex-inc/sdk 0.20.1 → 0.21.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/dist/esm/index.js CHANGED
@@ -381,6 +381,22 @@ function getExperimentPreview(search) {
381
381
  const resolved = search ?? (isBrowser ? window.location.search : "");
382
382
  return readExperimentPreview(resolved);
383
383
  }
384
+ async function fetchExperimentLineIds(apiBase, experimentId, workspaceKey) {
385
+ try {
386
+ const res = await fetch(`${apiBase}/api/experiments/${experimentId}/line?source=sdk`, {
387
+ credentials: "include",
388
+ headers: workspaceKey ? { "x-apex-workspace": workspaceKey } : {},
389
+ });
390
+ if (!res.ok)
391
+ return null;
392
+ const body = (await res.json());
393
+ const ids = body.data?.lineMemberIds ?? body.lineMemberIds;
394
+ return Array.isArray(ids) && ids.length > 0 ? ids : null;
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
384
400
  // DBS-1 — durable browser session id (`apex_sid`), GA4-parity 30-min
385
401
  // inactivity rotation. Mirrors the snippet's cookie format
386
402
  // (`<id>.<startedAtMs>.<lastActivityMs>.<sessionNumber>.<utmSig>`) so a page
@@ -540,12 +556,19 @@ export async function getVariant(experimentId, opts = {}) {
540
556
  const buildId = opts.buildId ?? config?.buildId;
541
557
  // 1. Preview mode — author previewing, not a real exposure. Runs
542
558
  // before the browser/consent gates so SSR and a declined cookie
543
- // banner still show the URL arm.
559
+ // banner still show the URL arm. A family member's `_apex_exp`
560
+ // applies once we know the line.
544
561
  const preview = getExperimentPreview(opts.search);
545
- if (preview && (!preview.expId || preview.expId === experimentId)) {
546
- return preview.variant;
547
- }
548
- if (preview?.expId && preview.expId !== experimentId) {
562
+ if (preview) {
563
+ const lineIds = opts.lineMemberIds ??
564
+ (isBrowser && preview.expId && preview.expId !== experimentId
565
+ ? await fetchExperimentLineIds(apiBase, experimentId, workspaceKey)
566
+ : null);
567
+ if (!preview.expId ||
568
+ preview.expId === experimentId ||
569
+ (lineIds && lineIds.includes(experimentId) && lineIds.includes(preview.expId))) {
570
+ return preview.variant;
571
+ }
549
572
  return "control";
550
573
  }
551
574
  if (!isBrowser)
@@ -574,18 +597,22 @@ export async function getVariant(experimentId, opts = {}) {
574
597
  if (!res.ok)
575
598
  throw new Error(`${res.status}`);
576
599
  const data = (await res.json());
600
+ const runId = data.experimentId || experimentId;
577
601
  if (data.eligible === false) {
578
602
  // Staging / TestFlight / localhost: serve the arm so they can QA.
579
603
  // Do not fire an exposure — that is what would move the result.
580
604
  if (data.reason === "non_production_audience") {
581
- localStorage.setItem(cacheKey, data.variant);
605
+ localStorage.setItem(`apex_var_${runId}`, data.variant);
582
606
  return data.variant;
583
607
  }
584
608
  localStorage.setItem(cacheKey, "control");
585
609
  return "control";
586
610
  }
587
- localStorage.setItem(cacheKey, data.variant);
588
- fireExperimentExposure(experimentId, data.variant, apiBase, workspaceKey, buildId);
611
+ if (runId !== experimentId) {
612
+ localStorage.removeItem(cacheKey);
613
+ }
614
+ localStorage.setItem(`apex_var_${runId}`, data.variant);
615
+ fireExperimentExposure(runId, data.variant, apiBase, workspaceKey, buildId);
589
616
  return data.variant;
590
617
  }
591
618
  catch {
@@ -344,59 +344,45 @@ export function createManagementClient(cfg) {
344
344
  return json.data.milestone;
345
345
  },
346
346
  /**
347
- * Create a milestone AND scaffold its journey(s) in one call. This is the
348
- * programmatic mirror of the dashboard sentence on-ramp.
349
- *
350
- * `scaffold` selects which legs to create:
351
- * - `"both"` (recommended) — the nudge (audience_entry over "hasn't
352
- * adopted within N days") AND the celebration (fires once on
353
- * adoption). This is what the dashboard on-ramp produces.
354
- * - `"celebration"` (default) — celebration leg only.
355
- * - `"nudge"` — nudge leg only.
356
- * - `"none"` — milestone only, no journey.
357
- *
358
- * `journeyName` applies only to single-leg scaffolds; for `"both"` each
359
- * leg takes its template's default (deduped server-side). Every
360
- * scaffolded journey is a draft — publish it from the canvas.
347
+ * Create a Milestone AND stamp one Adaptive Journey (Trigger → Wait
348
+ * until they finish → Celebration and/or Nudge). Draft only — publish
349
+ * from the canvas. `scaffold: "none"` is just-track (no journey).
361
350
  */
362
351
  async compose(params) {
363
- const { scaffold = "celebration", journeyName, ...createParams } = params;
352
+ const { scaffold = "both", journeyName, nudgeClocks, ...createParams } = params;
364
353
  const milestone = await this.create(createParams);
365
354
  if (scaffold === "none") {
366
- return { milestone, journeyId: null, nudgeJourneyId: null, celebrationJourneyId: null };
355
+ return {
356
+ milestone,
357
+ journeyId: null,
358
+ nudgeJourneyId: null,
359
+ celebrationJourneyId: null,
360
+ sends: [],
361
+ };
367
362
  }
368
- const scaffoldLeg = async (templateId, name) => {
369
- const res = await fetch(`${root}/api/journeys/from-template`, {
370
- method: "POST",
371
- headers,
372
- body: JSON.stringify({
373
- templateId,
374
- milestoneId: milestone.id,
375
- ...(name ? { name } : {}),
376
- }),
377
- });
378
- if (!res.ok)
379
- throw new Error(`Apex adoption.compose (journey scaffold) failed (${res.status}): ${await res.text().catch(() => "")}`);
380
- const journey = (await res.json());
381
- return journey.id ?? null;
382
- };
383
- const wantsNudge = scaffold === "nudge" || scaffold === "both";
384
- const wantsCelebration = scaffold === "celebration" || scaffold === "both";
385
- // Single-leg scaffolds honor journeyName; "both" lets each leg default.
386
- const legName = scaffold === "both" ? undefined : journeyName;
387
- const nudgeJourneyId = wantsNudge ? await scaffoldLeg("adoption-nudge", legName) : null;
388
- const celebrationJourneyId = wantsCelebration
389
- ? await scaffoldLeg("adoption-celebration", legName)
390
- : null;
391
- // Re-read so the returned milestone carries the nudge/celebration
392
- // back-references the scaffolds just stamped.
363
+ const res = await fetch(`${root}/api/journeys/from-template`, {
364
+ method: "POST",
365
+ headers,
366
+ body: JSON.stringify({
367
+ templateId: "adoption-nudge",
368
+ milestoneId: milestone.id,
369
+ scaffold,
370
+ name: journeyName ?? milestone.name,
371
+ ...(nudgeClocks ? { nudgeClocks } : {}),
372
+ }),
373
+ });
374
+ if (!res.ok) {
375
+ throw new Error(`Apex adoption.compose (journey scaffold) failed (${res.status}): ${await res.text().catch(() => "")}`);
376
+ }
377
+ const journey = (await res.json());
378
+ const journeyId = journey.id ?? null;
393
379
  const refreshed = (await this.get(milestone.id)) ?? milestone;
394
380
  return {
395
381
  milestone: refreshed,
396
- // Primary id: the nudge when it was scaffolded, else the celebration.
397
- journeyId: nudgeJourneyId ?? celebrationJourneyId,
398
- nudgeJourneyId,
399
- celebrationJourneyId,
382
+ journeyId,
383
+ nudgeJourneyId: journeyId,
384
+ celebrationJourneyId: null,
385
+ sends: journey.sends ?? [],
400
386
  };
401
387
  },
402
388
  /** Patch a milestone (name, priority, active, gapPolicy, adoptedWhen, ...). */