@runbandit/sdk 3.4.0 → 3.5.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.
package/dist/index.js CHANGED
@@ -614,7 +614,7 @@ function matchesConversionPath(pattern, pathname) {
614
614
  }
615
615
 
616
616
  // ../shared/dist/site.js
617
- var SITE_TARGET_KINDS = ["text", "image"];
617
+ var SITE_TARGET_KINDS = ["text", "image", "order"];
618
618
  var TAG_NAME = /^[a-z][a-z0-9-]*$/;
619
619
  function collapseWhitespace(raw) {
620
620
  return raw.replace(/\s+/g, " ").trim();
@@ -649,13 +649,13 @@ function parseSiteContent(config) {
649
649
  }
650
650
  function normalizeSiteTarget(target) {
651
651
  if (!SITE_TARGET_KINDS.includes(target.kind))
652
- throw new Error("A target is text or an image");
652
+ throw new Error("A target is text, an image, or a list");
653
653
  return {
654
654
  page: requirePage(target.page),
655
655
  selector: requireText(target.selector, "The picked element has no selector"),
656
656
  tag: requireTag(target.tag),
657
657
  kind: target.kind,
658
- original: requireText(target.original, "The picked element has no content to replace")
658
+ original: target.kind === "order" ? formatOrder(parseOrder(target.original)) : requireText(target.original, "The picked element has no content to replace")
659
659
  };
660
660
  }
661
661
  function normalizeFunnelGoal(goal) {
@@ -679,6 +679,39 @@ function goalMatchesPage(goal, pathname) {
679
679
  var MAX_ALTERNATIVES = 5;
680
680
  var MAX_TEXT_ALTERNATIVE_LENGTH = 300;
681
681
  var MAX_IMAGE_URL_LENGTH = 2048;
682
+ var MIN_ORDER_ITEMS = 2;
683
+ var MAX_ORDER_ITEMS = 24;
684
+ var MAX_ORDER_KEY_LENGTH = 160;
685
+ function formatOrder(keys) {
686
+ return JSON.stringify(keys);
687
+ }
688
+ function parseOrder(raw) {
689
+ let value;
690
+ try {
691
+ value = JSON.parse(raw);
692
+ } catch {
693
+ throw new Error("An order is a list of the items in the order to show them");
694
+ }
695
+ if (!Array.isArray(value) || !value.every((key) => typeof key === "string" && key.length > 0)) {
696
+ throw new Error("An order is a list of the items in the order to show them");
697
+ }
698
+ const keys = value;
699
+ if (keys.length < MIN_ORDER_ITEMS)
700
+ throw new Error(`A list needs at least ${MIN_ORDER_ITEMS} items`);
701
+ if (keys.length > MAX_ORDER_ITEMS)
702
+ throw new Error(`Reorder lists of at most ${MAX_ORDER_ITEMS} items`);
703
+ if (keys.some((key) => key.length > MAX_ORDER_KEY_LENGTH))
704
+ throw new Error("An item in that list is too long");
705
+ if (new Set(keys).size !== keys.length)
706
+ throw new Error("Every item in the list must be different");
707
+ return keys;
708
+ }
709
+ function isReorderOf(original, order) {
710
+ if (original.length !== order.length)
711
+ return false;
712
+ const remaining = new Set(original);
713
+ return order.every((key) => remaining.delete(key));
714
+ }
682
715
  var SUGGESTED_SITE_EXPERIMENTS = 3;
683
716
  function moreExperimentsSentence(running, suggested) {
684
717
  const noun = (count) => count === 1 ? "experiment" : "experiments";
@@ -707,6 +740,8 @@ function parseAlternative(kind, raw) {
707
740
  }
708
741
  return text;
709
742
  }
743
+ if (kind === "order")
744
+ return formatOrder(parseOrder(raw));
710
745
  const trimmed = raw.trim();
711
746
  if (trimmed.length > MAX_IMAGE_URL_LENGTH)
712
747
  throw new Error("That image address is too long");
@@ -722,8 +757,8 @@ function parseAlternative(kind, raw) {
722
757
  }
723
758
  var PICK_CHANNEL = "bandit-pick";
724
759
  var PICK_PROTOCOL_VERSION = 1;
725
- var PICK_MODES = ["idle", "target", "goal-click"];
726
- var PICK_REJECTIONS = ["not-text-or-image", "has-markup", "not-clickable", "no-selector"];
760
+ var PICK_MODES = ["idle", "target", "order", "goal-click"];
761
+ var PICK_REJECTIONS = ["not-text-or-image", "has-markup", "not-a-list", "not-clickable", "no-selector"];
727
762
  var PICK_TOKEN_PREFIX = "bpick_";
728
763
  function pickEnvelope(msg) {
729
764
  return { channel: PICK_CHANNEL, v: PICK_PROTOCOL_VERSION, msg };
@@ -806,6 +841,20 @@ function parsePanelMessage(data) {
806
841
  // ../shared/dist/site-platforms.js
807
842
  var SITE_PLATFORMS = [
808
843
  { id: "shopify", label: "Shopify", path: "shopify", codeBox: "", steps: [] },
844
+ {
845
+ id: "shopify_theme",
846
+ label: "Shopify",
847
+ path: "script",
848
+ codeBox: "Online Store, Themes, Edit code, layout/theme.liquid, just before </head>.",
849
+ steps: [
850
+ "In your Shopify admin open Online Store, then Themes.",
851
+ "On your live theme click the three dots, then Edit code.",
852
+ "Open layout/theme.liquid.",
853
+ "Paste the script on the line just before </head>.",
854
+ "Click Save.",
855
+ "Then add the purchase pixel below, so Bandit learns from orders."
856
+ ]
857
+ },
809
858
  {
810
859
  id: "webflow",
811
860
  label: "Webflow",
@@ -1148,6 +1197,9 @@ function touchFromPage(page) {
1148
1197
  const trackingParam = Object.keys(TRACKING_PARAM_SOURCES).find((name) => params.has(name));
1149
1198
  if (trackingParam)
1150
1199
  touch.trackingParam = trackingParam;
1200
+ const klaviyoExchangeId = params.get("_kx");
1201
+ if (klaviyoExchangeId)
1202
+ touch.klaviyoExchangeId = klaviyoExchangeId;
1151
1203
  const referrerHost = normalizeHost(safeUrl(page.referrer)?.hostname);
1152
1204
  if (referrerHost && referrerHost !== normalizeHost(url.hostname))
1153
1205
  touch.referrerHost = referrerHost;
@@ -1160,7 +1212,8 @@ function touchSignature(touch) {
1160
1212
  touch.utmCampaign ?? "",
1161
1213
  touch.utmContent ?? "",
1162
1214
  touch.trackingParam ?? "",
1163
- touch.referrerHost ?? ""
1215
+ touch.referrerHost ?? "",
1216
+ touch.klaviyoExchangeId ?? ""
1164
1217
  ]);
1165
1218
  }
1166
1219
  function shouldReportTouch(touch, stored, sessionId, now) {
@@ -1201,6 +1254,100 @@ function sourceOf(arrival) {
1201
1254
  return { key: "ch:tagged", label: "Tagged" };
1202
1255
  }
1203
1256
 
1257
+ // ../shared/dist/cdp-aliases.js
1258
+ var CDP_SOURCES = ["segment", "rudderstack", "posthog"];
1259
+ var MAX_CDP_EXTERNAL_ID_LENGTH = 256;
1260
+ var RUDDERSTACK_ENCRYPTED_PREFIX = "RS_ENC_v3_";
1261
+ var RUDDERSTACK_LEGACY_ENCRYPTED_PREFIX = "RudderEncrypt:";
1262
+ function decoded(raw) {
1263
+ try {
1264
+ return decodeURIComponent(raw);
1265
+ } catch {
1266
+ return raw;
1267
+ }
1268
+ }
1269
+ function jsonValue(text) {
1270
+ try {
1271
+ return JSON.parse(text);
1272
+ } catch {
1273
+ return void 0;
1274
+ }
1275
+ }
1276
+ function nonEmpty(value) {
1277
+ return typeof value === "string" && value.length > 0 ? value : null;
1278
+ }
1279
+ function plainOrQuoted(raw) {
1280
+ const text = decoded(raw);
1281
+ return nonEmpty(text.startsWith('"') ? jsonValue(text) : text);
1282
+ }
1283
+ function rudderstackValue(raw) {
1284
+ const text = plainOrQuoted(raw);
1285
+ if (text === null || text.startsWith(RUDDERSTACK_LEGACY_ENCRYPTED_PREFIX))
1286
+ return null;
1287
+ if (!text.startsWith(RUDDERSTACK_ENCRYPTED_PREFIX))
1288
+ return text;
1289
+ try {
1290
+ return nonEmpty(jsonValue(atob(text.slice(RUDDERSTACK_ENCRYPTED_PREFIX.length))));
1291
+ } catch {
1292
+ return null;
1293
+ }
1294
+ }
1295
+ function posthogDistinctId(raw) {
1296
+ const parsed = jsonValue(decoded(raw));
1297
+ if (typeof parsed !== "object" || parsed === null)
1298
+ return null;
1299
+ const state = parsed;
1300
+ const deviceId = nonEmpty(state.$device_id);
1301
+ if (deviceId !== null)
1302
+ return deviceId;
1303
+ return state.$user_state === "identified" ? null : nonEmpty(state.distinct_id);
1304
+ }
1305
+ var CDP_SOURCE_REGISTRY = [
1306
+ { source: "segment", key: /^ajs_anonymous_id$/, parse: plainOrQuoted },
1307
+ { source: "rudderstack", key: /^rl_anonymous_id$/, parse: rudderstackValue },
1308
+ { source: "posthog", key: /^ph_.+_posthog$/, parse: posthogDistinctId }
1309
+ ];
1310
+ function cookiePairs(header) {
1311
+ const pairs = [];
1312
+ for (const part of header.split(";")) {
1313
+ const index = part.indexOf("=");
1314
+ if (index === -1)
1315
+ continue;
1316
+ pairs.push([part.slice(0, index).trim(), part.slice(index + 1).trim()]);
1317
+ }
1318
+ return pairs;
1319
+ }
1320
+ function storagePairs(storage) {
1321
+ const pairs = [];
1322
+ try {
1323
+ for (let i = 0; i < (storage?.length ?? 0); i++) {
1324
+ const key = storage.key(i);
1325
+ const value = key === null ? null : storage.getItem(key);
1326
+ if (key !== null && value !== null)
1327
+ pairs.push([key, value]);
1328
+ }
1329
+ } catch {
1330
+ return pairs;
1331
+ }
1332
+ return pairs;
1333
+ }
1334
+ function readCdpAliases(cookieHeader, storage) {
1335
+ const candidates = [...cookiePairs(cookieHeader), ...storagePairs(storage)];
1336
+ const aliases = [];
1337
+ for (const { source, key, parse } of CDP_SOURCE_REGISTRY) {
1338
+ for (const [name, raw] of candidates) {
1339
+ if (!key.test(name))
1340
+ continue;
1341
+ const externalId = parse(raw);
1342
+ if (externalId === null || externalId.length > MAX_CDP_EXTERNAL_ID_LENGTH)
1343
+ continue;
1344
+ aliases.push({ source, externalId });
1345
+ break;
1346
+ }
1347
+ }
1348
+ return aliases;
1349
+ }
1350
+
1204
1351
  // ../shared/dist/payout-unit.js
1205
1352
  var PAYOUT_UNITS = ["count", "revenue", "margin"];
1206
1353
  var NEW_PAYOUT_UNIT = "count";
@@ -1246,6 +1393,24 @@ function formatPayout(unit, value) {
1246
1393
  return isCurrencyUnit(unit) ? USD.format(value) : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
1247
1394
  }
1248
1395
 
1396
+ // ../shared/dist/trial-rule.js
1397
+ var TRIAL_RULES = ["served", "exposure"];
1398
+ function isTrialRule(value) {
1399
+ return value === "served" || value === "exposure";
1400
+ }
1401
+ function trialRuleFor({ target, requested }) {
1402
+ if (requested)
1403
+ return requested;
1404
+ return target ? "exposure" : "served";
1405
+ }
1406
+ function trialRuleOf(experiment) {
1407
+ return experiment.trialOn ?? "served";
1408
+ }
1409
+ var TRIAL_RULE_COPY = {
1410
+ served: "Counts a trial when served",
1411
+ exposure: "Counts a trial when seen"
1412
+ };
1413
+
1249
1414
  // src/client.ts
1250
1415
  var ARRIVAL_KEY = "bandits_arrival";
1251
1416
  function readStoredArrival(storage) {
@@ -1257,6 +1422,22 @@ function readStoredArrival(storage) {
1257
1422
  return null;
1258
1423
  }
1259
1424
  }
1425
+ var CDP_ALIASES_KEY = "bandits_cdp_aliases";
1426
+ function readSentAliases(storage, visitorId) {
1427
+ try {
1428
+ const parsed = JSON.parse(storage?.getItem(CDP_ALIASES_KEY) ?? "null");
1429
+ return parsed?.visitorId === visitorId && typeof parsed.sent === "object" && parsed.sent !== null ? parsed.sent : {};
1430
+ } catch {
1431
+ return {};
1432
+ }
1433
+ }
1434
+ function readCookieHeader() {
1435
+ try {
1436
+ return document.cookie ?? "";
1437
+ } catch {
1438
+ return "";
1439
+ }
1440
+ }
1260
1441
  var BanditsRequestError = class extends Error {
1261
1442
  status;
1262
1443
  constructor(message, status) {
@@ -1279,6 +1460,8 @@ var BanditsClient = class {
1279
1460
  namedEventQueue = [];
1280
1461
  sessionId;
1281
1462
  visitorId;
1463
+ exposureQueue = [];
1464
+ exposedIds = /* @__PURE__ */ new Set();
1282
1465
  flushTimer;
1283
1466
  constructor(config) {
1284
1467
  this.config = {
@@ -1292,6 +1475,32 @@ var BanditsClient = class {
1292
1475
  this.visitorId = config.visitorId ?? getOrCreateVisitorId(defaultSessionStorage());
1293
1476
  this.startBatchFlush();
1294
1477
  if (config.captureArrival !== false) void this.reportArrival();
1478
+ if (config.captureCdpAliases !== false) void this.reportCdpAliases();
1479
+ }
1480
+ async reportCdpAliases() {
1481
+ if (typeof document === "undefined") return false;
1482
+ const storage = defaultSessionStorage();
1483
+ const sent = readSentAliases(storage, this.visitorId);
1484
+ const fresh = readCdpAliases(readCookieHeader(), storage).filter(({ source, externalId }) => sent[source] !== externalId);
1485
+ if (fresh.length === 0) return false;
1486
+ try {
1487
+ await this.aliasVisitor(fresh);
1488
+ } catch {
1489
+ return false;
1490
+ }
1491
+ const record = { visitorId: this.visitorId, sent: { ...sent, ...Object.fromEntries(fresh.map((a) => [a.source, a.externalId])) } };
1492
+ try {
1493
+ storage?.setItem(CDP_ALIASES_KEY, JSON.stringify(record));
1494
+ } catch {
1495
+ return true;
1496
+ }
1497
+ return true;
1498
+ }
1499
+ async aliasVisitor(aliases) {
1500
+ const response = await this.makeRequest("/aliases", "POST", { visitorId: this.visitorId, aliases });
1501
+ if (!response.success) {
1502
+ throw requestError(response, "Failed to alias visitor");
1503
+ }
1295
1504
  }
1296
1505
  async reportArrival() {
1297
1506
  if (typeof location === "undefined" || typeof document === "undefined") return false;
@@ -1336,7 +1545,8 @@ var BanditsClient = class {
1336
1545
  experimentId,
1337
1546
  userId,
1338
1547
  config: assignment.treatment?.config || {},
1339
- funnelId: assignment.funnelId
1548
+ funnelId: assignment.funnelId,
1549
+ trialOn: assignment.trialOn
1340
1550
  };
1341
1551
  }
1342
1552
  trackNamedEvent(event, opts) {
@@ -1373,6 +1583,12 @@ var BanditsClient = class {
1373
1583
  }
1374
1584
  }
1375
1585
  async flush() {
1586
+ const results = await Promise.allSettled([this.flushNamedEvents(), this.flushExposures()]);
1587
+ for (const result of results) {
1588
+ if (result.status === "rejected") throw result.reason;
1589
+ }
1590
+ }
1591
+ async flushNamedEvents() {
1376
1592
  const namedEvents = [...this.namedEventQueue];
1377
1593
  this.namedEventQueue = [];
1378
1594
  if (namedEvents.length === 0) return;
@@ -1391,14 +1607,75 @@ var BanditsClient = class {
1391
1607
  throw error;
1392
1608
  }
1393
1609
  }
1394
- async identify(anonymousId, identifiedUserId, mergeReason) {
1610
+ async flushExposures() {
1611
+ const exposures = [...this.exposureQueue];
1612
+ this.exposureQueue = [];
1613
+ if (exposures.length === 0) return;
1614
+ try {
1615
+ await fetch(`${this.normalizeApiUrl()}/exposures`, {
1616
+ method: "POST",
1617
+ headers: {
1618
+ "Authorization": `Bearer ${this.config.apiKey}`,
1619
+ "Content-Type": "application/json"
1620
+ },
1621
+ body: JSON.stringify({ exposures }),
1622
+ keepalive: true
1623
+ });
1624
+ } catch (error) {
1625
+ this.exposureQueue.unshift(...exposures);
1626
+ throw error;
1627
+ }
1628
+ }
1629
+ exposed(assignmentId, opts) {
1630
+ if (this.exposedIds.has(assignmentId)) return;
1631
+ this.exposedIds.add(assignmentId);
1632
+ this.exposureQueue.push({ assignmentId, exposedAt: Date.now() });
1633
+ if (opts?.immediate || this.exposureQueue.length >= this.config.batchSize) {
1634
+ this.flushExposures().catch(console.error);
1635
+ }
1636
+ }
1637
+ observeExposure(assignmentId, element, opts) {
1638
+ if (typeof IntersectionObserver === "undefined") return () => {
1639
+ };
1640
+ const threshold = opts?.threshold ?? 0.5;
1641
+ const dwellMs = opts?.dwellMs ?? 1e3;
1642
+ let dwell;
1643
+ const stop = () => {
1644
+ clearTimeout(dwell);
1645
+ dwell = void 0;
1646
+ };
1647
+ const observer = new IntersectionObserver(
1648
+ (entries) => {
1649
+ const entry = entries[entries.length - 1];
1650
+ if (!entry) return;
1651
+ const coversViewport = entry.rootBounds != null && entry.intersectionRect.height >= threshold * entry.rootBounds.height;
1652
+ if (entry.isIntersecting && (entry.intersectionRatio >= threshold || coversViewport)) {
1653
+ dwell ??= setTimeout(() => {
1654
+ this.exposed(assignmentId);
1655
+ disconnect();
1656
+ }, dwellMs);
1657
+ } else {
1658
+ stop();
1659
+ }
1660
+ },
1661
+ { threshold: Array.from({ length: 21 }, (_, i) => i / 20) }
1662
+ );
1663
+ const disconnect = () => {
1664
+ stop();
1665
+ observer.disconnect();
1666
+ };
1667
+ observer.observe(element);
1668
+ return disconnect;
1669
+ }
1670
+ async identify(anonymousId, identifiedUserId, mergeReason, options) {
1395
1671
  const response = await this.makeRequest(
1396
1672
  "/identify",
1397
1673
  "POST",
1398
1674
  {
1399
1675
  anonymousId,
1400
1676
  identifiedUserId,
1401
- mergeReason: mergeReason || "manual"
1677
+ mergeReason: mergeReason || "manual",
1678
+ ...options?.email ? { email: options.email } : {}
1402
1679
  }
1403
1680
  );
1404
1681
  if (!response.success) {
@@ -1412,6 +1689,7 @@ var BanditsClient = class {
1412
1689
  }
1413
1690
  }
1414
1691
  async trackFunnelReward(funnelId, userId, value) {
1692
+ await this.flushExposures().catch(() => void 0);
1415
1693
  const response = await this.makeRequest(
1416
1694
  `/funnels/${funnelId}/reward`,
1417
1695
  "POST",
@@ -1725,6 +2003,8 @@ export {
1725
2003
  BanditsClient as BanditClient,
1726
2004
  BanditsClient,
1727
2005
  BanditsRequestError,
2006
+ CDP_SOURCES,
2007
+ CDP_SOURCE_REGISTRY,
1728
2008
  CHANNEL_LABELS,
1729
2009
  CHANNEL_RULES,
1730
2010
  CONTENT_TYPE_BY_KIND,
@@ -1739,12 +2019,16 @@ export {
1739
2019
  FunnelStatus,
1740
2020
  MAX_ALTERNATIVES,
1741
2021
  MAX_ARRIVAL_VALUE_LENGTH,
2022
+ MAX_CDP_EXTERNAL_ID_LENGTH,
1742
2023
  MAX_IMAGE_URL_LENGTH,
1743
2024
  MAX_ORDERING_ITEMS,
2025
+ MAX_ORDER_ITEMS,
2026
+ MAX_ORDER_KEY_LENGTH,
1744
2027
  MAX_RAW_TOUCH_VALUE_LENGTH,
1745
2028
  MAX_REFERRER_HOST_LENGTH,
1746
2029
  MAX_TEXT_ALTERNATIVE_LENGTH,
1747
2030
  MIN_ORDERING_ITEMS,
2031
+ MIN_ORDER_ITEMS,
1748
2032
  NEW_PAYOUT_UNIT,
1749
2033
  PASS_THROUGH_HOSTS,
1750
2034
  PAYOUT_UNITS,
@@ -1768,6 +2052,8 @@ export {
1768
2052
  SUGGESTED_SITE_EXPERIMENTS,
1769
2053
  SnapshotClient,
1770
2054
  TRACKING_PARAM_SOURCES,
2055
+ TRIAL_RULES,
2056
+ TRIAL_RULE_COPY,
1771
2057
  UNKNOWN_SOURCE_KEY,
1772
2058
  UTM_PARAMS,
1773
2059
  VISITOR_COOKIE,
@@ -1781,6 +2067,7 @@ export {
1781
2067
  extractEmailLinks,
1782
2068
  extractSlotNames,
1783
2069
  extractSubdomain,
2070
+ formatOrder,
1784
2071
  formatPayout,
1785
2072
  generateId,
1786
2073
  generateSubdomain,
@@ -1796,8 +2083,10 @@ export {
1796
2083
  isPassThroughHost,
1797
2084
  isPayoutUnit,
1798
2085
  isPlacement,
2086
+ isReorderOf,
1799
2087
  isShopifyCdnUrl,
1800
2088
  isSlotBinding,
2089
+ isTrialRule,
1801
2090
  isValidEmail,
1802
2091
  isValidSubdomain,
1803
2092
  kindOfPlacement,
@@ -1814,6 +2103,7 @@ export {
1814
2103
  normalizeTreatmentWeights,
1815
2104
  parseAlternative,
1816
2105
  parseDecisions,
2106
+ parseOrder,
1817
2107
  parsePanelMessage,
1818
2108
  parsePickerMessage,
1819
2109
  parseSiteContent,
@@ -1824,6 +2114,7 @@ export {
1824
2114
  pickStep,
1825
2115
  pickTreatment,
1826
2116
  platformById,
2117
+ readCdpAliases,
1827
2118
  readCookie,
1828
2119
  renderEmail,
1829
2120
  renderEmailDocument,
@@ -1842,6 +2133,8 @@ export {
1842
2133
  targetMatchesPage,
1843
2134
  touchFromPage,
1844
2135
  touchSignature,
2136
+ trialRuleFor,
2137
+ trialRuleOf,
1845
2138
  uniformFor,
1846
2139
  validateTreatmentWeights,
1847
2140
  verifyEmailToken