@active-reach/web-sdk 1.3.0 → 1.4.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.
@@ -1761,6 +1761,191 @@ class RateLimiter {
1761
1761
  this.firstDroppedName = null;
1762
1762
  }
1763
1763
  }
1764
+ const C1 = 3432918353;
1765
+ const C2 = 461845907;
1766
+ function murmurhash3_x86_32(input, seed = 0) {
1767
+ const bytes = new TextEncoder().encode(input);
1768
+ return murmurhash3_bytes(bytes, seed);
1769
+ }
1770
+ function murmurhash3_bytes(bytes, seed = 0) {
1771
+ const len = bytes.length;
1772
+ const nBlocks = Math.floor(len / 4);
1773
+ let h1 = seed >>> 0;
1774
+ for (let i = 0; i < nBlocks; i++) {
1775
+ const offset = i * 4;
1776
+ let k12 = bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16 | bytes[offset + 3] << 24;
1777
+ k12 = Math.imul(k12, C1);
1778
+ k12 = k12 << 15 | k12 >>> 17;
1779
+ k12 = Math.imul(k12, C2);
1780
+ h1 ^= k12;
1781
+ h1 = h1 << 13 | h1 >>> 19;
1782
+ h1 = Math.imul(h1, 5) + 3864292196 >>> 0;
1783
+ }
1784
+ const tailStart = nBlocks * 4;
1785
+ let k1 = 0;
1786
+ const tailLen = len - tailStart;
1787
+ if (tailLen === 3) k1 ^= bytes[tailStart + 2] << 16;
1788
+ if (tailLen >= 2) k1 ^= bytes[tailStart + 1] << 8;
1789
+ if (tailLen >= 1) {
1790
+ k1 ^= bytes[tailStart];
1791
+ k1 = Math.imul(k1, C1);
1792
+ k1 = k1 << 15 | k1 >>> 17;
1793
+ k1 = Math.imul(k1, C2);
1794
+ h1 ^= k1;
1795
+ }
1796
+ h1 ^= len;
1797
+ h1 ^= h1 >>> 16;
1798
+ h1 = Math.imul(h1, 2246822507);
1799
+ h1 ^= h1 >>> 13;
1800
+ h1 = Math.imul(h1, 3266489909);
1801
+ h1 ^= h1 >>> 16;
1802
+ return h1 >>> 0;
1803
+ }
1804
+ function base64ToBytes(b64) {
1805
+ if (typeof atob !== "undefined") {
1806
+ const bin = atob(b64);
1807
+ const out = new Uint8Array(bin.length);
1808
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1809
+ return out;
1810
+ }
1811
+ const B = globalThis.Buffer;
1812
+ if (B && typeof B.from === "function") {
1813
+ const buf = B.from(b64, "base64");
1814
+ return new Uint8Array(buf);
1815
+ }
1816
+ throw new Error("No base64 decoder available (neither atob nor Buffer)");
1817
+ }
1818
+ class BloomFilter {
1819
+ constructor(buf, params) {
1820
+ this.params = params;
1821
+ if ((params.m & params.m - 1) !== 0) {
1822
+ throw new Error(`Bloom filter m must be a power of 2, got ${params.m}`);
1823
+ }
1824
+ if (buf.length !== params.m >> 3) {
1825
+ throw new Error(
1826
+ `Bloom filter buffer size mismatch: expected ${params.m >> 3} bytes, got ${buf.length}`
1827
+ );
1828
+ }
1829
+ this.buf = buf;
1830
+ this.mask = params.m - 1;
1831
+ }
1832
+ /** Build from the wire format (base64 bytes + explicit params). */
1833
+ static fromBase64(bloomB64, params) {
1834
+ const bytes = base64ToBytes(bloomB64);
1835
+ return new BloomFilter(bytes, params);
1836
+ }
1837
+ /**
1838
+ * Returns true if `name` is probably in the set — possibly with the
1839
+ * filter's configured false-positive rate. FALSE is always authoritative.
1840
+ *
1841
+ * FP here means: SDK thinks a name is already registered when it isn't.
1842
+ * That costs one wasted server round-trip (gateway does the exact check
1843
+ * and catches it) — strictly safer than a false-negative, which could
1844
+ * leak a novel name past the SDK.
1845
+ */
1846
+ has(name) {
1847
+ const h1 = murmurhash3_x86_32(name, this.params.seedA);
1848
+ const h2 = murmurhash3_x86_32(name, this.params.seedB);
1849
+ for (let i = 0; i < this.params.k; i++) {
1850
+ const combined = h1 + Math.imul(i, h2) >>> 0;
1851
+ const idx = combined & this.mask;
1852
+ const bit = this.buf[idx >> 3] & 1 << (idx & 7);
1853
+ if (bit === 0) return false;
1854
+ }
1855
+ return true;
1856
+ }
1857
+ }
1858
+ const SUPPORTED_ALGO = "mmh3_x86_32_km";
1859
+ function warnOncePerSession(message) {
1860
+ if (typeof console === "undefined" || typeof console.warn !== "function") return;
1861
+ console.warn(message);
1862
+ }
1863
+ class NameGovernor {
1864
+ constructor() {
1865
+ this.bloom = null;
1866
+ this.remainingNewNames = Infinity;
1867
+ this.localNovelNames = /* @__PURE__ */ new Set();
1868
+ this.droppedSinceLastReport = /* @__PURE__ */ new Map();
1869
+ this.reportWindowStart = Date.now();
1870
+ this.hasWarnedThisSession = false;
1871
+ }
1872
+ /**
1873
+ * Ingest a freshly-bootstrapped hint. Call on every successful bootstrap.
1874
+ * Passing `null` disables governance (fail-open).
1875
+ */
1876
+ ingestHint(hint) {
1877
+ if (!hint || hint.bloom_algo !== SUPPORTED_ALGO) {
1878
+ this.bloom = null;
1879
+ this.remainingNewNames = Infinity;
1880
+ this.localNovelNames.clear();
1881
+ return;
1882
+ }
1883
+ try {
1884
+ this.bloom = BloomFilter.fromBase64(hint.bloom_b64, {
1885
+ m: hint.m,
1886
+ k: hint.k,
1887
+ seedA: hint.seed_a,
1888
+ seedB: hint.seed_b
1889
+ });
1890
+ } catch {
1891
+ this.bloom = null;
1892
+ }
1893
+ this.remainingNewNames = hint.remaining_new_names ?? Infinity;
1894
+ this.localNovelNames.clear();
1895
+ }
1896
+ /**
1897
+ * Decide whether a `track()` call should proceed.
1898
+ *
1899
+ * Returns true = send to network (rate-limiter still runs after).
1900
+ * Returns false = drop locally; caller should return early.
1901
+ */
1902
+ shouldSend(eventName) {
1903
+ if (!this.bloom) return true;
1904
+ if (this.bloom.has(eventName)) return true;
1905
+ if (this.localNovelNames.has(eventName)) return true;
1906
+ if (this.remainingNewNames > 0) {
1907
+ this.localNovelNames.add(eventName);
1908
+ this.remainingNewNames -= 1;
1909
+ return true;
1910
+ }
1911
+ const prev = this.droppedSinceLastReport.get(eventName) ?? 0;
1912
+ this.droppedSinceLastReport.set(eventName, prev + 1);
1913
+ if (!this.hasWarnedThisSession) {
1914
+ this.hasWarnedThisSession = true;
1915
+ warnOncePerSession(
1916
+ `[aegis] Event-name cap reached — "${eventName}" dropped locally. Upgrade your plan or remove dynamically-generated event names.`
1917
+ );
1918
+ }
1919
+ return false;
1920
+ }
1921
+ /**
1922
+ * Snapshot + reset the dropped-names counter. Called by the telemetry
1923
+ * beacon on batch flush so the gateway gets visibility into client-side
1924
+ * drops for ops dashboards.
1925
+ */
1926
+ drainDropReport() {
1927
+ if (this.droppedSinceLastReport.size === 0) return null;
1928
+ const events = {};
1929
+ let total = 0;
1930
+ for (const [name, count] of this.droppedSinceLastReport) {
1931
+ events[name] = count;
1932
+ total += count;
1933
+ }
1934
+ const since = this.reportWindowStart;
1935
+ this.droppedSinceLastReport.clear();
1936
+ this.reportWindowStart = Date.now();
1937
+ return { events, total, since };
1938
+ }
1939
+ // Test-only accessors — not part of public API but easier than reflection.
1940
+ /** @internal */
1941
+ _debugState() {
1942
+ return {
1943
+ hasBloom: this.bloom !== null,
1944
+ remaining: this.remainingNewNames,
1945
+ localNovel: this.localNovelNames.size
1946
+ };
1947
+ }
1948
+ }
1764
1949
  class Aegis {
1765
1950
  constructor() {
1766
1951
  this.config = null;
@@ -1772,6 +1957,7 @@ class Aegis {
1772
1957
  this.consent = null;
1773
1958
  this._ecommerce = null;
1774
1959
  this.rateLimiter = null;
1960
+ this.nameGovernor = new NameGovernor();
1775
1961
  this._lastPageUrl = null;
1776
1962
  this._popstateHandler = null;
1777
1963
  this._originalPushState = null;
@@ -1954,6 +2140,7 @@ class Aegis {
1954
2140
  anonymousId: this.identity.getAnonymousId(),
1955
2141
  userId: this.identity.getUserId() || void 0,
1956
2142
  sessionId: this.session.getSessionId(),
2143
+ workspace_id: this.config.workspace_id || void 0,
1957
2144
  context: buildContext(this.config, this.session)
1958
2145
  };
1959
2146
  this.captureEvent(event);
@@ -1969,6 +2156,7 @@ class Aegis {
1969
2156
  anonymousId: this.identity.getAnonymousId(),
1970
2157
  userId,
1971
2158
  sessionId: this.session.getSessionId(),
2159
+ workspace_id: this.config.workspace_id || void 0,
1972
2160
  context: buildContext(this.config, this.session)
1973
2161
  };
1974
2162
  this.captureEvent(event);
@@ -1984,6 +2172,7 @@ class Aegis {
1984
2172
  anonymousId: this.identity.getAnonymousId(),
1985
2173
  userId: this.identity.getUserId() || void 0,
1986
2174
  sessionId: this.session.getSessionId(),
2175
+ workspace_id: this.config.workspace_id || void 0,
1987
2176
  context: buildContext(this.config, this.session)
1988
2177
  };
1989
2178
  this.captureEvent(event);
@@ -2028,6 +2217,13 @@ class Aegis {
2028
2217
  }
2029
2218
  }
2030
2219
  }
2220
+ if (event.type === "track") {
2221
+ const trackEvent = event;
2222
+ const isMetaEvent = typeof trackEvent.event === "string" && trackEvent.event.startsWith("aegis.client.");
2223
+ if (!isMetaEvent && !this.nameGovernor.shouldSend(trackEvent.event)) {
2224
+ return;
2225
+ }
2226
+ }
2031
2227
  if (this.rateLimiter) {
2032
2228
  const eventLabel = event.type === "track" && event.event ? event.event : event.type;
2033
2229
  const isMetaEvent = event.type === "track" && typeof event.event === "string" && event.event.startsWith("aegis.client.");
@@ -2035,6 +2231,21 @@ class Aegis {
2035
2231
  return;
2036
2232
  }
2037
2233
  }
2234
+ const ctx = event.context;
2235
+ const props = event.properties;
2236
+ if ((ctx == null ? void 0 : ctx.campaign) && props && !props.campaign_id) {
2237
+ const utm = ctx.campaign;
2238
+ if (utm.campaign) props.campaign_id = utm.campaign;
2239
+ if (utm.source) props.campaign_source = utm.source;
2240
+ if (utm.medium) props.campaign_medium = utm.medium;
2241
+ if (utm.content) props.campaign_content = utm.content;
2242
+ if (utm.term) props.campaign_term = utm.term;
2243
+ }
2244
+ if ((ctx == null ? void 0 : ctx.adClickIDs) && props) {
2245
+ const clicks = ctx.adClickIDs;
2246
+ const clickId = clicks.gclid || clicks.fbclid || clicks.ctwa_clid || clicks.msclkid || clicks.ttclid;
2247
+ if (clickId && !props.campaign_click_id) props.campaign_click_id = clickId;
2248
+ }
2038
2249
  logger.debug("Capturing event:", event);
2039
2250
  const transformedEvent = await this.plugins.executeHookChain(
2040
2251
  "beforeEventCapture",
@@ -2078,8 +2289,59 @@ class Aegis {
2078
2289
  this.identity.reset();
2079
2290
  logger.info("User identity reset");
2080
2291
  }
2292
+ /**
2293
+ * Ingest the event-governance hint returned from `/v1/sdk/bootstrap`.
2294
+ *
2295
+ * Callers (Shopify pixel, snippet, react integration) run `bootstrap()`
2296
+ * themselves and pass the resulting `eventGovernance` field here so the
2297
+ * SDK can self-throttle novel event names before they hit the gateway.
2298
+ *
2299
+ * Passing null/undefined disables governance (Enterprise plan / outage
2300
+ * fail-open). Safe to call before `init()` — the hint is stored and
2301
+ * applied as soon as init completes.
2302
+ */
2303
+ ingestGovernanceHint(hint) {
2304
+ this.nameGovernor.ingestHint(hint ?? null);
2305
+ logger.debug("Governance hint ingested", hint ? {
2306
+ k: hint.k,
2307
+ m: hint.m,
2308
+ remaining: hint.remaining_new_names
2309
+ } : "disabled");
2310
+ }
2311
+ /**
2312
+ * Drain the client-side drop counter and emit a meta-event. Called
2313
+ * periodically by the queue flush path so ops dashboards see novel-name
2314
+ * amplification patterns in near-real-time.
2315
+ */
2316
+ emitGovernanceDropMeta() {
2317
+ var _a, _b;
2318
+ if (!((_a = this.config) == null ? void 0 : _a.initialized) || !this.session || !this.identity) return;
2319
+ const report = this.nameGovernor.drainDropReport();
2320
+ if (!report) return;
2321
+ const event = {
2322
+ type: "track",
2323
+ event: "aegis.client.name_governor_dropped",
2324
+ properties: {
2325
+ dropped_count: report.total,
2326
+ distinct_names: Object.keys(report.events).length,
2327
+ // Cap the payload — a runaway loop could produce thousands of names;
2328
+ // we ship the top 10 for diagnostics and the total for counting.
2329
+ sample_names: Object.entries(report.events).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([name, count]) => ({ name, count })),
2330
+ window_start: report.since,
2331
+ window_end: Date.now()
2332
+ },
2333
+ messageId: generateMessageId(),
2334
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2335
+ anonymousId: this.identity.getAnonymousId(),
2336
+ userId: this.identity.getUserId() || void 0,
2337
+ sessionId: this.session.getSessionId(),
2338
+ context: buildContext(this.config, this.session)
2339
+ };
2340
+ (_b = this.queue) == null ? void 0 : _b.push(event);
2341
+ }
2081
2342
  async flush() {
2082
2343
  if (!this.assertInitialized()) return;
2344
+ this.emitGovernanceDropMeta();
2083
2345
  await this.queue.flush();
2084
2346
  }
2085
2347
  getAnonymousId() {
@@ -2211,8 +2473,11 @@ class Aegis {
2211
2473
  }
2212
2474
  export {
2213
2475
  Aegis as A,
2476
+ BloomFilter as B,
2214
2477
  EcommerceTracker as E,
2478
+ NameGovernor as N,
2215
2479
  RateLimiter as R,
2216
- logger as l
2480
+ logger as l,
2481
+ murmurhash3_x86_32 as m
2217
2482
  };
2218
- //# sourceMappingURL=analytics-Q2zua5Gf.mjs.map
2483
+ //# sourceMappingURL=analytics-D9BAnJAu.mjs.map