choreograph-create-pixel 1.4.3 → 1.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bundle.cjs +370 -0
- package/dist/bundle.esm.js +299 -377
- package/dist/bundle.iife.min.js +3 -2
- package/package.json +13 -21
- package/dist/bundle.cjs.js +0 -429
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Creative Optimizations Pixel
|
|
2
2
|
|
|
3
|
-
This library lets you apply best practises to [
|
|
3
|
+
This library lets you apply best practises to [Creative Optimizations](https://create.choreograph.com/) pixel development and implementation.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
package/dist/bundle.cjs
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/*! choreograph-create-pixel v1.4.4 2026/03/14 */
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.js
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
default: () => ChoreographCreatePixel
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/helpers.js
|
|
28
|
+
function getUrl({ allowedQueryParameters = [], allowHash = false } = {}) {
|
|
29
|
+
let url = `${location.protocol}//${location.host}${location.pathname}`;
|
|
30
|
+
const parameters = new URLSearchParams(location.search);
|
|
31
|
+
allowedQueryParameters.reduce((paramAdded, parameter) => {
|
|
32
|
+
const separator = paramAdded ? "&" : "?";
|
|
33
|
+
const key = encodeURI(parameter);
|
|
34
|
+
const value = parameters.get(parameter);
|
|
35
|
+
if (value != null) {
|
|
36
|
+
url += `${separator}${key}${value === "" ? "" : `=${encodeURI(value)}`}`;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return paramAdded;
|
|
40
|
+
}, false);
|
|
41
|
+
if (allowHash) url += location.hash;
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
function getAllPathSegments() {
|
|
45
|
+
return location.pathname.split("/").filter((segment) => segment).map((segment) => decodeURI(segment));
|
|
46
|
+
}
|
|
47
|
+
function getPathSegment(index) {
|
|
48
|
+
return getAllPathSegments()[index];
|
|
49
|
+
}
|
|
50
|
+
function getAllQueryParameters() {
|
|
51
|
+
return location.search.replace(/^\?/, "").split("&").filter((parameter) => parameter).reduce(
|
|
52
|
+
(parameters, parameter) => ({
|
|
53
|
+
...parameters,
|
|
54
|
+
[decodeURI(parameter.split("=")[0])]: decodeURI(
|
|
55
|
+
parameter.split("=")[1] || ""
|
|
56
|
+
)
|
|
57
|
+
}),
|
|
58
|
+
{}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function getQueryParameter(key) {
|
|
62
|
+
return getAllQueryParameters()[key];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/log.js
|
|
66
|
+
function log(level, message, data = null) {
|
|
67
|
+
if (!this.config.debug || this.logs.includes(message)) return;
|
|
68
|
+
const args = [
|
|
69
|
+
`%cCreative Optimizations%c ${this.config.icons[this.config.type]} ${this.config.type} %c${message}`,
|
|
70
|
+
"background:black;color:white;border-radius:3px;padding:3px 6px",
|
|
71
|
+
"font-weight:bold",
|
|
72
|
+
`color:${this.config.colors[level]}`
|
|
73
|
+
];
|
|
74
|
+
if (data) args.push(data);
|
|
75
|
+
console.info(...args);
|
|
76
|
+
this.logs.push(message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/validate.js
|
|
80
|
+
function validateConfig() {
|
|
81
|
+
if (typeof this.config.advertiser !== "number")
|
|
82
|
+
this.log("error", "Please use a number", {
|
|
83
|
+
advertiser: this.config.advertiser
|
|
84
|
+
});
|
|
85
|
+
else if (![
|
|
86
|
+
"scraper",
|
|
87
|
+
"viewed",
|
|
88
|
+
"basketed",
|
|
89
|
+
"purchased",
|
|
90
|
+
"attribution",
|
|
91
|
+
"conversion"
|
|
92
|
+
].includes(this.config.type))
|
|
93
|
+
this.log(
|
|
94
|
+
"error",
|
|
95
|
+
"Please use scraper, viewed, basketed, purchased, attribution or conversion",
|
|
96
|
+
{ type: this.config.type }
|
|
97
|
+
);
|
|
98
|
+
else if (["attribution", "conversion"].includes(this.config.type) && typeof this.config.label !== "string")
|
|
99
|
+
this.log("error", "Please use a string", {
|
|
100
|
+
label: this.config.label
|
|
101
|
+
});
|
|
102
|
+
else if (!(this.config.url instanceof RegExp))
|
|
103
|
+
this.log("error", "Please use a regular expression", {
|
|
104
|
+
url: this.config.url
|
|
105
|
+
});
|
|
106
|
+
else if (!["attribution", "conversion"].includes(this.config.type) && !this.config.scrape)
|
|
107
|
+
this.log("error", "Please provide something to scrape", {
|
|
108
|
+
scrape: this.config.scrape
|
|
109
|
+
});
|
|
110
|
+
else return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/attribute.js
|
|
114
|
+
function attribute() {
|
|
115
|
+
const ccpid = getQueryParameter("ccpid");
|
|
116
|
+
if (!ccpid) return this.log("warn", "ccpid query parameter not present");
|
|
117
|
+
if (!/^[0-9a-f]{20}$/.test(ccpid))
|
|
118
|
+
return this.log(
|
|
119
|
+
"error",
|
|
120
|
+
"ccpid query parameter is not formatted correctly"
|
|
121
|
+
);
|
|
122
|
+
const storageItemLabel = `choreograph-${this.config.label}`;
|
|
123
|
+
const storedCcpid = localStorage.getItem(storageItemLabel);
|
|
124
|
+
if (storedCcpid === "")
|
|
125
|
+
return this.log("warn", `"${this.config.label}" already converted`);
|
|
126
|
+
if (storedCcpid !== ccpid) localStorage.setItem(storageItemLabel, ccpid);
|
|
127
|
+
this.log("success", `Stored CCPID "${ccpid}" for "${this.config.label}"`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/convert.js
|
|
131
|
+
function convert() {
|
|
132
|
+
const label = `choreograph-${this.config.label}`;
|
|
133
|
+
const ccpid = localStorage.getItem(label);
|
|
134
|
+
if (!ccpid)
|
|
135
|
+
return this.log("warn", `"${this.config.label}" attribution not present`);
|
|
136
|
+
this.send(ccpid);
|
|
137
|
+
localStorage.setItem(label, "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/scrape.js
|
|
141
|
+
function scrape(element) {
|
|
142
|
+
let hasErrors = false;
|
|
143
|
+
let data;
|
|
144
|
+
const handleField = (field, fieldName) => {
|
|
145
|
+
let result = field;
|
|
146
|
+
if (typeof result === "function") {
|
|
147
|
+
try {
|
|
148
|
+
result = result(element);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (this.config.optional.includes(fieldName)) return void 0;
|
|
151
|
+
this.log("error", error.message, {
|
|
152
|
+
[fieldName ? `scrape.${fieldName}` : "scrape"]: result
|
|
153
|
+
});
|
|
154
|
+
hasErrors = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (typeof result === "string")
|
|
158
|
+
result = result.replace(/\s+/g, " ").trim();
|
|
159
|
+
if ((result == null || result === "") && !this.config.optional.includes(fieldName)) {
|
|
160
|
+
this.log("error", "Value is empty", {
|
|
161
|
+
[fieldName ? `scrape.${fieldName}` : "scrape"]: result
|
|
162
|
+
});
|
|
163
|
+
hasErrors = true;
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
};
|
|
167
|
+
if (this.config.type !== "scraper") {
|
|
168
|
+
data = handleField(this.config.scrape);
|
|
169
|
+
} else {
|
|
170
|
+
data = Object.keys(this.config.scrape).reduce(
|
|
171
|
+
(acc, fieldName) => ({
|
|
172
|
+
...acc,
|
|
173
|
+
[fieldName]: handleField(this.config.scrape[fieldName], fieldName)
|
|
174
|
+
}),
|
|
175
|
+
{}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const dataHash = JSON.stringify(data);
|
|
179
|
+
if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
|
|
180
|
+
try {
|
|
181
|
+
this.config.before(data, (newData) => {
|
|
182
|
+
if (Array.isArray(newData)) newData.forEach((id) => this.send(id));
|
|
183
|
+
else this.send(newData);
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.log("error", error.message, {
|
|
187
|
+
before: this.config.before
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
this.previouslyScrapedHash = dataHash;
|
|
191
|
+
this.logs.length = 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/send.js
|
|
196
|
+
function send(data) {
|
|
197
|
+
let payload;
|
|
198
|
+
let url;
|
|
199
|
+
let method = "GET";
|
|
200
|
+
switch (this.config.type) {
|
|
201
|
+
case "scraper": {
|
|
202
|
+
const { sku } = data;
|
|
203
|
+
delete data.sku;
|
|
204
|
+
payload = {
|
|
205
|
+
"advertiser-id": this.config.advertiser,
|
|
206
|
+
sku,
|
|
207
|
+
fields: data
|
|
208
|
+
};
|
|
209
|
+
url = `https://d.lemonpi.io/scrapes${this.config.debug ? "?validate=true" : ""}`;
|
|
210
|
+
method = "POST";
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case "viewed":
|
|
214
|
+
case "basketed":
|
|
215
|
+
case "purchased":
|
|
216
|
+
payload = {
|
|
217
|
+
"event-type": `product-${this.config.type}`,
|
|
218
|
+
sku: data
|
|
219
|
+
};
|
|
220
|
+
url = `https://d.lemonpi.io/a/${this.config.advertiser}/product/event?e=${encodeURIComponent(JSON.stringify(payload))}`;
|
|
221
|
+
break;
|
|
222
|
+
case "conversion":
|
|
223
|
+
payload = {
|
|
224
|
+
version: 1,
|
|
225
|
+
type: "conversion",
|
|
226
|
+
name: this.config.label,
|
|
227
|
+
"conversion-attribution-id": data,
|
|
228
|
+
"advertiser-id": this.config.advertiser
|
|
229
|
+
};
|
|
230
|
+
url = `https://content.lemonpi.io/track/event?e=${encodeURIComponent(
|
|
231
|
+
JSON.stringify(payload)
|
|
232
|
+
)}`;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (["viewed", "basketed", "purchased"].includes(this.config.type) && !this.config.debug) {
|
|
236
|
+
new Image().src = url;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
fetch(
|
|
240
|
+
url,
|
|
241
|
+
method === "POST" ? {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
body: JSON.stringify(payload)
|
|
245
|
+
} : null
|
|
246
|
+
).then(
|
|
247
|
+
(response) => response.json().then((result) => {
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
this.log("warn", "Successful, with warnings. Details:", {
|
|
250
|
+
payload,
|
|
251
|
+
result
|
|
252
|
+
});
|
|
253
|
+
try {
|
|
254
|
+
this.config.after(data);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.log("error", error.message, {
|
|
257
|
+
after: this.config.after
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else
|
|
261
|
+
this.log(
|
|
262
|
+
"error",
|
|
263
|
+
`Failed: ${response.status} (${response.statusText}). Details:`,
|
|
264
|
+
{ payload, result }
|
|
265
|
+
);
|
|
266
|
+
this.logs.length = 0;
|
|
267
|
+
}).catch(() => {
|
|
268
|
+
if (response.ok) {
|
|
269
|
+
this.log("success", "Successful!", { payload });
|
|
270
|
+
try {
|
|
271
|
+
this.config.after(data);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
this.log("error", error.message, {
|
|
274
|
+
after: this.config.after
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
} else
|
|
278
|
+
this.log(
|
|
279
|
+
"error",
|
|
280
|
+
`Failed: ${response.status} (${response.statusText}). Details:`,
|
|
281
|
+
{ payload, response }
|
|
282
|
+
);
|
|
283
|
+
this.logs.length = 0;
|
|
284
|
+
})
|
|
285
|
+
).catch((error) => {
|
|
286
|
+
this.log("error", `Failed: ${error.message}`, { payload });
|
|
287
|
+
this.logs.length = 0;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/index.js
|
|
292
|
+
var ChoreographCreatePixel = class {
|
|
293
|
+
constructor(userConfig) {
|
|
294
|
+
this.previouslyScrapedHash = null;
|
|
295
|
+
this.logs = [];
|
|
296
|
+
this.config = {
|
|
297
|
+
colors: { error: "red", warn: "orange", success: "green" },
|
|
298
|
+
icons: {
|
|
299
|
+
scraper: "\u{1F4DA}",
|
|
300
|
+
viewed: "\u{1F440}",
|
|
301
|
+
basketed: "\u{1F6D2}",
|
|
302
|
+
purchased: "\u{1F9FE}",
|
|
303
|
+
attribution: "\u{1F516}",
|
|
304
|
+
conversion: "\u{1F4C8}"
|
|
305
|
+
},
|
|
306
|
+
debug: /(pixel|lemonpi)_debug/i.test(location.href),
|
|
307
|
+
before: (data, callback) => callback(data),
|
|
308
|
+
after: () => {
|
|
309
|
+
},
|
|
310
|
+
optional: [],
|
|
311
|
+
...userConfig
|
|
312
|
+
};
|
|
313
|
+
if (this.validateConfig()) this.cycle();
|
|
314
|
+
}
|
|
315
|
+
cycle() {
|
|
316
|
+
if (!this.config.url.test(location.href))
|
|
317
|
+
this.log("warn", `URL pattern does not match "${location.href}"`, {
|
|
318
|
+
url: this.config.url
|
|
319
|
+
});
|
|
320
|
+
else if (this.config.type === "scraper" && document.querySelector('html[class*="translated-"]'))
|
|
321
|
+
this.log(
|
|
322
|
+
"warn",
|
|
323
|
+
"This page has been translated by the browser, and won't be scraped"
|
|
324
|
+
);
|
|
325
|
+
else if (this.config.trigger) {
|
|
326
|
+
const elementAttribute = `create-${this.config.type}-${this.config.trigger.event}`;
|
|
327
|
+
try {
|
|
328
|
+
let elements = typeof this.config.trigger.elements === "string" ? document.querySelectorAll(this.config.trigger.elements) : this.config.trigger.elements();
|
|
329
|
+
if (!elements.forEach) elements = [elements];
|
|
330
|
+
elements.forEach((element) => {
|
|
331
|
+
if (!element.hasAttribute(elementAttribute)) {
|
|
332
|
+
element.addEventListener(
|
|
333
|
+
this.config.trigger.event,
|
|
334
|
+
() => this.config.type === "conversion" ? this.convert() : this.scrape(element)
|
|
335
|
+
);
|
|
336
|
+
element.setAttribute(elementAttribute, "");
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
this.log("error", error.message, {
|
|
341
|
+
"trigger.elements": this.config.trigger.elements
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
switch (this.config.type) {
|
|
346
|
+
case "attribution":
|
|
347
|
+
this.attribute();
|
|
348
|
+
break;
|
|
349
|
+
case "conversion":
|
|
350
|
+
this.convert();
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
this.scrape();
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
setTimeout(() => this.cycle(), 750);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
ChoreographCreatePixel.getUrl = getUrl;
|
|
361
|
+
ChoreographCreatePixel.getAllPathSegments = getAllPathSegments;
|
|
362
|
+
ChoreographCreatePixel.getPathSegment = getPathSegment;
|
|
363
|
+
ChoreographCreatePixel.getAllQueryParameters = getAllQueryParameters;
|
|
364
|
+
ChoreographCreatePixel.getQueryParameter = getQueryParameter;
|
|
365
|
+
ChoreographCreatePixel.prototype.log = log;
|
|
366
|
+
ChoreographCreatePixel.prototype.validateConfig = validateConfig;
|
|
367
|
+
ChoreographCreatePixel.prototype.attribute = attribute;
|
|
368
|
+
ChoreographCreatePixel.prototype.convert = convert;
|
|
369
|
+
ChoreographCreatePixel.prototype.scrape = scrape;
|
|
370
|
+
ChoreographCreatePixel.prototype.send = send;
|