choreograph-create-pixel 1.4.2 โ†’ 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.
@@ -1,181 +1,320 @@
1
- /*! choreograph-create-pixel v1.4.2 2024/08/27 */
2
- class ChoreographCreatePixel {
3
- constructor(userConfig) {
4
- this.previouslyScrapedHash = null;
5
- this.logs = [];
6
-
7
- this.config = {
8
- colors: { error: "red", warn: "orange", success: "green" },
9
- icons: {
10
- scraper: "๐Ÿ“š",
11
- viewed: "๐Ÿ‘€",
12
- basketed: "๐Ÿ›’",
13
- purchased: "๐Ÿงพ",
14
- attribution: "๐Ÿ”–",
15
- conversion: "๐Ÿ“ˆ",
16
- },
17
- debug: /(pixel|lemonpi)_debug/i.test(location.href),
18
- before: (data, callback) => callback(data),
19
- after: () => {},
20
- optional: [],
21
- ...userConfig,
22
- };
1
+ /*! choreograph-create-pixel v1.4.4 2026/03/14 */
2
+
3
+ // src/helpers.js
4
+ function getUrl({ allowedQueryParameters = [], allowHash = false } = {}) {
5
+ let url = `${location.protocol}//${location.host}${location.pathname}`;
6
+ const parameters = new URLSearchParams(location.search);
7
+ allowedQueryParameters.reduce((paramAdded, parameter) => {
8
+ const separator = paramAdded ? "&" : "?";
9
+ const key = encodeURI(parameter);
10
+ const value = parameters.get(parameter);
11
+ if (value != null) {
12
+ url += `${separator}${key}${value === "" ? "" : `=${encodeURI(value)}`}`;
13
+ return true;
14
+ }
15
+ return paramAdded;
16
+ }, false);
17
+ if (allowHash) url += location.hash;
18
+ return url;
19
+ }
20
+ function getAllPathSegments() {
21
+ return location.pathname.split("/").filter((segment) => segment).map((segment) => decodeURI(segment));
22
+ }
23
+ function getPathSegment(index) {
24
+ return getAllPathSegments()[index];
25
+ }
26
+ function getAllQueryParameters() {
27
+ return location.search.replace(/^\?/, "").split("&").filter((parameter) => parameter).reduce(
28
+ (parameters, parameter) => ({
29
+ ...parameters,
30
+ [decodeURI(parameter.split("=")[0])]: decodeURI(
31
+ parameter.split("=")[1] || ""
32
+ )
33
+ }),
34
+ {}
35
+ );
36
+ }
37
+ function getQueryParameter(key) {
38
+ return getAllQueryParameters()[key];
39
+ }
23
40
 
24
- if (this.validateConfig()) this.cycle();
25
- }
41
+ // src/log.js
42
+ function log(level, message, data = null) {
43
+ if (!this.config.debug || this.logs.includes(message)) return;
44
+ const args = [
45
+ `%cCreative Optimizations%c ${this.config.icons[this.config.type]} ${this.config.type} %c${message}`,
46
+ "background:black;color:white;border-radius:3px;padding:3px 6px",
47
+ "font-weight:bold",
48
+ `color:${this.config.colors[level]}`
49
+ ];
50
+ if (data) args.push(data);
51
+ console.info(...args);
52
+ this.logs.push(message);
53
+ }
26
54
 
27
- static getUrl({ allowedQueryParameters = [], allowHash = false } = {}) {
28
- let url = `${location.protocol}//${location.host}${location.pathname}`;
29
- const parameters = new URLSearchParams(location.search);
55
+ // src/validate.js
56
+ function validateConfig() {
57
+ if (typeof this.config.advertiser !== "number")
58
+ this.log("error", "Please use a number", {
59
+ advertiser: this.config.advertiser
60
+ });
61
+ else if (![
62
+ "scraper",
63
+ "viewed",
64
+ "basketed",
65
+ "purchased",
66
+ "attribution",
67
+ "conversion"
68
+ ].includes(this.config.type))
69
+ this.log(
70
+ "error",
71
+ "Please use scraper, viewed, basketed, purchased, attribution or conversion",
72
+ { type: this.config.type }
73
+ );
74
+ else if (["attribution", "conversion"].includes(this.config.type) && typeof this.config.label !== "string")
75
+ this.log("error", "Please use a string", {
76
+ label: this.config.label
77
+ });
78
+ else if (!(this.config.url instanceof RegExp))
79
+ this.log("error", "Please use a regular expression", {
80
+ url: this.config.url
81
+ });
82
+ else if (!["attribution", "conversion"].includes(this.config.type) && !this.config.scrape)
83
+ this.log("error", "Please provide something to scrape", {
84
+ scrape: this.config.scrape
85
+ });
86
+ else return true;
87
+ }
30
88
 
31
- allowedQueryParameters.reduce((paramAdded, parameter) => {
32
- const separator = paramAdded ? "&" : "?";
33
- const key = encodeURI(parameter);
34
- const value = parameters.get(parameter);
89
+ // src/attribute.js
90
+ function attribute() {
91
+ const ccpid = getQueryParameter("ccpid");
92
+ if (!ccpid) return this.log("warn", "ccpid query parameter not present");
93
+ if (!/^[0-9a-f]{20}$/.test(ccpid))
94
+ return this.log(
95
+ "error",
96
+ "ccpid query parameter is not formatted correctly"
97
+ );
98
+ const storageItemLabel = `choreograph-${this.config.label}`;
99
+ const storedCcpid = localStorage.getItem(storageItemLabel);
100
+ if (storedCcpid === "")
101
+ return this.log("warn", `"${this.config.label}" already converted`);
102
+ if (storedCcpid !== ccpid) localStorage.setItem(storageItemLabel, ccpid);
103
+ this.log("success", `Stored CCPID "${ccpid}" for "${this.config.label}"`);
104
+ }
35
105
 
36
- if (value != null) {
37
- url += `${separator}${key}${
38
- value === "" ? "" : `=${encodeURI(value)}`
39
- }`;
106
+ // src/convert.js
107
+ function convert() {
108
+ const label = `choreograph-${this.config.label}`;
109
+ const ccpid = localStorage.getItem(label);
110
+ if (!ccpid)
111
+ return this.log("warn", `"${this.config.label}" attribution not present`);
112
+ this.send(ccpid);
113
+ localStorage.setItem(label, "");
114
+ }
40
115
 
41
- return true;
116
+ // src/scrape.js
117
+ function scrape(element) {
118
+ let hasErrors = false;
119
+ let data;
120
+ const handleField = (field, fieldName) => {
121
+ let result = field;
122
+ if (typeof result === "function") {
123
+ try {
124
+ result = result(element);
125
+ } catch (error) {
126
+ if (this.config.optional.includes(fieldName)) return void 0;
127
+ this.log("error", error.message, {
128
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result
129
+ });
130
+ hasErrors = true;
42
131
  }
43
-
44
- return paramAdded;
45
- }, false);
46
-
47
- if (allowHash) url += location.hash;
48
- return url;
49
- }
50
-
51
- static getAllPathSegments() {
52
- return location.pathname
53
- .split("/")
54
- .filter((segment) => segment)
55
- .map((segment) => decodeURI(segment));
56
- }
57
-
58
- static getPathSegment(index) {
59
- return this.getAllPathSegments()[index];
132
+ }
133
+ if (typeof result === "string")
134
+ result = result.replace(/\s+/g, " ").trim();
135
+ if ((result == null || result === "") && !this.config.optional.includes(fieldName)) {
136
+ this.log("error", "Value is empty", {
137
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result
138
+ });
139
+ hasErrors = true;
140
+ }
141
+ return result;
142
+ };
143
+ if (this.config.type !== "scraper") {
144
+ data = handleField(this.config.scrape);
145
+ } else {
146
+ data = Object.keys(this.config.scrape).reduce(
147
+ (acc, fieldName) => ({
148
+ ...acc,
149
+ [fieldName]: handleField(this.config.scrape[fieldName], fieldName)
150
+ }),
151
+ {}
152
+ );
60
153
  }
61
-
62
- static getAllQueryParameters() {
63
- return location.search
64
- .replace(/^\?/, "")
65
- .split("&")
66
- .filter((parameter) => parameter)
67
- .reduce(
68
- (parameters, parameter) => ({
69
- ...parameters,
70
- [decodeURI(parameter.split("=")[0])]: decodeURI(
71
- parameter.split("=")[1] || ""
72
- ),
73
- }),
74
- {}
75
- );
154
+ const dataHash = JSON.stringify(data);
155
+ if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
156
+ try {
157
+ this.config.before(data, (newData) => {
158
+ if (Array.isArray(newData)) newData.forEach((id) => this.send(id));
159
+ else this.send(newData);
160
+ });
161
+ } catch (error) {
162
+ this.log("error", error.message, {
163
+ before: this.config.before
164
+ });
165
+ }
166
+ this.previouslyScrapedHash = dataHash;
167
+ this.logs.length = 0;
76
168
  }
169
+ }
77
170
 
78
- static getQueryParameter(key) {
79
- return this.getAllQueryParameters()[key];
171
+ // src/send.js
172
+ function send(data) {
173
+ let payload;
174
+ let url;
175
+ let method = "GET";
176
+ switch (this.config.type) {
177
+ case "scraper": {
178
+ const { sku } = data;
179
+ delete data.sku;
180
+ payload = {
181
+ "advertiser-id": this.config.advertiser,
182
+ sku,
183
+ fields: data
184
+ };
185
+ url = `https://d.lemonpi.io/scrapes${this.config.debug ? "?validate=true" : ""}`;
186
+ method = "POST";
187
+ break;
188
+ }
189
+ case "viewed":
190
+ case "basketed":
191
+ case "purchased":
192
+ payload = {
193
+ "event-type": `product-${this.config.type}`,
194
+ sku: data
195
+ };
196
+ url = `https://d.lemonpi.io/a/${this.config.advertiser}/product/event?e=${encodeURIComponent(JSON.stringify(payload))}`;
197
+ break;
198
+ case "conversion":
199
+ payload = {
200
+ version: 1,
201
+ type: "conversion",
202
+ name: this.config.label,
203
+ "conversion-attribution-id": data,
204
+ "advertiser-id": this.config.advertiser
205
+ };
206
+ url = `https://content.lemonpi.io/track/event?e=${encodeURIComponent(
207
+ JSON.stringify(payload)
208
+ )}`;
209
+ break;
80
210
  }
81
-
82
- log(level, message, data = null) {
83
- if (!this.config.debug || this.logs.includes(message)) return;
84
-
85
- const args = [
86
- `%cโธฌ create%c ${this.config.icons[this.config.type]} ${
87
- this.config.type
88
- } %c${message}`,
89
- "background:black;color:white;border-radius:3px;padding:3px 6px",
90
- "font-weight:bold",
91
- `color:${this.config.colors[level]}`,
92
- ];
93
-
94
- if (data) args.push(data);
95
- console.info(...args);
96
- this.logs.push(message);
211
+ if (["viewed", "basketed", "purchased"].includes(this.config.type) && !this.config.debug) {
212
+ new Image().src = url;
213
+ return;
97
214
  }
215
+ fetch(
216
+ url,
217
+ method === "POST" ? {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json" },
220
+ body: JSON.stringify(payload)
221
+ } : null
222
+ ).then(
223
+ (response) => response.json().then((result) => {
224
+ if (response.ok) {
225
+ this.log("warn", "Successful, with warnings. Details:", {
226
+ payload,
227
+ result
228
+ });
229
+ try {
230
+ this.config.after(data);
231
+ } catch (error) {
232
+ this.log("error", error.message, {
233
+ after: this.config.after
234
+ });
235
+ }
236
+ } else
237
+ this.log(
238
+ "error",
239
+ `Failed: ${response.status} (${response.statusText}). Details:`,
240
+ { payload, result }
241
+ );
242
+ this.logs.length = 0;
243
+ }).catch(() => {
244
+ if (response.ok) {
245
+ this.log("success", "Successful!", { payload });
246
+ try {
247
+ this.config.after(data);
248
+ } catch (error) {
249
+ this.log("error", error.message, {
250
+ after: this.config.after
251
+ });
252
+ }
253
+ } else
254
+ this.log(
255
+ "error",
256
+ `Failed: ${response.status} (${response.statusText}). Details:`,
257
+ { payload, response }
258
+ );
259
+ this.logs.length = 0;
260
+ })
261
+ ).catch((error) => {
262
+ this.log("error", `Failed: ${error.message}`, { payload });
263
+ this.logs.length = 0;
264
+ });
265
+ }
98
266
 
99
- validateConfig() {
100
- if (typeof this.config.advertiser !== "number")
101
- this.log("error", "Please use a number", {
102
- advertiser: this.config.advertiser,
103
- });
104
- else if (
105
- ![
106
- "scraper",
107
- "viewed",
108
- "basketed",
109
- "purchased",
110
- "attribution",
111
- "conversion",
112
- ].includes(this.config.type)
113
- )
114
- this.log(
115
- "error",
116
- "Please use scraper, viewed, basketed, purchased, attribution or conversion",
117
- { type: this.config.type }
118
- );
119
- else if (
120
- ["attribution", "conversion"].includes(this.config.type) &&
121
- typeof this.config.label !== "string"
122
- )
123
- this.log("error", "Please use a string", {
124
- label: this.config.label,
125
- });
126
- else if (!(this.config.url instanceof RegExp))
127
- this.log("error", "Please use a regular expression", {
128
- url: this.config.url,
129
- });
130
- else if (
131
- !["attribution", "conversion"].includes(this.config.type) &&
132
- !this.config.scrape
133
- )
134
- this.log("error", "Please provide something to scrape", {
135
- scrape: this.config.scrape,
136
- });
137
- else return true;
267
+ // src/index.js
268
+ var ChoreographCreatePixel = class {
269
+ constructor(userConfig) {
270
+ this.previouslyScrapedHash = null;
271
+ this.logs = [];
272
+ this.config = {
273
+ colors: { error: "red", warn: "orange", success: "green" },
274
+ icons: {
275
+ scraper: "\u{1F4DA}",
276
+ viewed: "\u{1F440}",
277
+ basketed: "\u{1F6D2}",
278
+ purchased: "\u{1F9FE}",
279
+ attribution: "\u{1F516}",
280
+ conversion: "\u{1F4C8}"
281
+ },
282
+ debug: /(pixel|lemonpi)_debug/i.test(location.href),
283
+ before: (data, callback) => callback(data),
284
+ after: () => {
285
+ },
286
+ optional: [],
287
+ ...userConfig
288
+ };
289
+ if (this.validateConfig()) this.cycle();
138
290
  }
139
-
140
291
  cycle() {
141
292
  if (!this.config.url.test(location.href))
142
293
  this.log("warn", `URL pattern does not match "${location.href}"`, {
143
- url: this.config.url,
294
+ url: this.config.url
144
295
  });
145
- else if (
146
- this.config.type === "scraper" &&
147
- document.querySelector('html[class*="translated-"]')
148
- )
296
+ else if (this.config.type === "scraper" && document.querySelector('html[class*="translated-"]'))
149
297
  this.log(
150
298
  "warn",
151
299
  "This page has been translated by the browser, and won't be scraped"
152
300
  );
153
301
  else if (this.config.trigger) {
154
- // TO-DO: replace this.config.type with a unique pixel instance ID
155
302
  const elementAttribute = `create-${this.config.type}-${this.config.trigger.event}`;
156
-
157
303
  try {
158
- let elements =
159
- typeof this.config.trigger.elements === "string"
160
- ? document.querySelectorAll(this.config.trigger.elements)
161
- : this.config.trigger.elements();
162
-
304
+ let elements = typeof this.config.trigger.elements === "string" ? document.querySelectorAll(this.config.trigger.elements) : this.config.trigger.elements();
163
305
  if (!elements.forEach) elements = [elements];
164
-
165
306
  elements.forEach((element) => {
166
307
  if (!element.hasAttribute(elementAttribute)) {
167
- element.addEventListener(this.config.trigger.event, () =>
168
- this.config.type === "conversion"
169
- ? this.convert()
170
- : this.scrape(element)
308
+ element.addEventListener(
309
+ this.config.trigger.event,
310
+ () => this.config.type === "conversion" ? this.convert() : this.scrape(element)
171
311
  );
172
-
173
312
  element.setAttribute(elementAttribute, "");
174
313
  }
175
314
  });
176
315
  } catch (error) {
177
316
  this.log("error", error.message, {
178
- "trigger.elements": this.config.trigger.elements,
317
+ "trigger.elements": this.config.trigger.elements
179
318
  });
180
319
  }
181
320
  } else {
@@ -183,241 +322,28 @@ class ChoreographCreatePixel {
183
322
  case "attribution":
184
323
  this.attribute();
185
324
  break;
186
-
187
325
  case "conversion":
188
326
  this.convert();
189
327
  break;
190
-
191
328
  default:
192
329
  this.scrape();
193
330
  break;
194
331
  }
195
332
  }
196
-
197
- // TO-DO: Move this to each method's end? Pageview conversions are currently recursive
198
333
  setTimeout(() => this.cycle(), 750);
199
334
  }
200
-
201
- attribute() {
202
- const ccpid = this.constructor.getQueryParameter("ccpid");
203
- if (!ccpid) return this.log("warn", "ccpid query parameter not present");
204
-
205
- if (!/^[0-9a-f]{20}$/.test(ccpid))
206
- return this.log(
207
- "error",
208
- "ccpid query parameter is not formatted correctly"
209
- );
210
-
211
- const storageItemLabel = `choreograph-${this.config.label}`;
212
- const storedCcpid = localStorage.getItem(storageItemLabel);
213
- if (storedCcpid !== ccpid) localStorage.setItem(storageItemLabel, ccpid);
214
- this.log("success", `Stored CCPID "${ccpid}" for "${this.config.label}"`);
215
- }
216
-
217
- convert() {
218
- const label = `choreograph-${this.config.label}`;
219
- const ccpid = localStorage.getItem(label);
220
-
221
- if (!ccpid)
222
- return this.log("warn", `"${this.config.label}" attribution not present`);
223
-
224
- this.send(ccpid);
225
- localStorage.removeItem(label);
226
- }
227
-
228
- scrape(element) {
229
- let hasErrors = false;
230
- let data;
231
-
232
- const handleField = (field, fieldName) => {
233
- let result = field;
234
-
235
- if (typeof result === "function") {
236
- try {
237
- result = result(element);
238
- } catch (error) {
239
- if (this.config.optional.includes(fieldName)) return undefined;
240
-
241
- this.log("error", error.message, {
242
- [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
243
- });
244
-
245
- hasErrors = true;
246
- }
247
- }
248
-
249
- if (typeof result === "string")
250
- result = result.replace(/\s+/g, " ").trim();
251
-
252
- if (
253
- (result == null || result === "") &&
254
- !this.config.optional.includes(fieldName)
255
- ) {
256
- this.log("error", "Value is empty", {
257
- [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
258
- });
259
-
260
- hasErrors = true;
261
- }
262
-
263
- return result;
264
- };
265
-
266
- if (this.config.type !== "scraper") {
267
- data = handleField(this.config.scrape);
268
- } else {
269
- data = Object.keys(this.config.scrape).reduce(
270
- (acc, fieldName) => ({
271
- ...acc,
272
- [fieldName]: handleField(this.config.scrape[fieldName], fieldName),
273
- }),
274
- {}
275
- );
276
- }
277
-
278
- const dataHash = JSON.stringify(data);
279
-
280
- if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
281
- try {
282
- this.config.before(data, (newData) => {
283
- if (Array.isArray(newData)) newData.forEach((id) => this.send(id));
284
- else this.send(newData);
285
- });
286
- } catch (error) {
287
- this.log("error", error.message, {
288
- before: this.config.before,
289
- });
290
- }
291
-
292
- this.previouslyScrapedHash = dataHash;
293
- this.logs.length = 0;
294
- }
295
- }
296
-
297
- send(data) {
298
- let payload;
299
- let url;
300
- let method = "GET";
301
-
302
- switch (this.config.type) {
303
- case "scraper": {
304
- const { sku } = data;
305
- delete data.sku;
306
-
307
- payload = {
308
- "advertiser-id": this.config.advertiser,
309
- sku,
310
- fields: data,
311
- };
312
-
313
- url = `https://d.lemonpi.io/scrapes${
314
- this.config.debug ? "?validate=true" : ""
315
- }`;
316
-
317
- method = "POST";
318
- break;
319
- }
320
-
321
- case "viewed":
322
- case "basketed":
323
- case "purchased":
324
- payload = {
325
- "event-type": `product-${this.config.type}`,
326
- sku: data,
327
- };
328
-
329
- url = `https://d.lemonpi.io/a/${
330
- this.config.advertiser
331
- }/product/event?e=${encodeURIComponent(JSON.stringify(payload))}`;
332
-
333
- break;
334
-
335
- case "conversion":
336
- payload = {
337
- version: 1,
338
- type: "conversion",
339
- name: this.config.label,
340
- "conversion-attribution-id": data,
341
- "advertiser-id": this.config.advertiser,
342
- };
343
-
344
- url = `https://content.lemonpi.io/track/event?e=${encodeURIComponent(
345
- JSON.stringify(payload)
346
- )}`;
347
-
348
- break;
349
- }
350
-
351
- if (
352
- ["viewed", "basketed", "purchased"].includes(this.config.type) &&
353
- !this.config.debug
354
- ) {
355
- new Image().src = url;
356
- return;
357
- }
358
-
359
- fetch(
360
- url,
361
- method === "POST"
362
- ? {
363
- method: "POST",
364
- headers: { "Content-Type": "application/json" },
365
- body: JSON.stringify(payload),
366
- }
367
- : null
368
- )
369
- .then((response) =>
370
- response
371
- .json()
372
- .then((result) => {
373
- if (response.ok) {
374
- this.log("warn", "Successful, with warnings. Details:", {
375
- payload,
376
- result,
377
- });
378
-
379
- try {
380
- this.config.after(data);
381
- } catch (error) {
382
- this.log("error", error.message, {
383
- after: this.config.after,
384
- });
385
- }
386
- } else
387
- this.log(
388
- "error",
389
- `Failed: ${response.status} (${response.statusText}). Details:`,
390
- { payload, result }
391
- );
392
-
393
- this.logs.length = 0;
394
- })
395
- .catch(() => {
396
- if (response.ok) {
397
- this.log("success", "Successful!", { payload });
398
-
399
- try {
400
- this.config.after(data);
401
- } catch (error) {
402
- this.log("error", error.message, {
403
- after: this.config.after,
404
- });
405
- }
406
- } else
407
- this.log(
408
- "error",
409
- `Failed: ${response.status} (${response.statusText}). Details:`,
410
- { payload, response }
411
- );
412
-
413
- this.logs.length = 0;
414
- })
415
- )
416
- .catch((error) => {
417
- this.log("error", `Failed: ${error.message}`, { payload });
418
- this.logs.length = 0;
419
- });
420
- }
421
- }
422
-
423
- export { ChoreographCreatePixel as default };
335
+ };
336
+ ChoreographCreatePixel.getUrl = getUrl;
337
+ ChoreographCreatePixel.getAllPathSegments = getAllPathSegments;
338
+ ChoreographCreatePixel.getPathSegment = getPathSegment;
339
+ ChoreographCreatePixel.getAllQueryParameters = getAllQueryParameters;
340
+ ChoreographCreatePixel.getQueryParameter = getQueryParameter;
341
+ ChoreographCreatePixel.prototype.log = log;
342
+ ChoreographCreatePixel.prototype.validateConfig = validateConfig;
343
+ ChoreographCreatePixel.prototype.attribute = attribute;
344
+ ChoreographCreatePixel.prototype.convert = convert;
345
+ ChoreographCreatePixel.prototype.scrape = scrape;
346
+ ChoreographCreatePixel.prototype.send = send;
347
+ export {
348
+ ChoreographCreatePixel as default
349
+ };