@active-reach/web-sdk 1.13.0 → 1.14.1

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.
@@ -56,7 +56,7 @@ function generateMessageId() {
56
56
  class Logger {
57
57
  constructor() {
58
58
  this.enabled = false;
59
- this.prefix = "[Aegis SDK]";
59
+ this.prefix = "[Active Reach SDK]";
60
60
  }
61
61
  enable() {
62
62
  this.enabled = true;
@@ -216,6 +216,7 @@ class SessionManager {
216
216
  this.sessionStartTime = Date.now();
217
217
  this.lastActivityTime = Date.now();
218
218
  this.eventCount = 0;
219
+ this.sessionId = newSessionId;
219
220
  this.persistSession();
220
221
  logger.info("New session created:", newSessionId);
221
222
  return newSessionId;
@@ -824,7 +825,7 @@ class Storage {
824
825
  };
825
826
  localStorage.setItem(fullKey, JSON.stringify(item));
826
827
  } catch (error) {
827
- console.warn("[Aegis Storage] localStorage.setItem failed:", error);
828
+ console.warn("[Active Reach Storage] localStorage.setItem failed:", error);
828
829
  }
829
830
  }
830
831
  this.setCookie(fullKey, value, expiryDays);
@@ -843,7 +844,7 @@ class Storage {
843
844
  }
844
845
  }
845
846
  } catch (error) {
846
- console.warn("[Aegis Storage] localStorage.getItem failed:", error);
847
+ console.warn("[Active Reach Storage] localStorage.getItem failed:", error);
847
848
  }
848
849
  }
849
850
  return this.getCookie(fullKey);
@@ -854,7 +855,7 @@ class Storage {
854
855
  try {
855
856
  localStorage.removeItem(fullKey);
856
857
  } catch (error) {
857
- console.warn("[Aegis Storage] localStorage.removeItem failed:", error);
858
+ console.warn("[Active Reach Storage] localStorage.removeItem failed:", error);
858
859
  }
859
860
  }
860
861
  this.deleteCookie(fullKey);
@@ -869,7 +870,7 @@ class Storage {
869
870
  }
870
871
  });
871
872
  } catch (error) {
872
- console.warn("[Aegis Storage] localStorage.clear failed:", error);
873
+ console.warn("[Active Reach Storage] localStorage.clear failed:", error);
873
874
  }
874
875
  }
875
876
  const cookies = document.cookie.split(";");
@@ -900,7 +901,7 @@ class Storage {
900
901
  }
901
902
  document.cookie = cookieStr;
902
903
  } catch (error) {
903
- console.warn("[Aegis Storage] setCookie failed:", error);
904
+ console.warn("[Active Reach Storage] setCookie failed:", error);
904
905
  }
905
906
  }
906
907
  getCookie(name) {
@@ -917,7 +918,7 @@ class Storage {
917
918
  }
918
919
  }
919
920
  } catch (error) {
920
- console.warn("[Aegis Storage] getCookie failed:", error);
921
+ console.warn("[Active Reach Storage] getCookie failed:", error);
921
922
  }
922
923
  return null;
923
924
  }
@@ -934,7 +935,7 @@ class Storage {
934
935
  }
935
936
  document.cookie = cookieStr;
936
937
  } catch (error) {
937
- console.warn("[Aegis Storage] deleteCookie failed:", error);
938
+ console.warn("[Active Reach Storage] deleteCookie failed:", error);
938
939
  }
939
940
  }
940
941
  getRootDomain() {
@@ -952,7 +953,7 @@ class Storage {
952
953
  }
953
954
  return `.${parts.slice(-2).join(".")}`;
954
955
  } catch (error) {
955
- console.warn("[Aegis Storage] getRootDomain failed:", error);
956
+ console.warn("[Active Reach Storage] getRootDomain failed:", error);
956
957
  return null;
957
958
  }
958
959
  }
@@ -977,7 +978,7 @@ function parseUTMParameters(url) {
977
978
  });
978
979
  return utm;
979
980
  } catch (error) {
980
- console.warn("[Aegis URL Parser] parseUTMParameters failed:", error);
981
+ console.warn("[Active Reach URL Parser] parseUTMParameters failed:", error);
981
982
  return {};
982
983
  }
983
984
  }
@@ -1001,7 +1002,7 @@ function parseAdClickIDs(url) {
1001
1002
  });
1002
1003
  return adClickIDs;
1003
1004
  } catch (error) {
1004
- console.warn("[Aegis URL Parser] parseAdClickIDs failed:", error);
1005
+ console.warn("[Active Reach URL Parser] parseAdClickIDs failed:", error);
1005
1006
  return {};
1006
1007
  }
1007
1008
  }
@@ -1130,7 +1131,7 @@ function detectNetworkInfo() {
1130
1131
  };
1131
1132
  }
1132
1133
  } catch (error) {
1133
- console.warn("[Aegis Device] detectNetworkInfo failed:", error);
1134
+ console.warn("[Active Reach Device] detectNetworkInfo failed:", error);
1134
1135
  }
1135
1136
  return void 0;
1136
1137
  }
@@ -1764,6 +1765,24 @@ class EcommerceTracker {
1764
1765
  variant_id: wishlist.product.variant_id
1765
1766
  });
1766
1767
  }
1768
+ // -- Back-in-stock waitlist --
1769
+ //
1770
+ // Server-side substrate: contact_events row keyed on
1771
+ // (organization_id, contact_id, event_name='product_waitlisted',
1772
+ // event_properties['product_id']). Resolved by
1773
+ // product_event_trigger_service._get_waitlisted_contacts and fanned out via
1774
+ // catalog.back_in_stock journey trigger when stock_event_handler_worker
1775
+ // detects the SKU flipping back into stock.
1776
+ productWaitlisted(waitlist) {
1777
+ this.aegis.track("product_waitlisted", {
1778
+ product_id: waitlist.product.product_id,
1779
+ sku: waitlist.product.sku ?? waitlist.product.product_id,
1780
+ variant_id: waitlist.product.variant_id,
1781
+ name: waitlist.product.name,
1782
+ price: waitlist.product.price,
1783
+ channels: waitlist.channels
1784
+ });
1785
+ }
1767
1786
  // -- Promotions --
1768
1787
  promotionViewed(promo) {
1769
1788
  this.aegis.track("promotion_viewed", { ...promo });
@@ -1837,7 +1856,7 @@ class RateLimiter {
1837
1856
  try {
1838
1857
  this.onDropBatch(this.droppedThisWindow, this.firstDroppedName);
1839
1858
  } catch (err) {
1840
- logger.warn("[Aegis] RateLimiter.onDropBatch threw:", err);
1859
+ logger.warn("[Active Reach] RateLimiter.onDropBatch threw:", err);
1841
1860
  }
1842
1861
  }
1843
1862
  this.droppedThisWindow = 0;
@@ -2209,7 +2228,7 @@ const _TraitGovernor = class _TraitGovernor {
2209
2228
  if (count >= _TraitGovernor.WARN_CAP) return;
2210
2229
  perVerdict.set(drop.verdict, count + 1);
2211
2230
  warnOnConsole(
2212
- `[Aegis SDK] trait ${drop.verdict}: ${drop.reason} (original key: ${JSON.stringify(drop.originalKey)}). Backend will reject; fix the SDK call to silence this warning.`
2231
+ `[Active Reach SDK] trait ${drop.verdict}: ${drop.reason} (original key: ${JSON.stringify(drop.originalKey)}). Backend will reject; fix the SDK call to silence this warning.`
2213
2232
  );
2214
2233
  }
2215
2234
  };
@@ -2362,6 +2381,9 @@ class Aegis {
2362
2381
  this._originalPushState = null;
2363
2382
  this._originalReplaceState = null;
2364
2383
  this._lastEventIds = /* @__PURE__ */ new Map();
2384
+ this._workspaceCodes = /* @__PURE__ */ new Set();
2385
+ this._runtimeWorkspace = null;
2386
+ this._runtimeLocation = null;
2365
2387
  }
2366
2388
  async init(writeKey, config) {
2367
2389
  if (this.initPromise) {
@@ -2388,7 +2410,7 @@ class Aegis {
2388
2410
  if (this.config.debug) {
2389
2411
  logger.enable();
2390
2412
  }
2391
- logger.info("Initializing Aegis SDK...", this.config);
2413
+ logger.info("Initializing Active Reach SDK...", this.config);
2392
2414
  this.storage = new Storage({
2393
2415
  cookieDomain: this.config.cookie_domain,
2394
2416
  secureCookie: this.config.secure_cookie,
@@ -2448,7 +2470,7 @@ class Aegis {
2448
2470
  this.page();
2449
2471
  this.startSPATracking();
2450
2472
  }
2451
- logger.info("Aegis SDK initialized successfully");
2473
+ logger.info("Active Reach SDK initialized successfully");
2452
2474
  }
2453
2475
  /**
2454
2476
  * SPA navigation tracking — monkey-patches pushState/replaceState and
@@ -2543,6 +2565,244 @@ class Aegis {
2543
2565
  removePlugin(name) {
2544
2566
  this.plugins.unregister(name);
2545
2567
  }
2568
+ /**
2569
+ * Plugin-handshake P1 — ingest the workspace_code allowlist from the
2570
+ * bootstrap response. Pass `result.workspaceCodes` from `bootstrap()`
2571
+ * here so the SDK can do path-segment workspace detection on
2572
+ * customer-facing pages like `/south/rewards`, `/ghatkopar/feedback`.
2573
+ *
2574
+ * Empty array is the right answer for single-outlet tenants — SDK
2575
+ * will skip the path cascade and fall through to query param /
2576
+ * setWorkspace() / gateway origin lookup.
2577
+ *
2578
+ * Safe to call before init() — the set is consulted at event-fire time.
2579
+ */
2580
+ ingestWorkspaceCodes(codes) {
2581
+ this._workspaceCodes = new Set(
2582
+ (codes ?? []).filter((c) => typeof c === "string" && c.length > 0)
2583
+ );
2584
+ logger.debug("Workspace codes ingested", { count: this._workspaceCodes.size });
2585
+ }
2586
+ /**
2587
+ * Plugin-handshake P1 (Track E) — explicitly set the workspace for
2588
+ * subsequent events. Used by:
2589
+ * - SPAs where workspace context changes via in-app routing
2590
+ * - Mobile / RN apps that have no URL
2591
+ * - Custom integrations that resolve workspace from their own state
2592
+ *
2593
+ * Pass null to clear (or call clearWorkspace()).
2594
+ *
2595
+ * Persisted to sessionStorage so SPA reloads / page transitions on the
2596
+ * same outlet inherit it without re-calling.
2597
+ */
2598
+ setWorkspace(codeOrId) {
2599
+ this._runtimeWorkspace = codeOrId;
2600
+ if (typeof window !== "undefined" && window.sessionStorage) {
2601
+ try {
2602
+ if (codeOrId) {
2603
+ window.sessionStorage.setItem("aegis_runtime_ws", codeOrId);
2604
+ } else {
2605
+ window.sessionStorage.removeItem("aegis_runtime_ws");
2606
+ }
2607
+ } catch {
2608
+ }
2609
+ }
2610
+ logger.debug("Runtime workspace set", { value: codeOrId });
2611
+ }
2612
+ /** Symmetric helper to clear the runtime workspace. */
2613
+ clearWorkspace() {
2614
+ this.setWorkspace(null);
2615
+ }
2616
+ // ── P15.1 — location (outlet) facade ─────────────────────────
2617
+ //
2618
+ // Under the post-2026-05-26 canonical hierarchy:
2619
+ // workspace_id = brand-tenant (one per brand)
2620
+ // location_id = outlet within a brand
2621
+ // The two are distinct dimensions. Both can be stamped on the
2622
+ // same event. These methods mirror the workspace facade but
2623
+ // resolve the outlet specifically.
2624
+ /**
2625
+ * P15.1 — ingest the outlet-code allowlist from bootstrap. The
2626
+ * SDK consults this set when reading the URL path segment to
2627
+ * decide whether `location.pathname.split('/')[1]` is a valid
2628
+ * outlet code vs a generic route.
2629
+ *
2630
+ * Empty array = single-outlet tenant; SDK skips the path-segment
2631
+ * cascade for location resolution.
2632
+ *
2633
+ * Safe to call before init(); the set is consulted at event-
2634
+ * fire time.
2635
+ */
2636
+ ingestLocationCodes(codes) {
2637
+ this._workspaceCodes = new Set(
2638
+ (codes ?? []).filter((c) => typeof c === "string" && c.length > 0)
2639
+ );
2640
+ logger.debug("Location codes ingested", { count: this._workspaceCodes.size });
2641
+ }
2642
+ /**
2643
+ * P15.1 — explicitly set the active outlet for subsequent events.
2644
+ * Used by:
2645
+ * - Storefronts where outlet picker fires `aegis.setLocation()`
2646
+ * on selection (so events that follow carry that outlet's
2647
+ * location_id without the SDK having to re-parse the URL).
2648
+ * - SPAs where outlet context changes via in-app routing.
2649
+ * - Mobile/RN apps that have no URL.
2650
+ *
2651
+ * Pass null to clear (or call clearLocation()).
2652
+ *
2653
+ * Persisted to sessionStorage so SPA reloads + route transitions
2654
+ * on the same outlet inherit it without re-calling.
2655
+ */
2656
+ setLocation(codeOrId) {
2657
+ this._runtimeLocation = codeOrId;
2658
+ if (typeof window !== "undefined" && window.sessionStorage) {
2659
+ try {
2660
+ if (codeOrId) {
2661
+ window.sessionStorage.setItem("aegis_runtime_location", codeOrId);
2662
+ } else {
2663
+ window.sessionStorage.removeItem("aegis_runtime_location");
2664
+ }
2665
+ } catch {
2666
+ }
2667
+ }
2668
+ logger.debug("Runtime location set", { value: codeOrId });
2669
+ }
2670
+ /** Symmetric helper to clear the runtime location. */
2671
+ clearLocation() {
2672
+ this.setLocation(null);
2673
+ }
2674
+ /**
2675
+ * P15.1 — same logic as `getPathWorkspaceCode()` but expresses
2676
+ * the semantic correctly: under the new hierarchy, the first
2677
+ * path segment after the brand subdomain IS the outlet code.
2678
+ * Returns `undefined` when no outlet code matches the allowlist.
2679
+ */
2680
+ getPathLocationCode() {
2681
+ return this.getPathWorkspaceCode();
2682
+ }
2683
+ /**
2684
+ * P15.1 — resolve the effective `location_id` for the next event.
2685
+ * Independent of `getEffectiveWorkspaceId()`: both run on every
2686
+ * event so workspace_id (brand) and location_id (outlet) land
2687
+ * on the same event when both are resolvable.
2688
+ *
2689
+ * Cascade (highest precedence first):
2690
+ * 1. `this.config.location_id` — explicit operator config.
2691
+ * 2. `?location=` query param (also accepts `?ws=` for back-
2692
+ * compat with storefronts that pass outlet via the old
2693
+ * param name — the value is the same; param naming only).
2694
+ * 3. URL path segment matching the outlet-code allowlist.
2695
+ * 4. Runtime `aegis.setLocation()` (in-memory).
2696
+ * 5. sessionStorage `aegis_runtime_location`.
2697
+ * 6. `undefined` — event-ingress treats as brand-tier
2698
+ * (no outlet attribution).
2699
+ */
2700
+ getEffectiveLocationId() {
2701
+ var _a;
2702
+ const configured = (_a = this.config) == null ? void 0 : _a.location_id;
2703
+ if (configured) return configured;
2704
+ if (typeof window === "undefined") return void 0;
2705
+ try {
2706
+ const search = new URLSearchParams(window.location.search);
2707
+ const loc = search.get("location") || search.get("ws");
2708
+ if (loc && loc.length > 0) return loc;
2709
+ } catch {
2710
+ }
2711
+ const pathLoc = this.getPathLocationCode();
2712
+ if (pathLoc) return pathLoc;
2713
+ if (this._runtimeLocation) return this._runtimeLocation;
2714
+ try {
2715
+ if (window.sessionStorage) {
2716
+ const cached = window.sessionStorage.getItem("aegis_runtime_location");
2717
+ if (cached && cached.length > 0) return cached;
2718
+ }
2719
+ } catch {
2720
+ }
2721
+ return void 0;
2722
+ }
2723
+ /**
2724
+ * Inspect the URL's first path segment and return it if it's in the
2725
+ * org's workspace_code allowlist. Skips well-known non-workspace prefixes
2726
+ * (e.g. `b` for bill short codes; `s` is the storefront app slug).
2727
+ *
2728
+ * Returns undefined when:
2729
+ * - window is undefined (SSR / Node)
2730
+ * - path is empty / root
2731
+ * - first segment isn't in `_workspaceCodes` (e.g. /products, /cart)
2732
+ *
2733
+ * The returned value is a workspace CODE (slug like "south"), not a
2734
+ * UUID. The gateway normalizes code → UUID server-side via
2735
+ * workspace_subaccounts lookup with Redis cache.
2736
+ */
2737
+ getPathWorkspaceCode() {
2738
+ if (typeof window === "undefined") return void 0;
2739
+ if (this._workspaceCodes.size === 0) return void 0;
2740
+ try {
2741
+ const segments = window.location.pathname.split("/").filter(Boolean);
2742
+ if (segments.length === 0) return void 0;
2743
+ const first = segments[0].toLowerCase();
2744
+ return this._workspaceCodes.has(first) ? first : void 0;
2745
+ } catch {
2746
+ return void 0;
2747
+ }
2748
+ }
2749
+ /**
2750
+ * Resolve the effective `workspace_id` for the next event.
2751
+ *
2752
+ * Cascade (highest precedence first):
2753
+ * 1. `this.config.workspace_id` — explicit operator config (headless
2754
+ * SDK usage, server-rendered apps, native shells).
2755
+ * 2. `?ws=` query param on the current URL — the P3 storefront URL
2756
+ * contract. The outlet picker writes this via `router.replace`.
2757
+ * 3. URL path segment (P1 Track A) — `/south/rewards` → "south" when
2758
+ * "south" is in the org's workspace_code allowlist.
2759
+ * 4. `aegis.setWorkspace()` runtime override (P1 Track E) — SPAs +
2760
+ * mobile + custom integrations.
2761
+ * 5. sessionStorage `aegis_runtime_ws` — persists setWorkspace across
2762
+ * reloads/SPA route changes.
2763
+ * 6. `undefined` — gateway falls back to `resolveByOrigin` lookup
2764
+ * against the property's allowed_origins.
2765
+ *
2766
+ * The returned value can be EITHER a UUID (from config / ?ws=) or a
2767
+ * workspace CODE slug (from path / setWorkspace). The gateway
2768
+ * normalizes slug → UUID server-side; analytics layer never needs to
2769
+ * worry about the difference.
2770
+ *
2771
+ * See docs/architecture/MULTI_OUTLET_URL_STRATEGY.md §2.3 and
2772
+ * docs/architecture/PLUGIN_HANDSHAKE_AUTOMATION.md §2.
2773
+ */
2774
+ /**
2775
+ * Public so peer SDK surfaces (AegisMessageRuntime / AegisInAppManager /
2776
+ * AegisPlacementManager / AegisWidgetManager) can plumb the same
2777
+ * resolved workspace into their own gateway POSTs. Each manager calls
2778
+ * the gateway on its own endpoint (`/v1/in_app/events`,
2779
+ * `/v1/placements/track`, `/v1/widgets/track-event`); without this,
2780
+ * those events arrive at event-ingress with no workspace_id stamped
2781
+ * → impressions/clicks fall back to the org's primary workspace and
2782
+ * cross-outlet attribution breaks.
2783
+ */
2784
+ getEffectiveWorkspaceId() {
2785
+ var _a;
2786
+ const configured = (_a = this.config) == null ? void 0 : _a.workspace_id;
2787
+ if (configured) return configured;
2788
+ if (typeof window === "undefined") return void 0;
2789
+ try {
2790
+ const ws = new URLSearchParams(window.location.search).get("ws");
2791
+ if (ws && ws.length > 0) return ws;
2792
+ } catch {
2793
+ }
2794
+ const pathWs = this.getPathWorkspaceCode();
2795
+ if (pathWs) return pathWs;
2796
+ if (this._runtimeWorkspace) return this._runtimeWorkspace;
2797
+ try {
2798
+ if (window.sessionStorage) {
2799
+ const cached = window.sessionStorage.getItem("aegis_runtime_ws");
2800
+ if (cached && cached.length > 0) return cached;
2801
+ }
2802
+ } catch {
2803
+ }
2804
+ return void 0;
2805
+ }
2546
2806
  track(eventName, properties) {
2547
2807
  if (!this.assertInitialized()) return;
2548
2808
  const messageId = generateMessageId();
@@ -2555,7 +2815,8 @@ class Aegis {
2555
2815
  anonymousId: this.identity.getAnonymousId(),
2556
2816
  userId: this.identity.getUserId() || void 0,
2557
2817
  sessionId: this.session.getSessionId(),
2558
- workspace_id: this.config.workspace_id || void 0,
2818
+ workspace_id: this.getEffectiveWorkspaceId(),
2819
+ location_id: this.getEffectiveLocationId(),
2559
2820
  context: buildContext(this.config, this.session)
2560
2821
  };
2561
2822
  this._lastEventIds.set(eventName, messageId);
@@ -2563,7 +2824,7 @@ class Aegis {
2563
2824
  }
2564
2825
  identify(userId, traits) {
2565
2826
  if (!this.assertInitialized()) return;
2566
- const wsForGovernor = this.config.workspace_id || null;
2827
+ const wsForGovernor = this.getEffectiveWorkspaceId() || null;
2567
2828
  const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
2568
2829
  this.identity.setUserId(userId, governedTraits);
2569
2830
  const event = {
@@ -2574,7 +2835,8 @@ class Aegis {
2574
2835
  anonymousId: this.identity.getAnonymousId(),
2575
2836
  userId,
2576
2837
  sessionId: this.session.getSessionId(),
2577
- workspace_id: this.config.workspace_id || void 0,
2838
+ workspace_id: this.getEffectiveWorkspaceId(),
2839
+ location_id: this.getEffectiveLocationId(),
2578
2840
  context: buildContext(this.config, this.session)
2579
2841
  };
2580
2842
  this.captureEvent(event);
@@ -2590,14 +2852,15 @@ class Aegis {
2590
2852
  anonymousId: this.identity.getAnonymousId(),
2591
2853
  userId: this.identity.getUserId() || void 0,
2592
2854
  sessionId: this.session.getSessionId(),
2593
- workspace_id: this.config.workspace_id || void 0,
2855
+ workspace_id: this.getEffectiveWorkspaceId(),
2856
+ location_id: this.getEffectiveLocationId(),
2594
2857
  context: buildContext(this.config, this.session)
2595
2858
  };
2596
2859
  this.captureEvent(event);
2597
2860
  }
2598
2861
  group(groupId, traits) {
2599
2862
  if (!this.assertInitialized()) return;
2600
- const wsForGovernor = this.config.workspace_id || null;
2863
+ const wsForGovernor = this.getEffectiveWorkspaceId() || null;
2601
2864
  const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
2602
2865
  const event = {
2603
2866
  type: "group",
@@ -2647,7 +2910,8 @@ class Aegis {
2647
2910
  anonymousId: this.identity.getAnonymousId(),
2648
2911
  userId: this.identity.getUserId() || void 0,
2649
2912
  sessionId: this.session.getSessionId(),
2650
- workspace_id: this.config.workspace_id || void 0,
2913
+ workspace_id: this.getEffectiveWorkspaceId(),
2914
+ location_id: this.getEffectiveLocationId(),
2651
2915
  context: buildContext(this.config, this.session)
2652
2916
  };
2653
2917
  this.captureEvent(event);
@@ -2959,4 +3223,4 @@ export {
2959
3223
  logger as l,
2960
3224
  murmurhash3_x86_32 as m
2961
3225
  };
2962
- //# sourceMappingURL=analytics-Mh4H4ekQ.mjs.map
3226
+ //# sourceMappingURL=analytics-CjLItVo2.mjs.map