@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/chunk-BG5L4PBC.js +319 -0
- package/dist/chunk-BG5L4PBC.js.map +1 -0
- package/dist/decision-cookie-CNIp56yQ.d.ts +503 -0
- package/dist/index.d.ts +85 -468
- package/dist/index.js +414 -250
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +48 -0
- package/dist/server.js +195 -0
- package/dist/server.js.map +1 -0
- package/package.json +5 -1
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(
|
|
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(
|
|
363
|
-
visit(
|
|
364
|
-
|
|
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(
|
|
423
|
+
function renderEmailDocument(document2, slots) {
|
|
406
424
|
const missingSlots = [];
|
|
407
|
-
const width =
|
|
408
|
-
const background = escapeHtml(
|
|
409
|
-
const subject = resolve(
|
|
410
|
-
const preheader = resolve(
|
|
411
|
-
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");
|
|
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
|
-
//
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
var
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
916
|
-
const
|
|
917
|
-
if (!
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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
|
|
934
|
-
|
|
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
|
-
|
|
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
|
|
1128
|
+
return void 0;
|
|
943
1129
|
}
|
|
944
1130
|
}
|
|
945
|
-
function
|
|
946
|
-
|
|
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
|