@runbandit/sdk 3.3.0 → 3.5.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
@@ -361,7 +361,7 @@ function resolve(value, slots, missing) {
361
361
  }
362
362
  return resolved;
363
363
  }
364
- function extractDocumentSlots(document) {
364
+ function extractDocumentSlots(document2) {
365
365
  const names = [];
366
366
  const seen = /* @__PURE__ */ new Set();
367
367
  const visit = (value) => {
@@ -377,9 +377,9 @@ function extractDocumentSlots(document) {
377
377
  else if (typeof value === "object" && value !== null)
378
378
  Object.values(value).forEach(visit);
379
379
  };
380
- visit(document.subject);
381
- visit(document.preheader);
382
- document.blocks.forEach(visit);
380
+ visit(document2.subject);
381
+ visit(document2.preheader);
382
+ document2.blocks.forEach(visit);
383
383
  return names;
384
384
  }
385
385
  function renderBlock(block, slots, missing) {
@@ -420,13 +420,13 @@ function renderBlock(block, slots, missing) {
420
420
  return "";
421
421
  }
422
422
  }
423
- function renderEmailDocument(document, slots) {
423
+ function renderEmailDocument(document2, slots) {
424
424
  const missingSlots = [];
425
- const width = document.contentWidth ?? CONTENT_WIDTH;
426
- const background = escapeHtml(document.backgroundColor ?? "#f4f4f5");
427
- const subject = resolve(document.subject, slots, missingSlots);
428
- const preheader = resolve(document.preheader, slots, missingSlots);
429
- 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");
430
430
  const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
431
431
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
432
432
  <head>
@@ -916,7 +916,369 @@ function platformById(id) {
916
916
  return SITE_PLATFORMS.find((platform) => platform.id === id);
917
917
  }
918
918
 
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"]
1050
+ }
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));
1085
+ }
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 } : {} };
1120
+ }
1121
+ function isDirectTouch(touch) {
1122
+ return !touch.utmSource && !touch.utmMedium && !touch.utmCampaign && !touch.utmContent && !touch.trackingParam && !touch.referrerHost;
1123
+ }
1124
+ function safeUrl(raw) {
1125
+ try {
1126
+ return new URL(raw);
1127
+ } catch {
1128
+ return void 0;
1129
+ }
1130
+ }
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 klaviyoExchangeId = params.get("_kx");
1152
+ if (klaviyoExchangeId)
1153
+ touch.klaviyoExchangeId = klaviyoExchangeId;
1154
+ const referrerHost = normalizeHost(safeUrl(page.referrer)?.hostname);
1155
+ if (referrerHost && referrerHost !== normalizeHost(url.hostname))
1156
+ touch.referrerHost = referrerHost;
1157
+ return touch;
1158
+ }
1159
+ function touchSignature(touch) {
1160
+ return JSON.stringify([
1161
+ touch.utmSource ?? "",
1162
+ touch.utmMedium ?? "",
1163
+ touch.utmCampaign ?? "",
1164
+ touch.utmContent ?? "",
1165
+ touch.trackingParam ?? "",
1166
+ touch.referrerHost ?? "",
1167
+ touch.klaviyoExchangeId ?? ""
1168
+ ]);
1169
+ }
1170
+ function shouldReportTouch(touch, stored, sessionId, now) {
1171
+ if (!stored)
1172
+ return true;
1173
+ if (isDirectTouch(touch))
1174
+ return now - stored.at >= ARRIVAL_TTL_MS;
1175
+ return stored.signature !== touchSignature(touch) || stored.sessionId !== sessionId;
1176
+ }
1177
+ function isArrivalActive(startedAt, at) {
1178
+ return startedAt <= at && at - startedAt < ARRIVAL_TTL_MS;
1179
+ }
1180
+ var CHANNEL_LABELS = {
1181
+ ad: "Ad",
1182
+ email: "Email",
1183
+ social: "Social",
1184
+ search: "Search",
1185
+ referral: "Referral",
1186
+ direct: "Direct"
1187
+ };
1188
+ var UNKNOWN_SOURCE_KEY = "unknown";
1189
+ var titleCase = (value) => value.charAt(0).toUpperCase() + value.slice(1);
1190
+ function sourceOf(arrival) {
1191
+ if (!arrival)
1192
+ return { key: UNKNOWN_SOURCE_KEY, label: "Unknown" };
1193
+ const channel = arrival.channel ? { channel: arrival.channel } : {};
1194
+ if (arrival.source && arrival.campaign) {
1195
+ return { key: `src:${arrival.source}|${arrival.campaign}`, label: `${titleCase(arrival.source)} \xB7 ${arrival.campaign}`, ...channel };
1196
+ }
1197
+ if (arrival.source)
1198
+ return { key: `src:${arrival.source}`, label: titleCase(arrival.source), ...channel };
1199
+ if (arrival.campaign)
1200
+ return { key: `cmp:${arrival.campaign}`, label: arrival.campaign, ...channel };
1201
+ if (arrival.referrerHost)
1202
+ return { key: `ref:${arrival.referrerHost}`, label: arrival.referrerHost, ...channel };
1203
+ if (arrival.channel)
1204
+ return { key: `ch:${arrival.channel}`, label: CHANNEL_LABELS[arrival.channel], ...channel };
1205
+ return { key: "ch:tagged", label: "Tagged" };
1206
+ }
1207
+
1208
+ // ../shared/dist/payout-unit.js
1209
+ var PAYOUT_UNITS = ["count", "revenue", "margin"];
1210
+ var NEW_PAYOUT_UNIT = "count";
1211
+ function isPayoutUnit(value) {
1212
+ return value === "count" || value === "revenue" || value === "margin";
1213
+ }
1214
+ function isCurrencyUnit(unit) {
1215
+ return unit !== "count";
1216
+ }
1217
+ function rewardValue(unit, supplied) {
1218
+ if (unit === "count")
1219
+ return supplied === void 0 || supplied > 0 ? 1 : 0;
1220
+ return supplied ?? 1;
1221
+ }
1222
+ function payoutUnitOfRewardBasis(rewardBasis) {
1223
+ return rewardBasis === "revenue" || rewardBasis === "margin" ? rewardBasis : "count";
1224
+ }
1225
+ var PAYOUT_UNIT_COPY = {
1226
+ count: {
1227
+ label: "Count conversions",
1228
+ hint: "Each conversion counts as 1. Use this when a conversion has no price, like a booked demo call.",
1229
+ total: "Conversions",
1230
+ perVisitor: "Conversions per visitor",
1231
+ influence: "Influence on conversions"
1232
+ },
1233
+ revenue: {
1234
+ label: "Order value",
1235
+ hint: "Use the order value you send with each conversion.",
1236
+ total: "Total payout",
1237
+ perVisitor: "Revenue per visitor",
1238
+ influence: "Influence on payout"
1239
+ },
1240
+ margin: {
1241
+ label: "Contribution margin",
1242
+ hint: "Revenue minus product and shipping cost, from orders sent to POST /api/orders. A below-cost order counts against its treatment.",
1243
+ total: "Total margin",
1244
+ perVisitor: "Margin per visitor",
1245
+ influence: "Influence on margin"
1246
+ }
1247
+ };
1248
+ var USD = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2 });
1249
+ function formatPayout(unit, value) {
1250
+ return isCurrencyUnit(unit) ? USD.format(value) : value.toLocaleString("en-US", { maximumFractionDigits: 2 });
1251
+ }
1252
+
1253
+ // ../shared/dist/trial-rule.js
1254
+ var TRIAL_RULES = ["served", "exposure"];
1255
+ function isTrialRule(value) {
1256
+ return value === "served" || value === "exposure";
1257
+ }
1258
+ function trialRuleFor({ target, requested }) {
1259
+ if (requested)
1260
+ return requested;
1261
+ return target ? "exposure" : "served";
1262
+ }
1263
+ function trialRuleOf(experiment) {
1264
+ return experiment.trialOn ?? "served";
1265
+ }
1266
+ var TRIAL_RULE_COPY = {
1267
+ served: "Counts a trial when served",
1268
+ exposure: "Counts a trial when seen"
1269
+ };
1270
+
919
1271
  // src/client.ts
1272
+ var ARRIVAL_KEY = "bandits_arrival";
1273
+ function readStoredArrival(storage) {
1274
+ try {
1275
+ const parsed = JSON.parse(storage?.getItem(ARRIVAL_KEY) ?? "null");
1276
+ if (typeof parsed?.signature !== "string" || typeof parsed?.sessionId !== "string" || typeof parsed?.at !== "number") return null;
1277
+ return parsed;
1278
+ } catch {
1279
+ return null;
1280
+ }
1281
+ }
920
1282
  var BanditsRequestError = class extends Error {
921
1283
  status;
922
1284
  constructor(message, status) {
@@ -939,6 +1301,8 @@ var BanditsClient = class {
939
1301
  namedEventQueue = [];
940
1302
  sessionId;
941
1303
  visitorId;
1304
+ exposureQueue = [];
1305
+ exposedIds = /* @__PURE__ */ new Set();
942
1306
  flushTimer;
943
1307
  constructor(config) {
944
1308
  this.config = {
@@ -951,6 +1315,23 @@ var BanditsClient = class {
951
1315
  this.sessionId = this.getOrCreateSessionId();
952
1316
  this.visitorId = config.visitorId ?? getOrCreateVisitorId(defaultSessionStorage());
953
1317
  this.startBatchFlush();
1318
+ if (config.captureArrival !== false) void this.reportArrival();
1319
+ }
1320
+ async reportArrival() {
1321
+ if (typeof location === "undefined" || typeof document === "undefined") return false;
1322
+ const storage = defaultSessionStorage();
1323
+ const touch = touchFromPage({ href: location.href, referrer: document.referrer });
1324
+ const now = Date.now();
1325
+ if (!shouldReportTouch(touch, readStoredArrival(storage), this.sessionId, now)) return false;
1326
+ const response = await this.makeRequest("/arrivals", "POST", { userId: this.visitorId, touch });
1327
+ if (!response.success) return false;
1328
+ const stored = { signature: touchSignature(touch), sessionId: this.sessionId, at: now };
1329
+ try {
1330
+ storage?.setItem(ARRIVAL_KEY, JSON.stringify(stored));
1331
+ } catch {
1332
+ return true;
1333
+ }
1334
+ return true;
954
1335
  }
955
1336
  async getAssignment(experimentId, userId = this.visitorId, context) {
956
1337
  const request = {
@@ -979,7 +1360,8 @@ var BanditsClient = class {
979
1360
  experimentId,
980
1361
  userId,
981
1362
  config: assignment.treatment?.config || {},
982
- funnelId: assignment.funnelId
1363
+ funnelId: assignment.funnelId,
1364
+ trialOn: assignment.trialOn
983
1365
  };
984
1366
  }
985
1367
  trackNamedEvent(event, opts) {
@@ -990,6 +1372,8 @@ var BanditsClient = class {
990
1372
  metadata: event.metadata,
991
1373
  sessionId: this.sessionId
992
1374
  };
1375
+ if (event.cost !== void 0) request.cost = event.cost;
1376
+ if (event.lineItems) request.lineItems = event.lineItems;
993
1377
  if (opts?.userId) request.userId = opts.userId;
994
1378
  if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
995
1379
  if (opts?.immediate) {
@@ -1014,6 +1398,12 @@ var BanditsClient = class {
1014
1398
  }
1015
1399
  }
1016
1400
  async flush() {
1401
+ const results = await Promise.allSettled([this.flushNamedEvents(), this.flushExposures()]);
1402
+ for (const result of results) {
1403
+ if (result.status === "rejected") throw result.reason;
1404
+ }
1405
+ }
1406
+ async flushNamedEvents() {
1017
1407
  const namedEvents = [...this.namedEventQueue];
1018
1408
  this.namedEventQueue = [];
1019
1409
  if (namedEvents.length === 0) return;
@@ -1032,14 +1422,75 @@ var BanditsClient = class {
1032
1422
  throw error;
1033
1423
  }
1034
1424
  }
1035
- async identify(anonymousId, identifiedUserId, mergeReason) {
1425
+ async flushExposures() {
1426
+ const exposures = [...this.exposureQueue];
1427
+ this.exposureQueue = [];
1428
+ if (exposures.length === 0) return;
1429
+ try {
1430
+ await fetch(`${this.normalizeApiUrl()}/exposures`, {
1431
+ method: "POST",
1432
+ headers: {
1433
+ "Authorization": `Bearer ${this.config.apiKey}`,
1434
+ "Content-Type": "application/json"
1435
+ },
1436
+ body: JSON.stringify({ exposures }),
1437
+ keepalive: true
1438
+ });
1439
+ } catch (error) {
1440
+ this.exposureQueue.unshift(...exposures);
1441
+ throw error;
1442
+ }
1443
+ }
1444
+ exposed(assignmentId, opts) {
1445
+ if (this.exposedIds.has(assignmentId)) return;
1446
+ this.exposedIds.add(assignmentId);
1447
+ this.exposureQueue.push({ assignmentId, exposedAt: Date.now() });
1448
+ if (opts?.immediate || this.exposureQueue.length >= this.config.batchSize) {
1449
+ this.flushExposures().catch(console.error);
1450
+ }
1451
+ }
1452
+ observeExposure(assignmentId, element, opts) {
1453
+ if (typeof IntersectionObserver === "undefined") return () => {
1454
+ };
1455
+ const threshold = opts?.threshold ?? 0.5;
1456
+ const dwellMs = opts?.dwellMs ?? 1e3;
1457
+ let dwell;
1458
+ const stop = () => {
1459
+ clearTimeout(dwell);
1460
+ dwell = void 0;
1461
+ };
1462
+ const observer = new IntersectionObserver(
1463
+ (entries) => {
1464
+ const entry = entries[entries.length - 1];
1465
+ if (!entry) return;
1466
+ const coversViewport = entry.rootBounds != null && entry.intersectionRect.height >= threshold * entry.rootBounds.height;
1467
+ if (entry.isIntersecting && (entry.intersectionRatio >= threshold || coversViewport)) {
1468
+ dwell ??= setTimeout(() => {
1469
+ this.exposed(assignmentId);
1470
+ disconnect();
1471
+ }, dwellMs);
1472
+ } else {
1473
+ stop();
1474
+ }
1475
+ },
1476
+ { threshold: Array.from({ length: 21 }, (_, i) => i / 20) }
1477
+ );
1478
+ const disconnect = () => {
1479
+ stop();
1480
+ observer.disconnect();
1481
+ };
1482
+ observer.observe(element);
1483
+ return disconnect;
1484
+ }
1485
+ async identify(anonymousId, identifiedUserId, mergeReason, options) {
1036
1486
  const response = await this.makeRequest(
1037
1487
  "/identify",
1038
1488
  "POST",
1039
1489
  {
1040
1490
  anonymousId,
1041
1491
  identifiedUserId,
1042
- mergeReason: mergeReason || "manual"
1492
+ mergeReason: mergeReason || "manual",
1493
+ ...options?.email ? { email: options.email } : {}
1043
1494
  }
1044
1495
  );
1045
1496
  if (!response.success) {
@@ -1053,6 +1504,7 @@ var BanditsClient = class {
1053
1504
  }
1054
1505
  }
1055
1506
  async trackFunnelReward(funnelId, userId, value) {
1507
+ await this.flushExposures().catch(() => void 0);
1056
1508
  const response = await this.makeRequest(
1057
1509
  `/funnels/${funnelId}/reward`,
1058
1510
  "POST",
@@ -1360,10 +1812,14 @@ async function batchKeyFor(campaignId, recipientIds) {
1360
1812
  return Array.from(new Uint8Array(digest).slice(0, 16)).map((b) => b.toString(16).padStart(2, "0")).join("");
1361
1813
  }
1362
1814
  export {
1815
+ ARRIVAL_CHANNELS,
1816
+ ARRIVAL_TTL_MS,
1363
1817
  BanditAlgorithm,
1364
1818
  BanditsClient as BanditClient,
1365
1819
  BanditsClient,
1366
1820
  BanditsRequestError,
1821
+ CHANNEL_LABELS,
1822
+ CHANNEL_RULES,
1367
1823
  CONTENT_TYPE_BY_KIND,
1368
1824
  ContentType,
1369
1825
  DECISIONS_COOKIE,
@@ -1375,10 +1831,17 @@ export {
1375
1831
  ForgettingMode,
1376
1832
  FunnelStatus,
1377
1833
  MAX_ALTERNATIVES,
1834
+ MAX_ARRIVAL_VALUE_LENGTH,
1378
1835
  MAX_IMAGE_URL_LENGTH,
1379
1836
  MAX_ORDERING_ITEMS,
1837
+ MAX_RAW_TOUCH_VALUE_LENGTH,
1838
+ MAX_REFERRER_HOST_LENGTH,
1380
1839
  MAX_TEXT_ALTERNATIVE_LENGTH,
1381
1840
  MIN_ORDERING_ITEMS,
1841
+ NEW_PAYOUT_UNIT,
1842
+ PASS_THROUGH_HOSTS,
1843
+ PAYOUT_UNITS,
1844
+ PAYOUT_UNIT_COPY,
1382
1845
  PICK_CHANNEL,
1383
1846
  PICK_MODES,
1384
1847
  PICK_PROTOCOL_VERSION,
@@ -1397,15 +1860,23 @@ export {
1397
1860
  STOREFRONT_KINDS,
1398
1861
  SUGGESTED_SITE_EXPERIMENTS,
1399
1862
  SnapshotClient,
1863
+ TRACKING_PARAM_SOURCES,
1864
+ TRIAL_RULES,
1865
+ TRIAL_RULE_COPY,
1866
+ UNKNOWN_SOURCE_KEY,
1867
+ UTM_PARAMS,
1400
1868
  VISITOR_COOKIE,
1401
1869
  VISITOR_ID_KEY,
1402
1870
  appendEmailToken,
1871
+ arrivalFromTouch,
1403
1872
  buildSubdomainUrl,
1404
1873
  calculateConfidenceInterval,
1874
+ channelOf,
1405
1875
  extractDocumentSlots,
1406
1876
  extractEmailLinks,
1407
1877
  extractSlotNames,
1408
1878
  extractSubdomain,
1879
+ formatPayout,
1409
1880
  generateId,
1410
1881
  generateSubdomain,
1411
1882
  getContentTypeCategory,
@@ -1414,9 +1885,15 @@ export {
1414
1885
  goalMatchesPage,
1415
1886
  hashRecipient,
1416
1887
  impressionManifest,
1888
+ isArrivalActive,
1889
+ isCurrencyUnit,
1890
+ isDirectTouch,
1891
+ isPassThroughHost,
1892
+ isPayoutUnit,
1417
1893
  isPlacement,
1418
1894
  isShopifyCdnUrl,
1419
1895
  isSlotBinding,
1896
+ isTrialRule,
1420
1897
  isValidEmail,
1421
1898
  isValidSubdomain,
1422
1899
  kindOfPlacement,
@@ -1424,8 +1901,10 @@ export {
1424
1901
  mintEmailToken,
1425
1902
  moreExperimentsSentence,
1426
1903
  normalizeAlgorithm,
1904
+ normalizeArrivalValue,
1427
1905
  normalizeConversionPath,
1428
1906
  normalizeFunnelGoal,
1907
+ normalizeHost,
1429
1908
  normalizePosterior,
1430
1909
  normalizeSiteTarget,
1431
1910
  normalizeTreatmentWeights,
@@ -1435,6 +1914,7 @@ export {
1435
1914
  parsePickerMessage,
1436
1915
  parseSiteContent,
1437
1916
  parseTreatmentContent,
1917
+ payoutUnitOfRewardBasis,
1438
1918
  pickArm,
1439
1919
  pickEnvelope,
1440
1920
  pickStep,
@@ -1446,13 +1926,20 @@ export {
1446
1926
  renderFromPlan,
1447
1927
  reportPlanImpressions,
1448
1928
  resolveForgetting,
1929
+ rewardValue,
1449
1930
  rewriteEmailLinks,
1450
1931
  sampleCombination,
1451
1932
  sanitizeMetadata,
1452
1933
  serializeDecisions,
1453
1934
  serializeTreatmentConfig,
1454
1935
  shouldRefreshPlan,
1936
+ shouldReportTouch,
1937
+ sourceOf,
1455
1938
  targetMatchesPage,
1939
+ touchFromPage,
1940
+ touchSignature,
1941
+ trialRuleFor,
1942
+ trialRuleOf,
1456
1943
  uniformFor,
1457
1944
  validateTreatmentWeights,
1458
1945
  verifyEmailToken