@runbandit/sdk 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -3
- package/dist/index.js +819 -747
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,818 +1,886 @@
|
|
|
1
|
-
//
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
// ../shared/dist/types.js
|
|
2
|
+
var ExperimentStatus;
|
|
3
|
+
(function(ExperimentStatus2) {
|
|
4
|
+
ExperimentStatus2["DRAFT"] = "draft";
|
|
5
|
+
ExperimentStatus2["ACTIVE"] = "active";
|
|
6
|
+
ExperimentStatus2["PAUSED"] = "paused";
|
|
7
|
+
ExperimentStatus2["COMPLETED"] = "completed";
|
|
8
|
+
ExperimentStatus2["ARCHIVED"] = "archived";
|
|
9
|
+
})(ExperimentStatus || (ExperimentStatus = {}));
|
|
10
|
+
var BanditAlgorithm;
|
|
11
|
+
(function(BanditAlgorithm2) {
|
|
12
|
+
BanditAlgorithm2["THOMPSON_SAMPLING"] = "thompson_sampling";
|
|
13
|
+
BanditAlgorithm2["CONTEXTUAL_LINEAR"] = "contextual_linear";
|
|
14
|
+
})(BanditAlgorithm || (BanditAlgorithm = {}));
|
|
15
|
+
var Posterior;
|
|
16
|
+
(function(Posterior2) {
|
|
17
|
+
Posterior2["BETA"] = "beta";
|
|
18
|
+
Posterior2["NORMAL_GAMMA"] = "normal_gamma";
|
|
19
|
+
})(Posterior || (Posterior = {}));
|
|
20
|
+
function normalizePosterior(posterior) {
|
|
21
|
+
return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;
|
|
13
22
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
var ForgettingMode;
|
|
24
|
+
(function(ForgettingMode2) {
|
|
25
|
+
ForgettingMode2["NONE"] = "none";
|
|
26
|
+
ForgettingMode2["DISCOUNTED"] = "discounted";
|
|
27
|
+
ForgettingMode2["SLIDING_WINDOW"] = "sliding_window";
|
|
28
|
+
})(ForgettingMode || (ForgettingMode = {}));
|
|
29
|
+
var DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
30
|
+
var DEFAULT_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
31
|
+
var STATIONARY = { mode: ForgettingMode.NONE };
|
|
32
|
+
function resolveForgetting(config) {
|
|
33
|
+
switch (config?.mode) {
|
|
34
|
+
case ForgettingMode.DISCOUNTED:
|
|
35
|
+
return {
|
|
36
|
+
mode: ForgettingMode.DISCOUNTED,
|
|
37
|
+
halfLifeMs: config.halfLifeMs && config.halfLifeMs > 0 ? config.halfLifeMs : DEFAULT_HALF_LIFE_MS
|
|
38
|
+
};
|
|
39
|
+
case ForgettingMode.SLIDING_WINDOW:
|
|
40
|
+
return {
|
|
41
|
+
mode: ForgettingMode.SLIDING_WINDOW,
|
|
42
|
+
windowMs: config.windowMs && config.windowMs > 0 ? config.windowMs : DEFAULT_WINDOW_MS
|
|
43
|
+
};
|
|
44
|
+
default:
|
|
45
|
+
return STATIONARY;
|
|
30
46
|
}
|
|
31
47
|
}
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
var LEGACY_ALGORITHM_ALIASES = {
|
|
49
|
+
epsilon_greedy: BanditAlgorithm.THOMPSON_SAMPLING,
|
|
50
|
+
ucb1: BanditAlgorithm.THOMPSON_SAMPLING,
|
|
51
|
+
contextual_neural: BanditAlgorithm.CONTEXTUAL_LINEAR
|
|
52
|
+
};
|
|
53
|
+
function normalizeAlgorithm(algorithm) {
|
|
54
|
+
if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING)
|
|
55
|
+
return BanditAlgorithm.THOMPSON_SAMPLING;
|
|
56
|
+
if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR)
|
|
57
|
+
return BanditAlgorithm.CONTEXTUAL_LINEAR;
|
|
58
|
+
return LEGACY_ALGORITHM_ALIASES[algorithm ?? ""] ?? BanditAlgorithm.THOMPSON_SAMPLING;
|
|
34
59
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
var ContentType;
|
|
61
|
+
(function(ContentType2) {
|
|
62
|
+
ContentType2["PLAIN_TEXT"] = "plain_text";
|
|
63
|
+
ContentType2["MARKDOWN"] = "markdown";
|
|
64
|
+
ContentType2["HTML"] = "html";
|
|
65
|
+
ContentType2["CSS"] = "css";
|
|
66
|
+
ContentType2["JAVASCRIPT"] = "javascript";
|
|
67
|
+
ContentType2["JSON"] = "json";
|
|
68
|
+
ContentType2["IMAGE"] = "image";
|
|
69
|
+
ContentType2["VIDEO"] = "video";
|
|
70
|
+
ContentType2["AUDIO"] = "audio";
|
|
71
|
+
ContentType2["DOCUMENT"] = "document";
|
|
72
|
+
})(ContentType || (ContentType = {}));
|
|
73
|
+
function getContentTypeCategory(contentType) {
|
|
74
|
+
switch (contentType) {
|
|
75
|
+
case ContentType.PLAIN_TEXT:
|
|
76
|
+
case ContentType.MARKDOWN:
|
|
77
|
+
return "text";
|
|
78
|
+
case ContentType.HTML:
|
|
79
|
+
case ContentType.CSS:
|
|
80
|
+
case ContentType.JAVASCRIPT:
|
|
81
|
+
case ContentType.JSON:
|
|
82
|
+
return "code";
|
|
83
|
+
case ContentType.IMAGE:
|
|
84
|
+
case ContentType.VIDEO:
|
|
85
|
+
case ContentType.AUDIO:
|
|
86
|
+
case ContentType.DOCUMENT:
|
|
87
|
+
return "file";
|
|
40
88
|
}
|
|
41
|
-
return void 0;
|
|
42
89
|
}
|
|
43
|
-
var
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
config: assignment.treatment?.config || {},
|
|
86
|
-
funnelId: assignment.funnelId
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
trackNamedEvent(event, opts) {
|
|
90
|
-
const request = {
|
|
91
|
-
name: event.name,
|
|
92
|
-
category: event.category || "custom",
|
|
93
|
-
value: event.value,
|
|
94
|
-
metadata: event.metadata,
|
|
95
|
-
sessionId: this.sessionId
|
|
96
|
-
};
|
|
97
|
-
if (opts?.userId) request.userId = opts.userId;
|
|
98
|
-
if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
|
|
99
|
-
if (opts?.immediate) {
|
|
100
|
-
const url = `${this.normalizeApiUrl()}/events`;
|
|
101
|
-
try {
|
|
102
|
-
void fetch(url, {
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers: {
|
|
105
|
-
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
106
|
-
"Content-Type": "application/json"
|
|
107
|
-
},
|
|
108
|
-
body: JSON.stringify(request),
|
|
109
|
-
keepalive: true
|
|
110
|
-
});
|
|
111
|
-
} catch {
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
this.namedEventQueue.push(request);
|
|
116
|
-
if (this.namedEventQueue.length >= this.config.batchSize) {
|
|
117
|
-
this.flush();
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async flush() {
|
|
121
|
-
const namedEvents = [...this.namedEventQueue];
|
|
122
|
-
this.namedEventQueue = [];
|
|
123
|
-
if (namedEvents.length === 0) return;
|
|
124
|
-
try {
|
|
125
|
-
await fetch(`${this.normalizeApiUrl()}/events/batch`, {
|
|
126
|
-
method: "POST",
|
|
127
|
-
headers: {
|
|
128
|
-
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
129
|
-
"Content-Type": "application/json"
|
|
130
|
-
},
|
|
131
|
-
body: JSON.stringify({ events: namedEvents }),
|
|
132
|
-
keepalive: true
|
|
133
|
-
});
|
|
134
|
-
} catch (error) {
|
|
135
|
-
this.namedEventQueue.unshift(...namedEvents);
|
|
136
|
-
throw error;
|
|
90
|
+
var FunnelStatus;
|
|
91
|
+
(function(FunnelStatus2) {
|
|
92
|
+
FunnelStatus2["DRAFT"] = "draft";
|
|
93
|
+
FunnelStatus2["ACTIVE"] = "active";
|
|
94
|
+
FunnelStatus2["PAUSED"] = "paused";
|
|
95
|
+
FunnelStatus2["COMPLETED"] = "completed";
|
|
96
|
+
})(FunnelStatus || (FunnelStatus = {}));
|
|
97
|
+
|
|
98
|
+
// ../shared/dist/utils.js
|
|
99
|
+
function validateTreatmentWeights(treatments) {
|
|
100
|
+
const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
|
|
101
|
+
return totalWeight > 0;
|
|
102
|
+
}
|
|
103
|
+
function normalizeTreatmentWeights(treatments) {
|
|
104
|
+
const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
|
|
105
|
+
return treatments.map((treatment) => ({
|
|
106
|
+
...treatment,
|
|
107
|
+
weight: (treatment.weight || 1) / totalWeight
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
function calculateConfidenceInterval(successes, trials, confidence = 0.95) {
|
|
111
|
+
if (trials === 0)
|
|
112
|
+
return { lower: 0, upper: 0 };
|
|
113
|
+
const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;
|
|
114
|
+
const p = successes / trials;
|
|
115
|
+
const margin = z * Math.sqrt(p * (1 - p) / trials);
|
|
116
|
+
return {
|
|
117
|
+
lower: Math.max(0, p - margin),
|
|
118
|
+
upper: Math.min(1, p + margin)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function isValidEmail(email) {
|
|
122
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
123
|
+
return emailRegex.test(email);
|
|
124
|
+
}
|
|
125
|
+
function sanitizeMetadata(metadata) {
|
|
126
|
+
const sanitized = {};
|
|
127
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
128
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
129
|
+
sanitized[key] = value;
|
|
130
|
+
} else if (value !== null && value !== void 0) {
|
|
131
|
+
sanitized[key] = String(value);
|
|
137
132
|
}
|
|
138
133
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
if (!response.success) {
|
|
150
|
-
throw new Error(response.error || "Failed to identify user");
|
|
151
|
-
}
|
|
134
|
+
return sanitized;
|
|
135
|
+
}
|
|
136
|
+
function generateSubdomain(name) {
|
|
137
|
+
return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 63);
|
|
138
|
+
}
|
|
139
|
+
function isValidSubdomain(subdomain) {
|
|
140
|
+
if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {
|
|
141
|
+
return false;
|
|
152
142
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
144
|
+
const reserved = [
|
|
145
|
+
"www",
|
|
146
|
+
"api",
|
|
147
|
+
"app",
|
|
148
|
+
"admin",
|
|
149
|
+
"dashboard",
|
|
150
|
+
"mail",
|
|
151
|
+
"ftp",
|
|
152
|
+
"localhost",
|
|
153
|
+
"staging",
|
|
154
|
+
"dev",
|
|
155
|
+
"test",
|
|
156
|
+
"demo",
|
|
157
|
+
"blog",
|
|
158
|
+
"docs",
|
|
159
|
+
"status",
|
|
160
|
+
"help",
|
|
161
|
+
"support",
|
|
162
|
+
"cdn",
|
|
163
|
+
"static",
|
|
164
|
+
"assets",
|
|
165
|
+
"secure",
|
|
166
|
+
"payment",
|
|
167
|
+
"pay"
|
|
168
|
+
];
|
|
169
|
+
return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);
|
|
170
|
+
}
|
|
171
|
+
function extractSubdomain(hostname, baseDomain) {
|
|
172
|
+
if (!hostname || !baseDomain) {
|
|
173
|
+
return null;
|
|
158
174
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"POST",
|
|
163
|
-
{ userId, value }
|
|
164
|
-
);
|
|
165
|
-
if (!response.success) {
|
|
166
|
-
throw new Error(response.error || "Failed to record funnel reward");
|
|
167
|
-
}
|
|
175
|
+
const cleanHostname = hostname.split(":")[0];
|
|
176
|
+
if (!cleanHostname.endsWith(baseDomain)) {
|
|
177
|
+
return null;
|
|
168
178
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
userId,
|
|
173
|
-
context,
|
|
174
|
-
sessionId: this.sessionId
|
|
175
|
-
};
|
|
176
|
-
const pageUrl = currentPageUrl();
|
|
177
|
-
if (pageUrl) request.pageUrl = pageUrl;
|
|
178
|
-
const response = await this.makeRequest("/bundles/assignment", "POST", request);
|
|
179
|
-
if (!response.success || !response.data) {
|
|
180
|
-
throw new Error(response.error || "Failed to get bundle assignment");
|
|
181
|
-
}
|
|
182
|
-
const payload = response.data?.data ?? response.data;
|
|
183
|
-
if (!payload?.bundleAssignmentId || !payload?.slots) {
|
|
184
|
-
throw new Error("Invalid bundle assignment response");
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
bundleAssignmentId: payload.bundleAssignmentId,
|
|
188
|
-
slots: payload.slots
|
|
189
|
-
};
|
|
179
|
+
const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));
|
|
180
|
+
if (!subdomain || subdomain === "www") {
|
|
181
|
+
return null;
|
|
190
182
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
183
|
+
return subdomain;
|
|
184
|
+
}
|
|
185
|
+
function buildSubdomainUrl(subdomain, baseDomain, path = "", protocol = "https") {
|
|
186
|
+
const url = `${protocol}://${subdomain}.${baseDomain}`;
|
|
187
|
+
return path ? `${url}${path.startsWith("/") ? path : "/" + path}` : url;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ../shared/dist/email-token.js
|
|
191
|
+
var EMAIL_TOKEN_PARAM = "bt";
|
|
192
|
+
var encoder = new TextEncoder();
|
|
193
|
+
function base64UrlEncode(bytes) {
|
|
194
|
+
let binary = "";
|
|
195
|
+
for (const byte of bytes)
|
|
196
|
+
binary += String.fromCharCode(byte);
|
|
197
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
198
|
+
}
|
|
199
|
+
function base64UrlDecode(value) {
|
|
200
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
201
|
+
const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
202
|
+
const bytes = new Uint8Array(binary.length);
|
|
203
|
+
for (let i = 0; i < binary.length; i++)
|
|
204
|
+
bytes[i] = binary.charCodeAt(i);
|
|
205
|
+
return bytes;
|
|
206
|
+
}
|
|
207
|
+
async function hmac(secret, message) {
|
|
208
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
209
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
|
|
210
|
+
}
|
|
211
|
+
function timingSafeEqual(a, b) {
|
|
212
|
+
if (a.length !== b.length)
|
|
213
|
+
return false;
|
|
214
|
+
let diff = 0;
|
|
215
|
+
for (let i = 0; i < a.length; i++)
|
|
216
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
217
|
+
return diff === 0;
|
|
218
|
+
}
|
|
219
|
+
async function hashRecipient(recipientId, companySecret) {
|
|
220
|
+
const digest = await hmac(companySecret, `recipient:${recipientId}`);
|
|
221
|
+
return base64UrlEncode(digest.slice(0, 16));
|
|
222
|
+
}
|
|
223
|
+
async function mintEmailToken(payload, key) {
|
|
224
|
+
const body = base64UrlEncode(encoder.encode(JSON.stringify({
|
|
225
|
+
c: payload.c,
|
|
226
|
+
r: payload.r,
|
|
227
|
+
k: payload.k,
|
|
228
|
+
l: payload.l,
|
|
229
|
+
v: key.version
|
|
230
|
+
})));
|
|
231
|
+
const signature = base64UrlEncode(await hmac(key.secret, body));
|
|
232
|
+
return `${body}.${signature}`;
|
|
233
|
+
}
|
|
234
|
+
async function verifyEmailToken(token, keys) {
|
|
235
|
+
const parts = token.split(".");
|
|
236
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
237
|
+
return { valid: false, reason: "malformed" };
|
|
199
238
|
}
|
|
200
|
-
|
|
201
|
-
|
|
239
|
+
const [body, signature] = parts;
|
|
240
|
+
let payload;
|
|
241
|
+
try {
|
|
242
|
+
payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body)));
|
|
243
|
+
} catch {
|
|
244
|
+
return { valid: false, reason: "malformed" };
|
|
202
245
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!baseUrl.endsWith("/api")) {
|
|
206
|
-
baseUrl = `${baseUrl}/api`;
|
|
207
|
-
}
|
|
208
|
-
return baseUrl;
|
|
246
|
+
if (!payload || typeof payload.c !== "string" || typeof payload.r !== "string" || !Array.isArray(payload.k) || !payload.k.every((arm) => Number.isInteger(arm) && arm >= 0) || !Number.isInteger(payload.l) || payload.l < 0 || !Number.isInteger(payload.v)) {
|
|
247
|
+
return { valid: false, reason: "malformed" };
|
|
209
248
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
249
|
+
const key = keys.find((candidate) => candidate.version === payload.v);
|
|
250
|
+
if (!key)
|
|
251
|
+
return { valid: false, reason: "unknown_key" };
|
|
252
|
+
const expected = base64UrlEncode(await hmac(key.secret, body));
|
|
253
|
+
if (!timingSafeEqual(expected, signature)) {
|
|
254
|
+
return { valid: false, reason: "bad_signature" };
|
|
215
255
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
success: false,
|
|
243
|
-
error: error instanceof Error ? error.message : "Network error",
|
|
244
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
245
|
-
};
|
|
246
|
-
}
|
|
256
|
+
return { valid: true, payload };
|
|
257
|
+
}
|
|
258
|
+
function appendEmailToken(url, token) {
|
|
259
|
+
const hashIndex = url.indexOf("#");
|
|
260
|
+
const fragment = hashIndex === -1 ? "" : url.slice(hashIndex);
|
|
261
|
+
const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
262
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
263
|
+
return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ../shared/dist/email-plan.js
|
|
267
|
+
async function uniformFor(salt, recipientId, slotIndex) {
|
|
268
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
269
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${recipientId}:${slotIndex}`)));
|
|
270
|
+
let value = 0;
|
|
271
|
+
for (let i = 0; i < 6; i++)
|
|
272
|
+
value = value * 256 + mac[i];
|
|
273
|
+
return value / 2 ** 48;
|
|
274
|
+
}
|
|
275
|
+
function pickArm(probabilities, uniform) {
|
|
276
|
+
if (probabilities.length === 0)
|
|
277
|
+
return -1;
|
|
278
|
+
const total = probabilities.reduce((sum, p) => sum + Math.max(p, 0), 0);
|
|
279
|
+
if (total <= 0) {
|
|
280
|
+
return Math.min(probabilities.length - 1, Math.floor(uniform * probabilities.length));
|
|
247
281
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
282
|
+
const target = uniform * total;
|
|
283
|
+
let cumulative = 0;
|
|
284
|
+
for (let i = 0; i < probabilities.length; i++) {
|
|
285
|
+
cumulative += Math.max(probabilities[i], 0);
|
|
286
|
+
if (target < cumulative)
|
|
287
|
+
return i;
|
|
252
288
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
289
|
+
return probabilities.length - 1;
|
|
290
|
+
}
|
|
291
|
+
async function sampleCombination(plan, recipientId) {
|
|
292
|
+
const combination = [];
|
|
293
|
+
for (let i = 0; i < plan.slots.length; i++) {
|
|
294
|
+
const slot = plan.slots[i];
|
|
295
|
+
const uniform = await uniformFor(plan.salt, recipientId, i);
|
|
296
|
+
const chosen = pickArm(slot.probabilities, uniform);
|
|
297
|
+
combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);
|
|
262
298
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// ../shared/dist/types.js
|
|
266
|
-
var ExperimentStatus;
|
|
267
|
-
(function(ExperimentStatus2) {
|
|
268
|
-
ExperimentStatus2["DRAFT"] = "draft";
|
|
269
|
-
ExperimentStatus2["ACTIVE"] = "active";
|
|
270
|
-
ExperimentStatus2["PAUSED"] = "paused";
|
|
271
|
-
ExperimentStatus2["COMPLETED"] = "completed";
|
|
272
|
-
ExperimentStatus2["ARCHIVED"] = "archived";
|
|
273
|
-
})(ExperimentStatus || (ExperimentStatus = {}));
|
|
274
|
-
var BanditAlgorithm;
|
|
275
|
-
(function(BanditAlgorithm2) {
|
|
276
|
-
BanditAlgorithm2["THOMPSON_SAMPLING"] = "thompson_sampling";
|
|
277
|
-
BanditAlgorithm2["CONTEXTUAL_LINEAR"] = "contextual_linear";
|
|
278
|
-
})(BanditAlgorithm || (BanditAlgorithm = {}));
|
|
279
|
-
var Posterior;
|
|
280
|
-
(function(Posterior2) {
|
|
281
|
-
Posterior2["BETA"] = "beta";
|
|
282
|
-
Posterior2["NORMAL_GAMMA"] = "normal_gamma";
|
|
283
|
-
})(Posterior || (Posterior = {}));
|
|
284
|
-
function normalizePosterior(posterior) {
|
|
285
|
-
return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;
|
|
299
|
+
return combination;
|
|
286
300
|
}
|
|
287
|
-
|
|
288
|
-
(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
default:
|
|
309
|
-
return STATIONARY;
|
|
301
|
+
async function impressionManifest(plan, recipientIds) {
|
|
302
|
+
const counts = /* @__PURE__ */ new Map();
|
|
303
|
+
for (const recipientId of recipientIds) {
|
|
304
|
+
for (let i = 0; i < plan.slots.length; i++) {
|
|
305
|
+
const slot = plan.slots[i];
|
|
306
|
+
const uniform = await uniformFor(plan.salt, recipientId, i);
|
|
307
|
+
const chosen = pickArm(slot.probabilities, uniform);
|
|
308
|
+
const arm = slot.arms[chosen === -1 ? 0 : chosen];
|
|
309
|
+
if (!arm)
|
|
310
|
+
continue;
|
|
311
|
+
const key = `${slot.experimentId}:${arm.treatmentId}`;
|
|
312
|
+
const entry = counts.get(key);
|
|
313
|
+
if (entry)
|
|
314
|
+
entry.count++;
|
|
315
|
+
else
|
|
316
|
+
counts.set(key, {
|
|
317
|
+
experimentId: slot.experimentId,
|
|
318
|
+
treatmentId: arm.treatmentId,
|
|
319
|
+
count: 1
|
|
320
|
+
});
|
|
321
|
+
}
|
|
310
322
|
}
|
|
323
|
+
return Array.from(counts.values());
|
|
311
324
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
};
|
|
317
|
-
function normalizeAlgorithm(algorithm) {
|
|
318
|
-
if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING)
|
|
319
|
-
return BanditAlgorithm.THOMPSON_SAMPLING;
|
|
320
|
-
if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR)
|
|
321
|
-
return BanditAlgorithm.CONTEXTUAL_LINEAR;
|
|
322
|
-
return LEGACY_ALGORITHM_ALIASES[algorithm ?? ""] ?? BanditAlgorithm.THOMPSON_SAMPLING;
|
|
325
|
+
|
|
326
|
+
// ../shared/dist/email-blocks.js
|
|
327
|
+
function isSlotBinding(value) {
|
|
328
|
+
return typeof value === "object" && value !== null && typeof value.$slot === "string";
|
|
323
329
|
}
|
|
324
|
-
var
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
function getContentTypeCategory(contentType) {
|
|
338
|
-
switch (contentType) {
|
|
339
|
-
case ContentType.PLAIN_TEXT:
|
|
340
|
-
case ContentType.MARKDOWN:
|
|
341
|
-
return "text";
|
|
342
|
-
case ContentType.HTML:
|
|
343
|
-
case ContentType.CSS:
|
|
344
|
-
case ContentType.JAVASCRIPT:
|
|
345
|
-
case ContentType.JSON:
|
|
346
|
-
return "code";
|
|
347
|
-
case ContentType.IMAGE:
|
|
348
|
-
case ContentType.VIDEO:
|
|
349
|
-
case ContentType.AUDIO:
|
|
350
|
-
case ContentType.DOCUMENT:
|
|
351
|
-
return "file";
|
|
330
|
+
var CONTENT_WIDTH = 600;
|
|
331
|
+
function escapeHtml(value) {
|
|
332
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
333
|
+
}
|
|
334
|
+
function resolve(value, slots, missing) {
|
|
335
|
+
if (value === void 0)
|
|
336
|
+
return "";
|
|
337
|
+
if (!isSlotBinding(value))
|
|
338
|
+
return value;
|
|
339
|
+
const resolved = slots[value.$slot];
|
|
340
|
+
if (resolved === void 0) {
|
|
341
|
+
missing.push(value.$slot);
|
|
342
|
+
return "";
|
|
352
343
|
}
|
|
344
|
+
return resolved;
|
|
353
345
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
346
|
+
function extractDocumentSlots(document) {
|
|
347
|
+
const names = [];
|
|
348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
349
|
+
const visit = (value) => {
|
|
350
|
+
if (isSlotBinding(value)) {
|
|
351
|
+
if (!seen.has(value.$slot)) {
|
|
352
|
+
seen.add(value.$slot);
|
|
353
|
+
names.push(value.$slot);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (Array.isArray(value))
|
|
358
|
+
value.forEach(visit);
|
|
359
|
+
else if (typeof value === "object" && value !== null)
|
|
360
|
+
Object.values(value).forEach(visit);
|
|
361
|
+
};
|
|
362
|
+
visit(document.subject);
|
|
363
|
+
visit(document.preheader);
|
|
364
|
+
document.blocks.forEach(visit);
|
|
365
|
+
return names;
|
|
365
366
|
}
|
|
366
|
-
function
|
|
367
|
-
const
|
|
368
|
-
|
|
367
|
+
function renderBlock(block, slots, missing) {
|
|
368
|
+
const align = block.align ?? "left";
|
|
369
|
+
switch (block.type) {
|
|
370
|
+
case "heading": {
|
|
371
|
+
const size = block.level === 3 ? 18 : block.level === 2 ? 22 : 28;
|
|
372
|
+
const color = escapeHtml(resolve(block.color, slots, missing) || "#111111");
|
|
373
|
+
return `<tr><td style="padding:12px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:${size}px;line-height:1.3;font-weight:bold;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
|
|
374
|
+
}
|
|
375
|
+
case "text": {
|
|
376
|
+
const color = escapeHtml(resolve(block.color, slots, missing) || "#333333");
|
|
377
|
+
return `<tr><td style="padding:8px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
|
|
378
|
+
}
|
|
379
|
+
case "button": {
|
|
380
|
+
const href = escapeHtml(resolve(block.href, slots, missing));
|
|
381
|
+
const label = escapeHtml(resolve(block.text, slots, missing));
|
|
382
|
+
const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || "#1a73e8");
|
|
383
|
+
const fg = escapeHtml(resolve(block.textColor, slots, missing) || "#ffffff");
|
|
384
|
+
return `<tr><td style="padding:16px 24px;text-align:${align}">
|
|
385
|
+
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:220px;" arcsize="12%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${fg};font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">${label}</center></v:roundrect><![endif]-->
|
|
386
|
+
<!--[if !mso]><!--><a href="${href}" style="display:inline-block;padding:13px 28px;background-color:${bg};color:${fg};font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;text-decoration:none;border-radius:6px">${label}</a><!--<![endif]-->
|
|
387
|
+
</td></tr>`;
|
|
388
|
+
}
|
|
389
|
+
case "image": {
|
|
390
|
+
const src = escapeHtml(resolve(block.src, slots, missing));
|
|
391
|
+
const alt = escapeHtml(resolve(block.alt, slots, missing));
|
|
392
|
+
const width = block.width ?? CONTENT_WIDTH;
|
|
393
|
+
const img = `<img src="${src}" alt="${alt}" width="${width}" style="display:block;border:0;outline:none;text-decoration:none;max-width:100%;height:auto" />`;
|
|
394
|
+
const href = block.href ? escapeHtml(resolve(block.href, slots, missing)) : "";
|
|
395
|
+
return `<tr><td style="padding:0;text-align:center">${href ? `<a href="${href}">${img}</a>` : img}</td></tr>`;
|
|
396
|
+
}
|
|
397
|
+
case "divider":
|
|
398
|
+
return `<tr><td style="padding:12px 24px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:1px solid ${escapeHtml(block.color ?? "#e5e5e5")};font-size:0;line-height:0"> </td></tr></table></td></tr>`;
|
|
399
|
+
case "spacer":
|
|
400
|
+
return `<tr><td style="height:${block.height ?? 24}px;font-size:0;line-height:0"> </td></tr>`;
|
|
401
|
+
default:
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
369
404
|
}
|
|
370
|
-
function
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
405
|
+
function renderEmailDocument(document, slots) {
|
|
406
|
+
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");
|
|
412
|
+
const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
413
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
414
|
+
<head>
|
|
415
|
+
<meta charset="utf-8" />
|
|
416
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
417
|
+
<meta name="x-apple-disable-message-reformatting" />
|
|
418
|
+
<title>${escapeHtml(subject)}</title>
|
|
419
|
+
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
|
|
420
|
+
</head>
|
|
421
|
+
<body style="margin:0;padding:0;background-color:${background}">
|
|
422
|
+
<div style="display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all">${escapeHtml(preheader)}</div>
|
|
423
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:${background}">
|
|
424
|
+
<tr><td align="center" style="padding:24px 12px">
|
|
425
|
+
<table role="presentation" width="${width}" cellpadding="0" cellspacing="0" border="0" style="width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px">
|
|
426
|
+
${body}
|
|
427
|
+
</table>
|
|
428
|
+
</td></tr>
|
|
429
|
+
</table>
|
|
430
|
+
</body>
|
|
431
|
+
</html>`;
|
|
432
|
+
return { subject, html, missingSlots };
|
|
376
433
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
434
|
+
|
|
435
|
+
// ../shared/dist/storefront-content.js
|
|
436
|
+
var STOREFRONT_KINDS = ["text", "image", "ordering", "offer"];
|
|
437
|
+
var PLACEMENTS_BY_KIND = {
|
|
438
|
+
text: ["hero_headline", "hero_subhead", "cta_primary", "banner_text", "badge_text"],
|
|
439
|
+
image: ["hero_image", "banner_image", "feature_image"],
|
|
440
|
+
ordering: ["product_list_primary", "product_list_secondary"],
|
|
441
|
+
offer: ["offer_banner"]
|
|
442
|
+
};
|
|
443
|
+
var PLACEMENTS = STOREFRONT_KINDS.flatMap((kind) => PLACEMENTS_BY_KIND[kind]);
|
|
444
|
+
function isPlacement(value) {
|
|
445
|
+
return typeof value === "string" && PLACEMENTS.includes(value);
|
|
387
446
|
}
|
|
388
|
-
function
|
|
389
|
-
const
|
|
390
|
-
|
|
447
|
+
function kindOfPlacement(placement) {
|
|
448
|
+
const found = STOREFRONT_KINDS.find((kind) => PLACEMENTS_BY_KIND[kind].includes(placement));
|
|
449
|
+
if (!found)
|
|
450
|
+
throw new Error(`Unknown placement: ${placement}`);
|
|
451
|
+
return found;
|
|
391
452
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
453
|
+
var CONTENT_TYPE_BY_KIND = {
|
|
454
|
+
text: { contentType: "TEXT", contentSubtype: "plain_text" },
|
|
455
|
+
image: { contentType: "FILE", contentSubtype: "image" },
|
|
456
|
+
ordering: { contentType: "CODE", contentSubtype: "json" },
|
|
457
|
+
offer: { contentType: "CODE", contentSubtype: "json" }
|
|
458
|
+
};
|
|
459
|
+
var MIN_ORDERING_ITEMS = 2;
|
|
460
|
+
var MAX_ORDERING_ITEMS = 12;
|
|
461
|
+
function isShopifyCdnUrl(value) {
|
|
462
|
+
let url;
|
|
463
|
+
try {
|
|
464
|
+
url = new URL(value);
|
|
465
|
+
} catch {
|
|
466
|
+
return false;
|
|
400
467
|
}
|
|
401
|
-
return
|
|
468
|
+
return url.protocol === "https:" && url.hostname === "cdn.shopify.com";
|
|
402
469
|
}
|
|
403
|
-
function
|
|
404
|
-
return
|
|
470
|
+
function isRecord(value) {
|
|
471
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
405
472
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
411
|
-
const reserved = [
|
|
412
|
-
"www",
|
|
413
|
-
"api",
|
|
414
|
-
"app",
|
|
415
|
-
"admin",
|
|
416
|
-
"dashboard",
|
|
417
|
-
"mail",
|
|
418
|
-
"ftp",
|
|
419
|
-
"localhost",
|
|
420
|
-
"staging",
|
|
421
|
-
"dev",
|
|
422
|
-
"test",
|
|
423
|
-
"demo",
|
|
424
|
-
"blog",
|
|
425
|
-
"docs",
|
|
426
|
-
"status",
|
|
427
|
-
"help",
|
|
428
|
-
"support",
|
|
429
|
-
"cdn",
|
|
430
|
-
"static",
|
|
431
|
-
"assets",
|
|
432
|
-
"secure",
|
|
433
|
-
"payment",
|
|
434
|
-
"pay"
|
|
435
|
-
];
|
|
436
|
-
return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);
|
|
473
|
+
function isNonEmptyString(value) {
|
|
474
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
437
475
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
476
|
+
function isPositiveInt(value) {
|
|
477
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
478
|
+
}
|
|
479
|
+
function parseImage(raw) {
|
|
480
|
+
if (!isRecord(raw))
|
|
440
481
|
return null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (!cleanHostname.endsWith(baseDomain)) {
|
|
482
|
+
const { url, alt, width, height, sourceId } = raw;
|
|
483
|
+
if (typeof url !== "string" || !isShopifyCdnUrl(url))
|
|
444
484
|
return null;
|
|
445
|
-
|
|
446
|
-
const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));
|
|
447
|
-
if (!subdomain || subdomain === "www") {
|
|
485
|
+
if (typeof alt !== "string")
|
|
448
486
|
return null;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return path ? `${url}${path.startsWith("/") ? path : "/" + path}` : url;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ../shared/dist/email-token.js
|
|
458
|
-
var EMAIL_TOKEN_PARAM = "bt";
|
|
459
|
-
var encoder = new TextEncoder();
|
|
460
|
-
function base64UrlEncode(bytes) {
|
|
461
|
-
let binary = "";
|
|
462
|
-
for (const byte of bytes)
|
|
463
|
-
binary += String.fromCharCode(byte);
|
|
464
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
465
|
-
}
|
|
466
|
-
function base64UrlDecode(value) {
|
|
467
|
-
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
468
|
-
const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
469
|
-
const bytes = new Uint8Array(binary.length);
|
|
470
|
-
for (let i = 0; i < binary.length; i++)
|
|
471
|
-
bytes[i] = binary.charCodeAt(i);
|
|
472
|
-
return bytes;
|
|
473
|
-
}
|
|
474
|
-
async function hmac(secret, message) {
|
|
475
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
476
|
-
return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
|
|
477
|
-
}
|
|
478
|
-
function timingSafeEqual(a, b) {
|
|
479
|
-
if (a.length !== b.length)
|
|
480
|
-
return false;
|
|
481
|
-
let diff = 0;
|
|
482
|
-
for (let i = 0; i < a.length; i++)
|
|
483
|
-
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
484
|
-
return diff === 0;
|
|
487
|
+
if (!isPositiveInt(width) || !isPositiveInt(height))
|
|
488
|
+
return null;
|
|
489
|
+
if (!isNonEmptyString(sourceId))
|
|
490
|
+
return null;
|
|
491
|
+
return { url, alt, width, height, sourceId };
|
|
485
492
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
493
|
+
function parseOrdering(raw) {
|
|
494
|
+
if (!Array.isArray(raw))
|
|
495
|
+
return null;
|
|
496
|
+
if (raw.length < MIN_ORDERING_ITEMS || raw.length > MAX_ORDERING_ITEMS)
|
|
497
|
+
return null;
|
|
498
|
+
if (!raw.every((id) => typeof id === "string" && /^\d+$/.test(id)))
|
|
499
|
+
return null;
|
|
500
|
+
if (new Set(raw).size !== raw.length)
|
|
501
|
+
return null;
|
|
502
|
+
return raw;
|
|
489
503
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
504
|
+
function parseOffer(raw) {
|
|
505
|
+
if (raw === null)
|
|
506
|
+
return null;
|
|
507
|
+
if (!isRecord(raw))
|
|
508
|
+
return void 0;
|
|
509
|
+
const { discountId, code, headline, subtext } = raw;
|
|
510
|
+
if (!isNonEmptyString(discountId) || !isNonEmptyString(code))
|
|
511
|
+
return void 0;
|
|
512
|
+
if (typeof headline !== "string" || typeof subtext !== "string")
|
|
513
|
+
return void 0;
|
|
514
|
+
return { discountId, code, headline, subtext };
|
|
500
515
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (!timingSafeEqual(expected, signature)) {
|
|
521
|
-
return { valid: false, reason: "bad_signature" };
|
|
516
|
+
function parseTreatmentContent(kind, config) {
|
|
517
|
+
if (!isRecord(config))
|
|
518
|
+
return null;
|
|
519
|
+
const content = config.content;
|
|
520
|
+
switch (kind) {
|
|
521
|
+
case "text":
|
|
522
|
+
return isNonEmptyString(content) ? { kind, text: content } : null;
|
|
523
|
+
case "image": {
|
|
524
|
+
const image = parseImage(content);
|
|
525
|
+
return image ? { kind, image } : null;
|
|
526
|
+
}
|
|
527
|
+
case "ordering": {
|
|
528
|
+
const productIds = parseOrdering(content);
|
|
529
|
+
return productIds ? { kind, productIds } : null;
|
|
530
|
+
}
|
|
531
|
+
case "offer": {
|
|
532
|
+
const offer = parseOffer(content);
|
|
533
|
+
return offer === void 0 ? null : { kind, offer };
|
|
534
|
+
}
|
|
522
535
|
}
|
|
523
|
-
return { valid: true, payload };
|
|
524
|
-
}
|
|
525
|
-
function appendEmailToken(url, token) {
|
|
526
|
-
const hashIndex = url.indexOf("#");
|
|
527
|
-
const fragment = hashIndex === -1 ? "" : url.slice(hashIndex);
|
|
528
|
-
const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
529
|
-
const separator = base.includes("?") ? "&" : "?";
|
|
530
|
-
return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;
|
|
531
536
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
537
|
+
function serializeTreatmentConfig(content) {
|
|
538
|
+
switch (content.kind) {
|
|
539
|
+
case "text":
|
|
540
|
+
return { content: content.text };
|
|
541
|
+
case "image":
|
|
542
|
+
return { content: content.image };
|
|
543
|
+
case "ordering":
|
|
544
|
+
return { content: content.productIds };
|
|
545
|
+
case "offer":
|
|
546
|
+
return { content: content.offer };
|
|
547
|
+
}
|
|
541
548
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
549
|
+
|
|
550
|
+
// ../shared/dist/conversion-path.js
|
|
551
|
+
var SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
552
|
+
var isWildcard = (segment) => segment === "*" || segment === "**";
|
|
553
|
+
function segmentsOf(path) {
|
|
554
|
+
return path.split("/").filter(Boolean);
|
|
555
|
+
}
|
|
556
|
+
function normalizeConversionPath(input) {
|
|
557
|
+
const trimmed = input?.trim();
|
|
558
|
+
if (!trimmed)
|
|
559
|
+
return void 0;
|
|
560
|
+
if (/[?#]/.test(trimmed)) {
|
|
561
|
+
throw new Error("Leave out the query string: enter a path like /thank-you or /orders/*/thanks");
|
|
548
562
|
}
|
|
549
|
-
const
|
|
550
|
-
let
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
563
|
+
const withScheme = SCHEME.test(trimmed) ? trimmed : `https://placeholder.invalid/${trimmed.replace(/^\/+/, "")}`;
|
|
564
|
+
let pathname;
|
|
565
|
+
try {
|
|
566
|
+
pathname = new URL(withScheme).pathname;
|
|
567
|
+
} catch {
|
|
568
|
+
throw new Error("Enter the conversion page as a path like /thank-you");
|
|
555
569
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const combination = [];
|
|
560
|
-
for (let i = 0; i < plan.slots.length; i++) {
|
|
561
|
-
const slot = plan.slots[i];
|
|
562
|
-
const uniform = await uniformFor(plan.salt, recipientId, i);
|
|
563
|
-
const chosen = pickArm(slot.probabilities, uniform);
|
|
564
|
-
combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);
|
|
570
|
+
const segments = segmentsOf(pathname);
|
|
571
|
+
if (segments.some((segment) => segment.includes("*") && !isWildcard(segment))) {
|
|
572
|
+
throw new Error("Use * for a whole path segment, like /orders/*/thanks");
|
|
565
573
|
}
|
|
566
|
-
|
|
574
|
+
if (segments.length > 0 && segments.every(isWildcard)) {
|
|
575
|
+
throw new Error("That pattern matches every page. Add a fixed part, like /orders/*/thanks");
|
|
576
|
+
}
|
|
577
|
+
return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
567
578
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (!arm)
|
|
577
|
-
continue;
|
|
578
|
-
const key = `${slot.experimentId}:${arm.treatmentId}`;
|
|
579
|
-
const entry = counts.get(key);
|
|
580
|
-
if (entry)
|
|
581
|
-
entry.count++;
|
|
582
|
-
else
|
|
583
|
-
counts.set(key, {
|
|
584
|
-
experimentId: slot.experimentId,
|
|
585
|
-
treatmentId: arm.treatmentId,
|
|
586
|
-
count: 1
|
|
587
|
-
});
|
|
579
|
+
function matchSegments(pattern, path) {
|
|
580
|
+
if (pattern.length === 0)
|
|
581
|
+
return path.length === 0;
|
|
582
|
+
const [head, ...rest] = pattern;
|
|
583
|
+
if (head === "**") {
|
|
584
|
+
for (let skip = 0; skip <= path.length; skip++) {
|
|
585
|
+
if (matchSegments(rest, path.slice(skip)))
|
|
586
|
+
return true;
|
|
588
587
|
}
|
|
588
|
+
return false;
|
|
589
589
|
}
|
|
590
|
-
|
|
590
|
+
if (path.length === 0)
|
|
591
|
+
return false;
|
|
592
|
+
return (head === "*" || head === path[0]) && matchSegments(rest, path.slice(1));
|
|
593
|
+
}
|
|
594
|
+
function matchesConversionPath(pattern, pathname) {
|
|
595
|
+
return matchSegments(segmentsOf(pattern), segmentsOf(pathname));
|
|
591
596
|
}
|
|
592
597
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
598
|
+
// src/session.ts
|
|
599
|
+
var SESSION_ID_KEY = "bandits_session_id";
|
|
600
|
+
var SESSION_CREATED_AT_KEY = "bandits_session_created_at";
|
|
601
|
+
var VISITOR_ID_KEY = "bandits_visitor_id";
|
|
602
|
+
var DEFAULT_SESSION_TTL_MS = 864e5;
|
|
603
|
+
function generateId() {
|
|
604
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
605
|
+
return crypto.randomUUID();
|
|
606
|
+
}
|
|
607
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
608
|
+
const r = Math.random() * 16 | 0;
|
|
609
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
610
|
+
});
|
|
596
611
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
612
|
+
function getOrCreateSessionId(options = {}) {
|
|
613
|
+
const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;
|
|
614
|
+
if (!storage) return generateId();
|
|
615
|
+
try {
|
|
616
|
+
const stored = storage.getItem(SESSION_ID_KEY);
|
|
617
|
+
const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);
|
|
618
|
+
if (stored && storedAt) {
|
|
619
|
+
const age = now() - parseInt(storedAt, 10);
|
|
620
|
+
if (age < ttlMs) return stored;
|
|
621
|
+
}
|
|
622
|
+
const id = generateId();
|
|
623
|
+
storage.setItem(SESSION_ID_KEY, id);
|
|
624
|
+
storage.setItem(SESSION_CREATED_AT_KEY, String(now()));
|
|
625
|
+
return id;
|
|
626
|
+
} catch {
|
|
627
|
+
return generateId();
|
|
628
|
+
}
|
|
600
629
|
}
|
|
601
|
-
function
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
return
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
630
|
+
function getOrCreateVisitorId(storage) {
|
|
631
|
+
if (!storage) return generateId();
|
|
632
|
+
try {
|
|
633
|
+
const stored = storage.getItem(VISITOR_ID_KEY);
|
|
634
|
+
if (stored) return stored;
|
|
635
|
+
const id = generateId();
|
|
636
|
+
storage.setItem(VISITOR_ID_KEY, id);
|
|
637
|
+
return id;
|
|
638
|
+
} catch {
|
|
639
|
+
return generateId();
|
|
610
640
|
}
|
|
611
|
-
return resolved;
|
|
612
641
|
}
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
642
|
+
function defaultSessionStorage() {
|
|
643
|
+
return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/client.ts
|
|
647
|
+
function currentPageUrl() {
|
|
648
|
+
if (typeof location !== "undefined" && location.origin && location.pathname) {
|
|
649
|
+
return location.origin + location.pathname;
|
|
650
|
+
}
|
|
651
|
+
return void 0;
|
|
652
|
+
}
|
|
653
|
+
var BanditsClient = class {
|
|
654
|
+
config;
|
|
655
|
+
namedEventQueue = [];
|
|
656
|
+
sessionId;
|
|
657
|
+
visitorId;
|
|
658
|
+
flushTimer;
|
|
659
|
+
constructor(config) {
|
|
660
|
+
this.config = {
|
|
661
|
+
timeout: 5e3,
|
|
662
|
+
batchSize: 100,
|
|
663
|
+
flushInterval: 1e4,
|
|
664
|
+
sessionTtl: 864e5,
|
|
665
|
+
...config
|
|
666
|
+
};
|
|
667
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
668
|
+
this.visitorId = getOrCreateVisitorId(defaultSessionStorage());
|
|
669
|
+
this.startBatchFlush();
|
|
670
|
+
}
|
|
671
|
+
async getAssignment(experimentId, userId = this.visitorId, context) {
|
|
672
|
+
const request = {
|
|
673
|
+
experimentId,
|
|
674
|
+
userId,
|
|
675
|
+
context,
|
|
676
|
+
sessionId: this.sessionId
|
|
677
|
+
};
|
|
678
|
+
const pageUrl = currentPageUrl();
|
|
679
|
+
if (pageUrl) request.pageUrl = pageUrl;
|
|
680
|
+
const response = await this.makeRequest(
|
|
681
|
+
"/assignments",
|
|
682
|
+
"POST",
|
|
683
|
+
request
|
|
684
|
+
);
|
|
685
|
+
if (!response.success || !response.data) {
|
|
686
|
+
throw new Error(response.error || "Failed to get assignment");
|
|
687
|
+
}
|
|
688
|
+
const assignment = response.data?.data ?? response.data;
|
|
689
|
+
if (!assignment?.assignmentId) {
|
|
690
|
+
throw new Error("Invalid assignment response");
|
|
691
|
+
}
|
|
692
|
+
return {
|
|
693
|
+
assignmentId: assignment.assignmentId,
|
|
694
|
+
treatmentId: assignment.treatmentId,
|
|
695
|
+
experimentId,
|
|
696
|
+
userId,
|
|
697
|
+
config: assignment.treatment?.config || {},
|
|
698
|
+
funnelId: assignment.funnelId
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
trackNamedEvent(event, opts) {
|
|
702
|
+
const request = {
|
|
703
|
+
name: event.name,
|
|
704
|
+
category: event.category || "custom",
|
|
705
|
+
value: event.value,
|
|
706
|
+
metadata: event.metadata,
|
|
707
|
+
sessionId: this.sessionId
|
|
708
|
+
};
|
|
709
|
+
if (opts?.userId) request.userId = opts.userId;
|
|
710
|
+
if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
|
|
711
|
+
if (opts?.immediate) {
|
|
712
|
+
const url = `${this.normalizeApiUrl()}/events`;
|
|
713
|
+
try {
|
|
714
|
+
void fetch(url, {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: {
|
|
717
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
718
|
+
"Content-Type": "application/json"
|
|
719
|
+
},
|
|
720
|
+
body: JSON.stringify(request),
|
|
721
|
+
keepalive: true
|
|
722
|
+
});
|
|
723
|
+
} catch {
|
|
621
724
|
}
|
|
622
725
|
return;
|
|
623
726
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
727
|
+
this.namedEventQueue.push(request);
|
|
728
|
+
if (this.namedEventQueue.length >= this.config.batchSize) {
|
|
729
|
+
this.flush();
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
async flush() {
|
|
733
|
+
const namedEvents = [...this.namedEventQueue];
|
|
734
|
+
this.namedEventQueue = [];
|
|
735
|
+
if (namedEvents.length === 0) return;
|
|
736
|
+
try {
|
|
737
|
+
await fetch(`${this.normalizeApiUrl()}/events/batch`, {
|
|
738
|
+
method: "POST",
|
|
739
|
+
headers: {
|
|
740
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
741
|
+
"Content-Type": "application/json"
|
|
742
|
+
},
|
|
743
|
+
body: JSON.stringify({ events: namedEvents }),
|
|
744
|
+
keepalive: true
|
|
745
|
+
});
|
|
746
|
+
} catch (error) {
|
|
747
|
+
this.namedEventQueue.unshift(...namedEvents);
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async identify(anonymousId, identifiedUserId, mergeReason) {
|
|
752
|
+
const response = await this.makeRequest(
|
|
753
|
+
"/identify",
|
|
754
|
+
"POST",
|
|
755
|
+
{
|
|
756
|
+
anonymousId,
|
|
757
|
+
identifiedUserId,
|
|
758
|
+
mergeReason: mergeReason || "manual"
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
if (!response.success) {
|
|
762
|
+
throw new Error(response.error || "Failed to identify user");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async label(userId, labels) {
|
|
766
|
+
const response = await this.makeRequest("/labels", "POST", { userId, labels });
|
|
767
|
+
if (!response.success) {
|
|
768
|
+
throw new Error(response.error || "Failed to record labels");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async trackFunnelReward(funnelId, userId, value) {
|
|
772
|
+
const response = await this.makeRequest(
|
|
773
|
+
`/funnels/${funnelId}/reward`,
|
|
774
|
+
"POST",
|
|
775
|
+
{ userId, value }
|
|
776
|
+
);
|
|
777
|
+
if (!response.success) {
|
|
778
|
+
throw new Error(response.error || "Failed to record funnel reward");
|
|
641
779
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
780
|
+
}
|
|
781
|
+
convert(funnelId, value = 1) {
|
|
782
|
+
return this.trackFunnelReward(funnelId, this.visitorId, value);
|
|
783
|
+
}
|
|
784
|
+
async convertOn(funnelId, pathPattern, value = 1) {
|
|
785
|
+
if (typeof location === "undefined" || !matchesConversionPath(pathPattern, location.pathname)) return false;
|
|
786
|
+
await this.convert(funnelId, value);
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
async getBundle(bundleId, userId, context) {
|
|
790
|
+
const request = {
|
|
791
|
+
bundleId,
|
|
792
|
+
userId,
|
|
793
|
+
context,
|
|
794
|
+
sessionId: this.sessionId
|
|
795
|
+
};
|
|
796
|
+
const pageUrl = currentPageUrl();
|
|
797
|
+
if (pageUrl) request.pageUrl = pageUrl;
|
|
798
|
+
const response = await this.makeRequest("/bundles/assignment", "POST", request);
|
|
799
|
+
if (!response.success || !response.data) {
|
|
800
|
+
throw new Error(response.error || "Failed to get bundle assignment");
|
|
645
801
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || "#1a73e8");
|
|
650
|
-
const fg = escapeHtml(resolve(block.textColor, slots, missing) || "#ffffff");
|
|
651
|
-
return `<tr><td style="padding:16px 24px;text-align:${align}">
|
|
652
|
-
<!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:220px;" arcsize="12%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${fg};font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">${label}</center></v:roundrect><![endif]-->
|
|
653
|
-
<!--[if !mso]><!--><a href="${href}" style="display:inline-block;padding:13px 28px;background-color:${bg};color:${fg};font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;text-decoration:none;border-radius:6px">${label}</a><!--<![endif]-->
|
|
654
|
-
</td></tr>`;
|
|
802
|
+
const payload = response.data?.data ?? response.data;
|
|
803
|
+
if (!payload?.bundleAssignmentId || !payload?.slots) {
|
|
804
|
+
throw new Error("Invalid bundle assignment response");
|
|
655
805
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
806
|
+
return {
|
|
807
|
+
bundleAssignmentId: payload.bundleAssignmentId,
|
|
808
|
+
slots: payload.slots
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
async trackBundleReward(bundleId, userId, value) {
|
|
812
|
+
const response = await this.makeRequest(`/bundles/${bundleId}/reward`, "POST", {
|
|
813
|
+
userId,
|
|
814
|
+
value
|
|
815
|
+
});
|
|
816
|
+
if (!response.success) {
|
|
817
|
+
throw new Error(response.error || "Failed to record bundle reward");
|
|
663
818
|
}
|
|
664
|
-
case "divider":
|
|
665
|
-
return `<tr><td style="padding:12px 24px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:1px solid ${escapeHtml(block.color ?? "#e5e5e5")};font-size:0;line-height:0"> </td></tr></table></td></tr>`;
|
|
666
|
-
case "spacer":
|
|
667
|
-
return `<tr><td style="height:${block.height ?? 24}px;font-size:0;line-height:0"> </td></tr>`;
|
|
668
|
-
default:
|
|
669
|
-
return "";
|
|
670
819
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
const missingSlots = [];
|
|
674
|
-
const width = document.contentWidth ?? CONTENT_WIDTH;
|
|
675
|
-
const background = escapeHtml(document.backgroundColor ?? "#f4f4f5");
|
|
676
|
-
const subject = resolve(document.subject, slots, missingSlots);
|
|
677
|
-
const preheader = resolve(document.preheader, slots, missingSlots);
|
|
678
|
-
const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join("\n");
|
|
679
|
-
const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
680
|
-
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
681
|
-
<head>
|
|
682
|
-
<meta charset="utf-8" />
|
|
683
|
-
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
684
|
-
<meta name="x-apple-disable-message-reformatting" />
|
|
685
|
-
<title>${escapeHtml(subject)}</title>
|
|
686
|
-
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
|
|
687
|
-
</head>
|
|
688
|
-
<body style="margin:0;padding:0;background-color:${background}">
|
|
689
|
-
<div style="display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all">${escapeHtml(preheader)}</div>
|
|
690
|
-
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:${background}">
|
|
691
|
-
<tr><td align="center" style="padding:24px 12px">
|
|
692
|
-
<table role="presentation" width="${width}" cellpadding="0" cellspacing="0" border="0" style="width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px">
|
|
693
|
-
${body}
|
|
694
|
-
</table>
|
|
695
|
-
</td></tr>
|
|
696
|
-
</table>
|
|
697
|
-
</body>
|
|
698
|
-
</html>`;
|
|
699
|
-
return { subject, html, missingSlots };
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// ../shared/dist/storefront-content.js
|
|
703
|
-
var STOREFRONT_KINDS = ["text", "image", "ordering", "offer"];
|
|
704
|
-
var PLACEMENTS_BY_KIND = {
|
|
705
|
-
text: ["hero_headline", "hero_subhead", "cta_primary", "banner_text", "badge_text"],
|
|
706
|
-
image: ["hero_image", "banner_image", "feature_image"],
|
|
707
|
-
ordering: ["product_list_primary", "product_list_secondary"],
|
|
708
|
-
offer: ["offer_banner"]
|
|
709
|
-
};
|
|
710
|
-
var PLACEMENTS = STOREFRONT_KINDS.flatMap((kind) => PLACEMENTS_BY_KIND[kind]);
|
|
711
|
-
function isPlacement(value) {
|
|
712
|
-
return typeof value === "string" && PLACEMENTS.includes(value);
|
|
713
|
-
}
|
|
714
|
-
function kindOfPlacement(placement) {
|
|
715
|
-
const found = STOREFRONT_KINDS.find((kind) => PLACEMENTS_BY_KIND[kind].includes(placement));
|
|
716
|
-
if (!found)
|
|
717
|
-
throw new Error(`Unknown placement: ${placement}`);
|
|
718
|
-
return found;
|
|
719
|
-
}
|
|
720
|
-
var CONTENT_TYPE_BY_KIND = {
|
|
721
|
-
text: { contentType: "TEXT", contentSubtype: "plain_text" },
|
|
722
|
-
image: { contentType: "FILE", contentSubtype: "image" },
|
|
723
|
-
ordering: { contentType: "CODE", contentSubtype: "json" },
|
|
724
|
-
offer: { contentType: "CODE", contentSubtype: "json" }
|
|
725
|
-
};
|
|
726
|
-
var MIN_ORDERING_ITEMS = 2;
|
|
727
|
-
var MAX_ORDERING_ITEMS = 12;
|
|
728
|
-
function isShopifyCdnUrl(value) {
|
|
729
|
-
let url;
|
|
730
|
-
try {
|
|
731
|
-
url = new URL(value);
|
|
732
|
-
} catch {
|
|
733
|
-
return false;
|
|
820
|
+
generateId() {
|
|
821
|
+
return generateId();
|
|
734
822
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
function isNonEmptyString(value) {
|
|
741
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
742
|
-
}
|
|
743
|
-
function isPositiveInt(value) {
|
|
744
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
745
|
-
}
|
|
746
|
-
function parseImage(raw) {
|
|
747
|
-
if (!isRecord(raw))
|
|
748
|
-
return null;
|
|
749
|
-
const { url, alt, width, height, sourceId } = raw;
|
|
750
|
-
if (typeof url !== "string" || !isShopifyCdnUrl(url))
|
|
751
|
-
return null;
|
|
752
|
-
if (typeof alt !== "string")
|
|
753
|
-
return null;
|
|
754
|
-
if (!isPositiveInt(width) || !isPositiveInt(height))
|
|
755
|
-
return null;
|
|
756
|
-
if (!isNonEmptyString(sourceId))
|
|
757
|
-
return null;
|
|
758
|
-
return { url, alt, width, height, sourceId };
|
|
759
|
-
}
|
|
760
|
-
function parseOrdering(raw) {
|
|
761
|
-
if (!Array.isArray(raw))
|
|
762
|
-
return null;
|
|
763
|
-
if (raw.length < MIN_ORDERING_ITEMS || raw.length > MAX_ORDERING_ITEMS)
|
|
764
|
-
return null;
|
|
765
|
-
if (!raw.every((id) => typeof id === "string" && /^\d+$/.test(id)))
|
|
766
|
-
return null;
|
|
767
|
-
if (new Set(raw).size !== raw.length)
|
|
768
|
-
return null;
|
|
769
|
-
return raw;
|
|
770
|
-
}
|
|
771
|
-
function parseOffer(raw) {
|
|
772
|
-
if (raw === null)
|
|
773
|
-
return null;
|
|
774
|
-
if (!isRecord(raw))
|
|
775
|
-
return void 0;
|
|
776
|
-
const { discountId, code, headline, subtext } = raw;
|
|
777
|
-
if (!isNonEmptyString(discountId) || !isNonEmptyString(code))
|
|
778
|
-
return void 0;
|
|
779
|
-
if (typeof headline !== "string" || typeof subtext !== "string")
|
|
780
|
-
return void 0;
|
|
781
|
-
return { discountId, code, headline, subtext };
|
|
782
|
-
}
|
|
783
|
-
function parseTreatmentContent(kind, config) {
|
|
784
|
-
if (!isRecord(config))
|
|
785
|
-
return null;
|
|
786
|
-
const content = config.content;
|
|
787
|
-
switch (kind) {
|
|
788
|
-
case "text":
|
|
789
|
-
return isNonEmptyString(content) ? { kind, text: content } : null;
|
|
790
|
-
case "image": {
|
|
791
|
-
const image = parseImage(content);
|
|
792
|
-
return image ? { kind, image } : null;
|
|
823
|
+
normalizeApiUrl() {
|
|
824
|
+
let baseUrl = this.config.apiUrl.replace(/\/$/, "");
|
|
825
|
+
if (!baseUrl.endsWith("/api")) {
|
|
826
|
+
baseUrl = `${baseUrl}/api`;
|
|
793
827
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
828
|
+
return baseUrl;
|
|
829
|
+
}
|
|
830
|
+
getOrCreateSessionId() {
|
|
831
|
+
return getOrCreateSessionId({
|
|
832
|
+
storage: defaultSessionStorage(),
|
|
833
|
+
ttlMs: this.config.sessionTtl
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
async makeRequest(endpoint, method, body, extraHeaders) {
|
|
837
|
+
const url = `${this.normalizeApiUrl()}${endpoint}`;
|
|
838
|
+
const headers = {
|
|
839
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
840
|
+
"Content-Type": "application/json",
|
|
841
|
+
...extraHeaders
|
|
842
|
+
};
|
|
843
|
+
const requestConfig = {
|
|
844
|
+
method,
|
|
845
|
+
headers,
|
|
846
|
+
signal: AbortSignal.timeout(this.config.timeout)
|
|
847
|
+
};
|
|
848
|
+
if (body && method !== "GET") {
|
|
849
|
+
requestConfig.body = JSON.stringify(body);
|
|
797
850
|
}
|
|
798
|
-
|
|
799
|
-
const
|
|
800
|
-
|
|
851
|
+
try {
|
|
852
|
+
const response = await fetch(url, requestConfig);
|
|
853
|
+
const data = await response.json();
|
|
854
|
+
return {
|
|
855
|
+
success: response.ok,
|
|
856
|
+
data: response.ok ? data : void 0,
|
|
857
|
+
error: response.ok ? void 0 : data?.message || "Request failed",
|
|
858
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
859
|
+
};
|
|
860
|
+
} catch (error) {
|
|
861
|
+
return {
|
|
862
|
+
success: false,
|
|
863
|
+
error: error instanceof Error ? error.message : "Network error",
|
|
864
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
865
|
+
};
|
|
801
866
|
}
|
|
802
867
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
return { content: content.text };
|
|
808
|
-
case "image":
|
|
809
|
-
return { content: content.image };
|
|
810
|
-
case "ordering":
|
|
811
|
-
return { content: content.productIds };
|
|
812
|
-
case "offer":
|
|
813
|
-
return { content: content.offer };
|
|
868
|
+
startBatchFlush() {
|
|
869
|
+
this.flushTimer = setInterval(() => {
|
|
870
|
+
this.flush().catch(console.error);
|
|
871
|
+
}, this.config.flushInterval);
|
|
814
872
|
}
|
|
815
|
-
|
|
873
|
+
destroy() {
|
|
874
|
+
if (this.flushTimer) {
|
|
875
|
+
clearInterval(this.flushTimer);
|
|
876
|
+
}
|
|
877
|
+
this.flush().catch(console.error);
|
|
878
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
879
|
+
localStorage.removeItem("bandits_session_id");
|
|
880
|
+
localStorage.removeItem("bandits_session_created_at");
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
816
884
|
|
|
817
885
|
// src/snapshot-cache.ts
|
|
818
886
|
var STORAGE_KEY = "bandits_snapshot";
|
|
@@ -1228,6 +1296,7 @@ export {
|
|
|
1228
1296
|
STATIONARY,
|
|
1229
1297
|
STOREFRONT_KINDS,
|
|
1230
1298
|
SnapshotClient,
|
|
1299
|
+
VISITOR_ID_KEY,
|
|
1231
1300
|
appendEmailToken,
|
|
1232
1301
|
buildSubdomainUrl,
|
|
1233
1302
|
calculateConfidenceInterval,
|
|
@@ -1235,10 +1304,11 @@ export {
|
|
|
1235
1304
|
extractEmailLinks,
|
|
1236
1305
|
extractSlotNames,
|
|
1237
1306
|
extractSubdomain,
|
|
1238
|
-
|
|
1307
|
+
generateId,
|
|
1239
1308
|
generateSubdomain,
|
|
1240
1309
|
getContentTypeCategory,
|
|
1241
1310
|
getOrCreateSessionId,
|
|
1311
|
+
getOrCreateVisitorId,
|
|
1242
1312
|
hashRecipient,
|
|
1243
1313
|
impressionManifest,
|
|
1244
1314
|
isPlacement,
|
|
@@ -1247,8 +1317,10 @@ export {
|
|
|
1247
1317
|
isValidEmail,
|
|
1248
1318
|
isValidSubdomain,
|
|
1249
1319
|
kindOfPlacement,
|
|
1320
|
+
matchesConversionPath,
|
|
1250
1321
|
mintEmailToken,
|
|
1251
1322
|
normalizeAlgorithm,
|
|
1323
|
+
normalizeConversionPath,
|
|
1252
1324
|
normalizePosterior,
|
|
1253
1325
|
normalizeTreatmentWeights,
|
|
1254
1326
|
parseTreatmentContent,
|