@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/README.md +1 -1
- package/dist/{decision-cookie-Mb4G_lTe.d.ts → decision-cookie-DgVvhn7c.d.ts} +22 -1
- package/dist/index.d.ts +112 -4
- package/dist/index.js +500 -13
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/package.json +1 -1
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(
|
|
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(
|
|
381
|
-
visit(
|
|
382
|
-
|
|
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(
|
|
423
|
+
function renderEmailDocument(document2, slots) {
|
|
424
424
|
const missingSlots = [];
|
|
425
|
-
const width =
|
|
426
|
-
const background = escapeHtml(
|
|
427
|
-
const subject = resolve(
|
|
428
|
-
const preheader = resolve(
|
|
429
|
-
const body =
|
|
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
|
|
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
|