@runbandit/sdk 3.1.3 → 3.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.
package/dist/index.js CHANGED
@@ -1,3 +1,21 @@
1
+ import {
2
+ DECISIONS_COOKIE,
3
+ DEFAULT_SESSION_TTL_MS,
4
+ SESSION_CREATED_AT_KEY,
5
+ SESSION_ID_KEY,
6
+ SnapshotClient,
7
+ VISITOR_COOKIE,
8
+ VISITOR_ID_KEY,
9
+ defaultSessionStorage,
10
+ generateId,
11
+ getOrCreateSessionId,
12
+ getOrCreateVisitorId,
13
+ parseDecisions,
14
+ pickTreatment,
15
+ readCookie,
16
+ serializeDecisions
17
+ } from "./chunk-BG5L4PBC.js";
18
+
1
19
  // ../shared/dist/types.js
2
20
  var ExperimentStatus;
3
21
  (function(ExperimentStatus2) {
@@ -343,7 +361,7 @@ function resolve(value, slots, missing) {
343
361
  }
344
362
  return resolved;
345
363
  }
346
- function extractDocumentSlots(document) {
364
+ function extractDocumentSlots(document2) {
347
365
  const names = [];
348
366
  const seen = /* @__PURE__ */ new Set();
349
367
  const visit = (value) => {
@@ -359,9 +377,9 @@ function extractDocumentSlots(document) {
359
377
  else if (typeof value === "object" && value !== null)
360
378
  Object.values(value).forEach(visit);
361
379
  };
362
- visit(document.subject);
363
- visit(document.preheader);
364
- document.blocks.forEach(visit);
380
+ visit(document2.subject);
381
+ visit(document2.preheader);
382
+ document2.blocks.forEach(visit);
365
383
  return names;
366
384
  }
367
385
  function renderBlock(block, slots, missing) {
@@ -402,13 +420,13 @@ function renderBlock(block, slots, missing) {
402
420
  return "";
403
421
  }
404
422
  }
405
- function renderEmailDocument(document, slots) {
423
+ function renderEmailDocument(document2, slots) {
406
424
  const missingSlots = [];
407
- const width = document.contentWidth ?? CONTENT_WIDTH;
408
- const background = escapeHtml(document.backgroundColor ?? "#f4f4f5");
409
- const subject = resolve(document.subject, slots, missingSlots);
410
- const preheader = resolve(document.preheader, slots, missingSlots);
411
- const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join("\n");
425
+ const width = document2.contentWidth ?? CONTENT_WIDTH;
426
+ const background = escapeHtml(document2.backgroundColor ?? "#f4f4f5");
427
+ const subject = resolve(document2.subject, slots, missingSlots);
428
+ const preheader = resolve(document2.preheader, slots, missingSlots);
429
+ const body = document2.blocks.map((block) => renderBlock(block, slots, missingSlots)).join("\n");
412
430
  const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
413
431
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
414
432
  <head>
@@ -898,55 +916,347 @@ function platformById(id) {
898
916
  return SITE_PLATFORMS.find((platform) => platform.id === id);
899
917
  }
900
918
 
901
- // src/session.ts
902
- var SESSION_ID_KEY = "bandits_session_id";
903
- var SESSION_CREATED_AT_KEY = "bandits_session_created_at";
904
- var VISITOR_ID_KEY = "bandits_visitor_id";
905
- var DEFAULT_SESSION_TTL_MS = 864e5;
906
- function generateId() {
907
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
908
- return crypto.randomUUID();
919
+ // ../shared/dist/arrival.js
920
+ var ARRIVAL_CHANNELS = ["ad", "email", "social", "search", "referral", "direct"];
921
+ var ARRIVAL_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
922
+ var MAX_ARRIVAL_VALUE_LENGTH = 40;
923
+ var MAX_REFERRER_HOST_LENGTH = 100;
924
+ var MAX_RAW_TOUCH_VALUE_LENGTH = 200;
925
+ var UTM_PARAMS = [
926
+ ["utmSource", "utm_source"],
927
+ ["utmMedium", "utm_medium"],
928
+ ["utmCampaign", "utm_campaign"],
929
+ ["utmContent", "utm_content"]
930
+ ];
931
+ var TRACKING_PARAM_SOURCES = {
932
+ gclid: "google",
933
+ gbraid: "google",
934
+ wbraid: "google",
935
+ fbclid: "facebook",
936
+ ttclid: "tiktok",
937
+ msclkid: "bing",
938
+ li_fat_id: "linkedin",
939
+ twclid: "x",
940
+ epik: "pinterest",
941
+ sccid: "snapchat",
942
+ _kx: "klaviyo",
943
+ mc_eid: "mailchimp"
944
+ };
945
+ var CHANNEL_RULES = [
946
+ {
947
+ channel: "ad",
948
+ field: "medium",
949
+ values: [
950
+ "cpc",
951
+ "ppc",
952
+ "paid",
953
+ "paid-social",
954
+ "paidsocial",
955
+ "paid-search",
956
+ "paidsearch",
957
+ "display",
958
+ "cpm",
959
+ "cpv",
960
+ "cpa",
961
+ "banner",
962
+ "ad",
963
+ "ads",
964
+ "paid-media",
965
+ "retargeting",
966
+ "affiliate"
967
+ ]
968
+ },
969
+ { channel: "email", field: "medium", values: ["email", "e-mail", "newsletter", "sms"] },
970
+ { channel: "social", field: "medium", values: ["social", "social-media", "organic-social", "sm"] },
971
+ { channel: "search", field: "medium", values: ["organic", "organic-search", "seo"] },
972
+ { channel: "referral", field: "medium", values: ["referral", "partner"] },
973
+ {
974
+ channel: "ad",
975
+ field: "trackingParam",
976
+ values: ["gclid", "gbraid", "wbraid", "fbclid", "ttclid", "msclkid", "li_fat_id", "twclid", "epik", "sccid"]
977
+ },
978
+ { channel: "email", field: "trackingParam", values: ["_kx", "mc_eid"] },
979
+ {
980
+ channel: "email",
981
+ field: "source",
982
+ values: [
983
+ "klaviyo",
984
+ "mailchimp",
985
+ "sendgrid",
986
+ "hubspot",
987
+ "hs-email",
988
+ "braze",
989
+ "iterable",
990
+ "customer.io",
991
+ "customerio",
992
+ "customer-io",
993
+ "omnisend",
994
+ "email",
995
+ "newsletter"
996
+ ]
997
+ },
998
+ {
999
+ channel: "social",
1000
+ field: "source",
1001
+ values: [
1002
+ "facebook",
1003
+ "fb",
1004
+ "instagram",
1005
+ "ig",
1006
+ "tiktok",
1007
+ "twitter",
1008
+ "x",
1009
+ "linkedin",
1010
+ "pinterest",
1011
+ "reddit",
1012
+ "youtube",
1013
+ "snapchat",
1014
+ "threads"
1015
+ ]
1016
+ },
1017
+ { channel: "search", field: "source", values: ["google", "bing", "duckduckgo", "yahoo", "baidu", "yandex", "ecosia"] },
1018
+ {
1019
+ channel: "email",
1020
+ field: "referrerHost",
1021
+ values: ["mail.google.com", "outlook.live.com", "outlook.office.com", "outlook.office365.com", "mail.yahoo.com"]
1022
+ },
1023
+ {
1024
+ channel: "social",
1025
+ field: "referrerHost",
1026
+ values: [
1027
+ "facebook.com",
1028
+ "fb.com",
1029
+ "fb.me",
1030
+ "instagram.com",
1031
+ "t.co",
1032
+ "twitter.com",
1033
+ "x.com",
1034
+ "linkedin.com",
1035
+ "lnkd.in",
1036
+ "pinterest.com",
1037
+ "pin.it",
1038
+ "reddit.com",
1039
+ "youtube.com",
1040
+ "youtu.be",
1041
+ "tiktok.com",
1042
+ "snapchat.com",
1043
+ "threads.net"
1044
+ ]
1045
+ },
1046
+ {
1047
+ channel: "search",
1048
+ field: "referrerHost",
1049
+ values: ["google", "bing.com", "duckduckgo.com", "yahoo", "baidu.com", "yandex", "ecosia.org", "search.brave.com"]
909
1050
  }
910
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
911
- const r = Math.random() * 16 | 0;
912
- return (c === "x" ? r : r & 3 | 8).toString(16);
913
- });
1051
+ ];
1052
+ function normalizeArrivalValue(raw) {
1053
+ const value = raw?.trim().toLowerCase().replace(/[\s_]+/g, "-").slice(0, MAX_ARRIVAL_VALUE_LENGTH);
1054
+ return value ? value : void 0;
1055
+ }
1056
+ function normalizeHost(raw) {
1057
+ const host = raw?.trim().toLowerCase().replace(/^www\./, "").slice(0, MAX_REFERRER_HOST_LENGTH);
1058
+ return host ? host : void 0;
1059
+ }
1060
+ function hostMatches(host, pattern) {
1061
+ if (pattern.includes("."))
1062
+ return host === pattern || host.endsWith(`.${pattern}`);
1063
+ return host.split(".").slice(0, -1).includes(pattern);
1064
+ }
1065
+ var PASS_THROUGH_HOSTS = [
1066
+ "checkout.stripe.com",
1067
+ "stripe.com",
1068
+ "paypal.com",
1069
+ "shop.app",
1070
+ "shopify.com",
1071
+ "klarna.com",
1072
+ "afterpay.com",
1073
+ "affirm.com",
1074
+ "accounts.google.com",
1075
+ "appleid.apple.com",
1076
+ "login.microsoftonline.com",
1077
+ "login.live.com",
1078
+ "auth0.com",
1079
+ "clerk.accounts.dev",
1080
+ "clerk.com",
1081
+ "okta.com"
1082
+ ];
1083
+ function isPassThroughHost(host) {
1084
+ return PASS_THROUGH_HOSTS.some((pattern) => hostMatches(host, pattern));
914
1085
  }
915
- function getOrCreateSessionId(options = {}) {
916
- const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;
917
- if (!storage) return generateId();
918
- try {
919
- const stored = storage.getItem(SESSION_ID_KEY);
920
- const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);
921
- if (stored && storedAt) {
922
- const age = now() - parseInt(storedAt, 10);
923
- if (age < ttlMs) return stored;
924
- }
925
- const id = generateId();
926
- storage.setItem(SESSION_ID_KEY, id);
927
- storage.setItem(SESSION_CREATED_AT_KEY, String(now()));
928
- return id;
929
- } catch {
930
- return generateId();
931
- }
1086
+ function ruleMatches(rule, signals) {
1087
+ const value = signals[rule.field];
1088
+ if (!value)
1089
+ return false;
1090
+ if (rule.field === "referrerHost")
1091
+ return rule.values.some((pattern) => hostMatches(value, pattern));
1092
+ return rule.values.includes(value);
1093
+ }
1094
+ function channelOf(signals) {
1095
+ const { trackingParam, ...arrival } = signals;
1096
+ if (!trackingParam && Object.values(arrival).every((value) => !value))
1097
+ return "direct";
1098
+ const rule = CHANNEL_RULES.find((candidate) => ruleMatches(candidate, signals));
1099
+ if (rule)
1100
+ return rule.channel;
1101
+ return arrival.referrerHost ? "referral" : void 0;
1102
+ }
1103
+ function arrivalReferrerHost(raw) {
1104
+ const host = normalizeHost(raw);
1105
+ return host && !isPassThroughHost(host) ? host : void 0;
1106
+ }
1107
+ function arrivalFromTouch(touch) {
1108
+ const named = touch.trackingParam?.toLowerCase();
1109
+ const trackingParam = named && Object.hasOwn(TRACKING_PARAM_SOURCES, named) ? named : void 0;
1110
+ const fields = {
1111
+ source: normalizeArrivalValue(touch.utmSource) ?? (trackingParam ? TRACKING_PARAM_SOURCES[trackingParam] : void 0),
1112
+ medium: normalizeArrivalValue(touch.utmMedium),
1113
+ campaign: normalizeArrivalValue(touch.utmCampaign),
1114
+ content: normalizeArrivalValue(touch.utmContent),
1115
+ referrerHost: arrivalReferrerHost(touch.referrerHost)
1116
+ };
1117
+ const present = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
1118
+ const channel = channelOf({ ...present, ...trackingParam ? { trackingParam } : {} });
1119
+ return { ...present, ...channel ? { channel } : {} };
932
1120
  }
933
- function getOrCreateVisitorId(storage) {
934
- if (!storage) return generateId();
1121
+ function isDirectTouch(touch) {
1122
+ return !touch.utmSource && !touch.utmMedium && !touch.utmCampaign && !touch.utmContent && !touch.trackingParam && !touch.referrerHost;
1123
+ }
1124
+ function safeUrl(raw) {
935
1125
  try {
936
- const stored = storage.getItem(VISITOR_ID_KEY);
937
- if (stored) return stored;
938
- const id = generateId();
939
- storage.setItem(VISITOR_ID_KEY, id);
940
- return id;
1126
+ return new URL(raw);
941
1127
  } catch {
942
- return generateId();
1128
+ return void 0;
943
1129
  }
944
1130
  }
945
- function defaultSessionStorage() {
946
- return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
1131
+ function touchFromPage(page) {
1132
+ const url = safeUrl(page.href);
1133
+ if (!url)
1134
+ return {};
1135
+ const params = /* @__PURE__ */ new Map();
1136
+ url.searchParams.forEach((value, key) => {
1137
+ const name = key.toLowerCase();
1138
+ const trimmed = value.trim();
1139
+ if (trimmed && !params.has(name))
1140
+ params.set(name, trimmed.slice(0, MAX_RAW_TOUCH_VALUE_LENGTH));
1141
+ });
1142
+ const touch = {};
1143
+ for (const [field, param] of UTM_PARAMS) {
1144
+ const value = params.get(param);
1145
+ if (value)
1146
+ touch[field] = value;
1147
+ }
1148
+ const trackingParam = Object.keys(TRACKING_PARAM_SOURCES).find((name) => params.has(name));
1149
+ if (trackingParam)
1150
+ touch.trackingParam = trackingParam;
1151
+ const referrerHost = normalizeHost(safeUrl(page.referrer)?.hostname);
1152
+ if (referrerHost && referrerHost !== normalizeHost(url.hostname))
1153
+ touch.referrerHost = referrerHost;
1154
+ return touch;
1155
+ }
1156
+ function touchSignature(touch) {
1157
+ return JSON.stringify([
1158
+ touch.utmSource ?? "",
1159
+ touch.utmMedium ?? "",
1160
+ touch.utmCampaign ?? "",
1161
+ touch.utmContent ?? "",
1162
+ touch.trackingParam ?? "",
1163
+ touch.referrerHost ?? ""
1164
+ ]);
1165
+ }
1166
+ function shouldReportTouch(touch, stored, sessionId, now) {
1167
+ if (!stored)
1168
+ return true;
1169
+ if (isDirectTouch(touch))
1170
+ return now - stored.at >= ARRIVAL_TTL_MS;
1171
+ return stored.signature !== touchSignature(touch) || stored.sessionId !== sessionId;
1172
+ }
1173
+ function isArrivalActive(startedAt, at) {
1174
+ return startedAt <= at && at - startedAt < ARRIVAL_TTL_MS;
1175
+ }
1176
+ var CHANNEL_LABELS = {
1177
+ ad: "Ad",
1178
+ email: "Email",
1179
+ social: "Social",
1180
+ search: "Search",
1181
+ referral: "Referral",
1182
+ direct: "Direct"
1183
+ };
1184
+ var UNKNOWN_SOURCE_KEY = "unknown";
1185
+ var titleCase = (value) => value.charAt(0).toUpperCase() + value.slice(1);
1186
+ function sourceOf(arrival) {
1187
+ if (!arrival)
1188
+ return { key: UNKNOWN_SOURCE_KEY, label: "Unknown" };
1189
+ const channel = arrival.channel ? { channel: arrival.channel } : {};
1190
+ if (arrival.source && arrival.campaign) {
1191
+ return { key: `src:${arrival.source}|${arrival.campaign}`, label: `${titleCase(arrival.source)} \xB7 ${arrival.campaign}`, ...channel };
1192
+ }
1193
+ if (arrival.source)
1194
+ return { key: `src:${arrival.source}`, label: titleCase(arrival.source), ...channel };
1195
+ if (arrival.campaign)
1196
+ return { key: `cmp:${arrival.campaign}`, label: arrival.campaign, ...channel };
1197
+ if (arrival.referrerHost)
1198
+ return { key: `ref:${arrival.referrerHost}`, label: arrival.referrerHost, ...channel };
1199
+ if (arrival.channel)
1200
+ return { key: `ch:${arrival.channel}`, label: CHANNEL_LABELS[arrival.channel], ...channel };
1201
+ return { key: "ch:tagged", label: "Tagged" };
1202
+ }
1203
+
1204
+ // ../shared/dist/payout-unit.js
1205
+ var PAYOUT_UNITS = ["count", "revenue", "margin"];
1206
+ var NEW_PAYOUT_UNIT = "count";
1207
+ function isPayoutUnit(value) {
1208
+ return value === "count" || value === "revenue" || value === "margin";
1209
+ }
1210
+ function isCurrencyUnit(unit) {
1211
+ return unit !== "count";
1212
+ }
1213
+ function rewardValue(unit, supplied) {
1214
+ if (unit === "count")
1215
+ return supplied === void 0 || supplied > 0 ? 1 : 0;
1216
+ return supplied ?? 1;
1217
+ }
1218
+ function payoutUnitOfRewardBasis(rewardBasis) {
1219
+ return rewardBasis === "revenue" || rewardBasis === "margin" ? rewardBasis : "count";
1220
+ }
1221
+ var PAYOUT_UNIT_COPY = {
1222
+ count: {
1223
+ label: "Count conversions",
1224
+ hint: "Each conversion counts as 1. Use this when a conversion has no price, like a booked demo call.",
1225
+ total: "Conversions",
1226
+ perVisitor: "Conversions per visitor",
1227
+ influence: "Influence on conversions"
1228
+ },
1229
+ revenue: {
1230
+ label: "Order value",
1231
+ hint: "Use the order value you send with each conversion.",
1232
+ total: "Total payout",
1233
+ perVisitor: "Revenue per visitor",
1234
+ influence: "Influence on payout"
1235
+ },
1236
+ margin: {
1237
+ label: "Contribution margin",
1238
+ hint: "Revenue minus product and shipping cost, from orders sent to POST /api/orders. A below-cost order counts against its treatment.",
1239
+ total: "Total margin",
1240
+ perVisitor: "Margin per visitor",
1241
+ influence: "Influence on margin"
1242
+ }
1243
+ };
1244
+ var USD = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2 });
1245
+ function formatPayout(unit, value) {
1246
+ return isCurrencyUnit(unit) ? USD.format(value) : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
947
1247
  }
948
1248
 
949
1249
  // src/client.ts
1250
+ var ARRIVAL_KEY = "bandits_arrival";
1251
+ function readStoredArrival(storage) {
1252
+ try {
1253
+ const parsed = JSON.parse(storage?.getItem(ARRIVAL_KEY) ?? "null");
1254
+ if (typeof parsed?.signature !== "string" || typeof parsed?.sessionId !== "string" || typeof parsed?.at !== "number") return null;
1255
+ return parsed;
1256
+ } catch {
1257
+ return null;
1258
+ }
1259
+ }
950
1260
  var BanditsRequestError = class extends Error {
951
1261
  status;
952
1262
  constructor(message, status) {
@@ -979,8 +1289,25 @@ var BanditsClient = class {
979
1289
  ...config
980
1290
  };
981
1291
  this.sessionId = this.getOrCreateSessionId();
982
- this.visitorId = getOrCreateVisitorId(defaultSessionStorage());
1292
+ this.visitorId = config.visitorId ?? getOrCreateVisitorId(defaultSessionStorage());
983
1293
  this.startBatchFlush();
1294
+ if (config.captureArrival !== false) void this.reportArrival();
1295
+ }
1296
+ async reportArrival() {
1297
+ if (typeof location === "undefined" || typeof document === "undefined") return false;
1298
+ const storage = defaultSessionStorage();
1299
+ const touch = touchFromPage({ href: location.href, referrer: document.referrer });
1300
+ const now = Date.now();
1301
+ if (!shouldReportTouch(touch, readStoredArrival(storage), this.sessionId, now)) return false;
1302
+ const response = await this.makeRequest("/arrivals", "POST", { userId: this.visitorId, touch });
1303
+ if (!response.success) return false;
1304
+ const stored = { signature: touchSignature(touch), sessionId: this.sessionId, at: now };
1305
+ try {
1306
+ storage?.setItem(ARRIVAL_KEY, JSON.stringify(stored));
1307
+ } catch {
1308
+ return true;
1309
+ }
1310
+ return true;
984
1311
  }
985
1312
  async getAssignment(experimentId, userId = this.visitorId, context) {
986
1313
  const request = {
@@ -1020,6 +1347,8 @@ var BanditsClient = class {
1020
1347
  metadata: event.metadata,
1021
1348
  sessionId: this.sessionId
1022
1349
  };
1350
+ if (event.cost !== void 0) request.cost = event.cost;
1351
+ if (event.lineItems) request.lineItems = event.lineItems;
1023
1352
  if (opts?.userId) request.userId = opts.userId;
1024
1353
  if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
1025
1354
  if (opts?.immediate) {
@@ -1198,206 +1527,6 @@ var BanditsClient = class {
1198
1527
  }
1199
1528
  };
1200
1529
 
1201
- // src/snapshot-cache.ts
1202
- var STORAGE_KEY = "bandits_snapshot";
1203
- var REPORT_QUEUE_KEY = "bandits_pending_reports";
1204
- var DECISIONS_KEY = "bandits_client_assignments";
1205
- function pickTreatment(treatments, draw) {
1206
- if (treatments.length === 0) return null;
1207
- let cumulative = 0;
1208
- for (const treatment of treatments) {
1209
- cumulative += treatment.pct;
1210
- if (draw < cumulative) return treatment;
1211
- }
1212
- return treatments[treatments.length - 1];
1213
- }
1214
- var DEFAULT_ASSIGNMENT_TTL_SECONDS = 86400;
1215
- function randomId() {
1216
- const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
1217
- let id = "";
1218
- for (let i = 0; i < 24; i++) {
1219
- id += chars.charAt(Math.floor(Math.random() * chars.length));
1220
- }
1221
- return id;
1222
- }
1223
- var SnapshotClient = class {
1224
- config;
1225
- storage;
1226
- now;
1227
- fetchImpl;
1228
- snapshot = null;
1229
- queue = [];
1230
- sessionId;
1231
- inFlight = null;
1232
- constructor(config) {
1233
- this.config = config;
1234
- this.now = config.now ?? (() => Date.now());
1235
- this.fetchImpl = config.fetchImpl ?? ((...args) => fetch(...args));
1236
- this.storage = config.storage ?? (typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
1237
- this.queue = this.readQueue();
1238
- this.sessionId = getOrCreateSessionId({
1239
- storage: this.storage ?? defaultSessionStorage(),
1240
- now: this.now,
1241
- ttlMs: config.sessionTtl
1242
- });
1243
- if (config.inlineSnapshot) {
1244
- this.snapshot = config.inlineSnapshot;
1245
- this.persistSnapshot(config.inlineSnapshot);
1246
- } else {
1247
- this.snapshot = this.readSnapshot();
1248
- }
1249
- }
1250
- getAssignmentSync(experimentId, userId) {
1251
- const snapshot = this.freshSnapshot();
1252
- if (!snapshot) return null;
1253
- const experiment = snapshot.experiments[experimentId];
1254
- if (!experiment || experiment.mode !== "snapshot") return null;
1255
- const key = `${experimentId}:${userId}`;
1256
- const decisions = this.readDecisions();
1257
- const stored = decisions[key];
1258
- if (stored && stored.expiresAt > this.now()) {
1259
- const held = experiment.treatments.find((t) => t.id === stored.treatmentId);
1260
- if (held) {
1261
- return {
1262
- clientAssignmentId: stored.clientAssignmentId,
1263
- experimentId,
1264
- treatmentId: held.id,
1265
- config: held.config
1266
- };
1267
- }
1268
- }
1269
- const treatment = pickTreatment(experiment.treatments, Math.random());
1270
- if (!treatment) return null;
1271
- const clientAssignmentId = randomId();
1272
- const ttlSeconds = experiment.assignmentTtl ?? DEFAULT_ASSIGNMENT_TTL_SECONDS;
1273
- decisions[key] = {
1274
- clientAssignmentId,
1275
- treatmentId: treatment.id,
1276
- expiresAt: this.now() + ttlSeconds * 1e3
1277
- };
1278
- this.writeDecisions(decisions);
1279
- this.queue.push({
1280
- clientAssignmentId,
1281
- experimentId,
1282
- treatmentId: treatment.id,
1283
- userId,
1284
- sessionId: this.sessionId
1285
- });
1286
- this.persistQueue();
1287
- return {
1288
- clientAssignmentId,
1289
- experimentId,
1290
- treatmentId: treatment.id,
1291
- config: treatment.config
1292
- };
1293
- }
1294
- async refresh() {
1295
- const response = await this.fetchImpl(`${this.config.apiUrl}/snapshot`, {
1296
- method: "GET",
1297
- headers: { "x-api-key": this.config.publishableKey }
1298
- });
1299
- if (!response.ok) return;
1300
- const snapshot = await response.json();
1301
- this.snapshot = snapshot;
1302
- this.persistSnapshot(snapshot);
1303
- }
1304
- async flush() {
1305
- if (this.inFlight) return this.inFlight;
1306
- this.inFlight = this.doFlush().finally(() => {
1307
- this.inFlight = null;
1308
- });
1309
- return this.inFlight;
1310
- }
1311
- async doFlush() {
1312
- if (this.queue.length === 0) return;
1313
- const reports = this.queue.slice(0, 1e3);
1314
- try {
1315
- const response = await this.fetchImpl(`${this.config.apiUrl}/assignments/report`, {
1316
- method: "POST",
1317
- headers: {
1318
- "content-type": "application/json",
1319
- "x-api-key": this.config.publishableKey
1320
- },
1321
- body: JSON.stringify({ reports }),
1322
- keepalive: true
1323
- });
1324
- if (!response.ok) return;
1325
- } catch {
1326
- return;
1327
- }
1328
- this.queue = this.queue.slice(reports.length);
1329
- this.persistQueue();
1330
- }
1331
- getSessionId() {
1332
- return this.sessionId;
1333
- }
1334
- pendingReportCount() {
1335
- return this.queue.length;
1336
- }
1337
- freshSnapshot() {
1338
- if (!this.snapshot) return null;
1339
- if (this.snapshot.ttl === 0) return null;
1340
- const ageMs = this.now() - this.snapshot.generatedAt;
1341
- if (ageMs > this.snapshot.ttl * 1e3) return null;
1342
- return this.snapshot;
1343
- }
1344
- readSnapshot() {
1345
- if (!this.storage) return null;
1346
- try {
1347
- const raw = this.storage.getItem(STORAGE_KEY);
1348
- return raw ? JSON.parse(raw) : null;
1349
- } catch {
1350
- return null;
1351
- }
1352
- }
1353
- persistSnapshot(snapshot) {
1354
- if (!this.storage) return;
1355
- try {
1356
- this.storage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
1357
- } catch {
1358
- }
1359
- }
1360
- readQueue() {
1361
- if (!this.storage) return [];
1362
- try {
1363
- const raw = this.storage.getItem(REPORT_QUEUE_KEY);
1364
- return raw ? JSON.parse(raw) : [];
1365
- } catch {
1366
- return [];
1367
- }
1368
- }
1369
- readDecisions() {
1370
- if (!this.storage) return {};
1371
- try {
1372
- const raw = this.storage.getItem(DECISIONS_KEY);
1373
- if (!raw) return {};
1374
- const parsed = JSON.parse(raw);
1375
- const now = this.now();
1376
- const live = {};
1377
- for (const [k, d] of Object.entries(parsed)) {
1378
- if (d && d.expiresAt > now) live[k] = d;
1379
- }
1380
- return live;
1381
- } catch {
1382
- return {};
1383
- }
1384
- }
1385
- writeDecisions(decisions) {
1386
- if (!this.storage) return;
1387
- try {
1388
- this.storage.setItem(DECISIONS_KEY, JSON.stringify(decisions));
1389
- } catch {
1390
- }
1391
- }
1392
- persistQueue() {
1393
- if (!this.storage) return;
1394
- try {
1395
- this.storage.setItem(REPORT_QUEUE_KEY, JSON.stringify(this.queue));
1396
- } catch {
1397
- }
1398
- }
1399
- };
1400
-
1401
1530
  // src/email-links.ts
1402
1531
  var UNTRACKABLE_SCHEME = /^(mailto:|tel:|sms:|javascript:|data:|#)/i;
1403
1532
  var NOTRACK_ATTRIBUTE = "data-bandit-notrack";
@@ -1590,12 +1719,17 @@ async function batchKeyFor(campaignId, recipientIds) {
1590
1719
  return Array.from(new Uint8Array(digest).slice(0, 16)).map((b) => b.toString(16).padStart(2, "0")).join("");
1591
1720
  }
1592
1721
  export {
1722
+ ARRIVAL_CHANNELS,
1723
+ ARRIVAL_TTL_MS,
1593
1724
  BanditAlgorithm,
1594
1725
  BanditsClient as BanditClient,
1595
1726
  BanditsClient,
1596
1727
  BanditsRequestError,
1728
+ CHANNEL_LABELS,
1729
+ CHANNEL_RULES,
1597
1730
  CONTENT_TYPE_BY_KIND,
1598
1731
  ContentType,
1732
+ DECISIONS_COOKIE,
1599
1733
  DEFAULT_HALF_LIFE_MS,
1600
1734
  DEFAULT_SESSION_TTL_MS,
1601
1735
  DEFAULT_WINDOW_MS,
@@ -1604,10 +1738,17 @@ export {
1604
1738
  ForgettingMode,
1605
1739
  FunnelStatus,
1606
1740
  MAX_ALTERNATIVES,
1741
+ MAX_ARRIVAL_VALUE_LENGTH,
1607
1742
  MAX_IMAGE_URL_LENGTH,
1608
1743
  MAX_ORDERING_ITEMS,
1744
+ MAX_RAW_TOUCH_VALUE_LENGTH,
1745
+ MAX_REFERRER_HOST_LENGTH,
1609
1746
  MAX_TEXT_ALTERNATIVE_LENGTH,
1610
1747
  MIN_ORDERING_ITEMS,
1748
+ NEW_PAYOUT_UNIT,
1749
+ PASS_THROUGH_HOSTS,
1750
+ PAYOUT_UNITS,
1751
+ PAYOUT_UNIT_COPY,
1611
1752
  PICK_CHANNEL,
1612
1753
  PICK_MODES,
1613
1754
  PICK_PROTOCOL_VERSION,
@@ -1626,14 +1767,21 @@ export {
1626
1767
  STOREFRONT_KINDS,
1627
1768
  SUGGESTED_SITE_EXPERIMENTS,
1628
1769
  SnapshotClient,
1770
+ TRACKING_PARAM_SOURCES,
1771
+ UNKNOWN_SOURCE_KEY,
1772
+ UTM_PARAMS,
1773
+ VISITOR_COOKIE,
1629
1774
  VISITOR_ID_KEY,
1630
1775
  appendEmailToken,
1776
+ arrivalFromTouch,
1631
1777
  buildSubdomainUrl,
1632
1778
  calculateConfidenceInterval,
1779
+ channelOf,
1633
1780
  extractDocumentSlots,
1634
1781
  extractEmailLinks,
1635
1782
  extractSlotNames,
1636
1783
  extractSubdomain,
1784
+ formatPayout,
1637
1785
  generateId,
1638
1786
  generateSubdomain,
1639
1787
  getContentTypeCategory,
@@ -1642,6 +1790,11 @@ export {
1642
1790
  goalMatchesPage,
1643
1791
  hashRecipient,
1644
1792
  impressionManifest,
1793
+ isArrivalActive,
1794
+ isCurrencyUnit,
1795
+ isDirectTouch,
1796
+ isPassThroughHost,
1797
+ isPayoutUnit,
1645
1798
  isPlacement,
1646
1799
  isShopifyCdnUrl,
1647
1800
  isSlotBinding,
@@ -1652,32 +1805,43 @@ export {
1652
1805
  mintEmailToken,
1653
1806
  moreExperimentsSentence,
1654
1807
  normalizeAlgorithm,
1808
+ normalizeArrivalValue,
1655
1809
  normalizeConversionPath,
1656
1810
  normalizeFunnelGoal,
1811
+ normalizeHost,
1657
1812
  normalizePosterior,
1658
1813
  normalizeSiteTarget,
1659
1814
  normalizeTreatmentWeights,
1660
1815
  parseAlternative,
1816
+ parseDecisions,
1661
1817
  parsePanelMessage,
1662
1818
  parsePickerMessage,
1663
1819
  parseSiteContent,
1664
1820
  parseTreatmentContent,
1821
+ payoutUnitOfRewardBasis,
1665
1822
  pickArm,
1666
1823
  pickEnvelope,
1667
1824
  pickStep,
1668
1825
  pickTreatment,
1669
1826
  platformById,
1827
+ readCookie,
1670
1828
  renderEmail,
1671
1829
  renderEmailDocument,
1672
1830
  renderFromPlan,
1673
1831
  reportPlanImpressions,
1674
1832
  resolveForgetting,
1833
+ rewardValue,
1675
1834
  rewriteEmailLinks,
1676
1835
  sampleCombination,
1677
1836
  sanitizeMetadata,
1837
+ serializeDecisions,
1678
1838
  serializeTreatmentConfig,
1679
1839
  shouldRefreshPlan,
1840
+ shouldReportTouch,
1841
+ sourceOf,
1680
1842
  targetMatchesPage,
1843
+ touchFromPage,
1844
+ touchSignature,
1681
1845
  uniformFor,
1682
1846
  validateTreatmentWeights,
1683
1847
  verifyEmailToken