choreograph-create-pixel 1.3.3 β†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ This library lets you apply best practises to [Create](https://www.lemonpi.io/)
4
4
 
5
5
  ## Features
6
6
 
7
- - [x] Supports [scraper](#scraper-pixels), [segment](#segment-pixels) and [conversion](#conversion-pixels) (coming soon) pixels
7
+ - [x] Supports [scraper](#scraper-pixels), [segment](#segment-pixels) and [post-click](#post-click-pixels) pixels
8
8
  - [x] Supports dynamic page content updates
9
9
  - [x] Prevents scraping browser-translated content
10
10
  - [x] Continuous URL validation
@@ -15,7 +15,7 @@ This library lets you apply best practises to [Create](https://www.lemonpi.io/)
15
15
 
16
16
  <small>Type: `scraper`</small>
17
17
 
18
- Scraper pixels are used to scrape (read) data from websites, and store that data as products within an advertiser's product store.
18
+ Scraper pixels are used to scrape (collect) data from websites, and store that data as products within an advertiser's product store.
19
19
 
20
20
  ### Segment pixels
21
21
 
@@ -23,9 +23,11 @@ Scraper pixels are used to scrape (read) data from websites, and store that data
23
23
 
24
24
  Products in ads are shown by recommendation. Segment pixels determine this recommendation on a consumer level, by storing a **cookie** at different moments or events during the consumer's journey.
25
25
 
26
- ### Conversion pixels
26
+ ### Post-click pixels
27
27
 
28
- _(coming soon)_
28
+ <small>Types: `attribution` and `conversion`</small>
29
+
30
+ Post-click conversion attribution tracks which clicked creative variant attributed to a website conversion. The **attribution** pixel collects the creative's impression ID from the URL (`ccpid` query parameter) on the landing page, and the **conversion** pixel logs this ID as a performance metric in Create when a user performed a conversion, like a purchase.
29
31
 
30
32
  ## Quickstart
31
33
 
@@ -44,7 +46,7 @@ new Pixel({
44
46
  // Where on the website should this pixel be enabled?
45
47
  url: /example\.com\/products\/\d/,
46
48
 
47
- // Functions here are continuously being evaluated for value changes
49
+ // Scrape functions are continuously re-evaluated for value changes
48
50
  scrape: {
49
51
  // Data layer example
50
52
  sku: () => window.dataLayer[0].product.sku,
@@ -97,6 +99,33 @@ new Pixel({
97
99
  // Segment pixels support multiple SKUs
98
100
  scrape: () => window.dataLayer[0].cart.map((product) => product.sku),
99
101
  });
102
+
103
+ // Attribution post-click pixel
104
+ new Pixel({
105
+ advertiser: 0,
106
+ type: "attribution",
107
+
108
+ // Uniquely label each set of post-click pixels
109
+ label: "example",
110
+
111
+ // Match this URL to the landing page of your campaign
112
+ url: /example\.com\/products\/\d/,
113
+ });
114
+
115
+ // Conversion post-click pixel
116
+ new Pixel({
117
+ advertiser: 0,
118
+ type: "conversion",
119
+ label: "example",
120
+
121
+ // Match this URL to the conversion page of your campaign
122
+ url: /example\.com\/cart/,
123
+
124
+ trigger: {
125
+ event: "click",
126
+ elements: "button.checkout",
127
+ },
128
+ });
100
129
  ```
101
130
 
102
131
  > ES modules require manual bundling and transpiling before they can be safely embedded on websites.
@@ -128,6 +157,7 @@ Enable browser console debugging by adding `?pixel_debug` or `#pixel_debug` to t
128
157
  | ------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
129
158
  | `advertiser` | Number | You can retrieve the advertiser ID from the URL in Create: _manage.lemonpi.io/r/AGENCY_ID/advertiser/`ADVERTISER_ID`_. | _Required_ |
130
159
  | `type` | String | Pixel type. One of: `"scraper"`, `"viewed"`, `"basketed"` or `"purchased"`. | _Required_ |
160
+ | `label`<br>_(post-click only)_ | String | Conversion label, used for aggregation in reporting. | _Required_ |
131
161
  | `url` | RegExp | Only enables the pixel on page URLs that are matched by this pattern. | _Required_ |
132
162
  | `scrape`<br>_(segments only)_ | String, Array or Function | Scrapes the product's SKU for segments. | _Required_ |
133
163
  | `scrape.*`<br>_(scrapers only)_ | String, Number, Boolean or Function | Scrapes arbitrary product data. `*` should always match with existing field names in the advertiser's product store. | _Required_ |
@@ -1,4 +1,4 @@
1
- /*! choreograph-create-pixel v1.3.3 2022/12/22 */
1
+ /*! choreograph-create-pixel v1.4.0 2023/02/01 */
2
2
  'use strict';
3
3
 
4
4
  class ChoreographCreatePixel {
@@ -14,7 +14,7 @@ class ChoreographCreatePixel {
14
14
  basketed: "πŸ›’",
15
15
  purchased: "🧾",
16
16
  attribution: "πŸ”–",
17
- conversion: "πŸ’΅",
17
+ conversion: "πŸ“ˆ",
18
18
  },
19
19
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
20
20
  before: (data, callback) => callback(data),
@@ -85,8 +85,8 @@ class ChoreographCreatePixel {
85
85
  if (!this.config.debug || this.logs.includes(message)) return;
86
86
 
87
87
  const args = [
88
- `%cβΈ¬ create%c ${this.config.type} ${
89
- this.config.icons[this.config.type]
88
+ `%cβΈ¬ create%c ${this.config.icons[this.config.type]} ${this.config.type}${
89
+ this.config.label ? ` (${this.config.label})` : ""
90
90
  } %c${message}`,
91
91
  "background:black;color:white;border-radius:3px;padding:3px 6px",
92
92
  "font-weight:bold",
@@ -104,16 +104,35 @@ class ChoreographCreatePixel {
104
104
  advertiser: this.config.advertiser,
105
105
  });
106
106
  else if (
107
- !["scraper", "viewed", "basketed", "purchased"].includes(this.config.type)
107
+ ![
108
+ "scraper",
109
+ "viewed",
110
+ "basketed",
111
+ "purchased",
112
+ "attribution",
113
+ "conversion",
114
+ ].includes(this.config.type)
108
115
  )
109
- this.log("error", "Please use scraper, viewed, basketed or purchased", {
110
- type: this.config.type,
116
+ this.log(
117
+ "error",
118
+ "Please use scraper, viewed, basketed, purchased, attribution or conversion",
119
+ { type: this.config.type }
120
+ );
121
+ else if (
122
+ ["attribution", "conversion"].includes(this.config.type) &&
123
+ typeof this.config.label !== "string"
124
+ )
125
+ this.log("error", "Please use a string", {
126
+ label: this.config.label,
111
127
  });
112
128
  else if (!(this.config.url instanceof RegExp))
113
129
  this.log("error", "Please use a regular expression", {
114
130
  url: this.config.url,
115
131
  });
116
- else if (!this.config.scrape)
132
+ else if (
133
+ !["attribution", "conversion"].includes(this.config.type) &&
134
+ !this.config.scrape
135
+ )
117
136
  this.log("error", "Please provide something to scrape", {
118
137
  scrape: this.config.scrape,
119
138
  });
@@ -134,7 +153,8 @@ class ChoreographCreatePixel {
134
153
  "This page has been translated by the browser, and won't be scraped"
135
154
  );
136
155
  else if (this.config.trigger) {
137
- const attribute = `create-${this.config.type}-${this.config.trigger.event}`;
156
+ // TO-DO: replace this.config.type with a unique pixel instance ID
157
+ const elementAttribute = `create-${this.config.type}-${this.config.trigger.event}`;
138
158
 
139
159
  try {
140
160
  let elements =
@@ -145,12 +165,14 @@ class ChoreographCreatePixel {
145
165
  if (!elements.forEach) elements = [elements];
146
166
 
147
167
  elements.forEach((element) => {
148
- if (!element.hasAttribute(attribute)) {
168
+ if (!element.hasAttribute(elementAttribute)) {
149
169
  element.addEventListener(this.config.trigger.event, () =>
150
- this.scrape(element)
170
+ this.config.type === "conversion"
171
+ ? this.convert()
172
+ : this.scrape(element)
151
173
  );
152
174
 
153
- element.setAttribute(attribute, "");
175
+ element.setAttribute(elementAttribute, "");
154
176
  }
155
177
  });
156
178
  } catch (error) {
@@ -159,12 +181,53 @@ class ChoreographCreatePixel {
159
181
  });
160
182
  }
161
183
  } else {
162
- this.scrape();
184
+ switch (this.config.type) {
185
+ case "attribution":
186
+ this.attribute();
187
+ break;
188
+
189
+ case "conversion":
190
+ this.convert();
191
+ break;
192
+
193
+ default:
194
+ this.scrape();
195
+ break;
196
+ }
163
197
  }
164
198
 
165
199
  setTimeout(() => this.cycle(), 750);
166
200
  }
167
201
 
202
+ attribute() {
203
+ const ccpid = this.constructor.getQueryParameter("ccpid");
204
+ if (!ccpid) return this.log("warn", "ccpid query parameter not present");
205
+
206
+ if (
207
+ !/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(
208
+ ccpid
209
+ )
210
+ )
211
+ return this.log(
212
+ "error",
213
+ "ccpid query parameter is not formatted correctly"
214
+ );
215
+
216
+ const storageItemLabel = `choreograph-${this.config.label}`;
217
+ const storedCcpid = localStorage.getItem(storageItemLabel);
218
+ if (storedCcpid !== ccpid) localStorage.setItem(storageItemLabel, ccpid);
219
+ this.log("success", "Successful!", ccpid);
220
+ }
221
+
222
+ convert() {
223
+ const ccpid = localStorage.getItem(`choreograph-${this.config.label}`);
224
+
225
+ if (!ccpid)
226
+ return this.log("warn", `"${this.config.label}" attribution not present`);
227
+
228
+ this.send(ccpid);
229
+ }
230
+
168
231
  scrape(element) {
169
232
  let hasErrors = false;
170
233
  let data;
@@ -178,11 +241,9 @@ class ChoreographCreatePixel {
178
241
  } catch (error) {
179
242
  if (this.config.optional.includes(fieldName)) return undefined;
180
243
 
181
- this.log(
182
- this.config.type === "attribution" ? "warn" : "error",
183
- error.message,
184
- { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
185
- );
244
+ this.log("error", error.message, {
245
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
246
+ });
186
247
 
187
248
  hasErrors = true;
188
249
  }
@@ -195,11 +256,9 @@ class ChoreographCreatePixel {
195
256
  (result == null || result === "") &&
196
257
  !this.config.optional.includes(fieldName)
197
258
  ) {
198
- this.log(
199
- this.config.type === "attribution" ? "warn" : "error",
200
- "Value is empty",
201
- { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
202
- );
259
+ this.log("error", "Value is empty", {
260
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
261
+ });
203
262
 
204
263
  hasErrors = true;
205
264
  }
@@ -207,17 +266,7 @@ class ChoreographCreatePixel {
207
266
  return result;
208
267
  };
209
268
 
210
- if (this.config.type === "conversion") {
211
- data = {
212
- conversion: handleField(this.config.scrape),
213
- attribution: localStorage.getItem("create-attribution-id"),
214
- };
215
-
216
- if (typeof data.attribution !== "string") {
217
- this.log("warn", "There's no attribution ID stored yet");
218
- hasErrors = true;
219
- }
220
- } else if (this.config.type !== "scraper") {
269
+ if (this.config.type !== "scraper") {
221
270
  data = handleField(this.config.scrape);
222
271
  } else {
223
272
  data = Object.keys(this.config.scrape).reduce(
@@ -234,19 +283,7 @@ class ChoreographCreatePixel {
234
283
  if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
235
284
  try {
236
285
  this.config.before(data, (newData) => {
237
- if (this.config.type === "attribution") {
238
- localStorage.setItem("create-attribution-id", data);
239
- this.log("success", "Successful!", { attribution: data });
240
-
241
- try {
242
- this.config.after(data);
243
- } catch (error) {
244
- this.log("error", error.message, {
245
- after: this.config.after,
246
- });
247
- }
248
- } else if (Array.isArray(newData))
249
- newData.forEach((id) => this.send(id));
286
+ if (Array.isArray(newData)) newData.forEach((id) => this.send(id));
250
287
  else this.send(newData);
251
288
  });
252
289
  } catch (error) {
@@ -268,10 +305,20 @@ class ChoreographCreatePixel {
268
305
  url = `https://d.lemonpi.io/scrapes${
269
306
  this.config.debug ? "?validate=true" : ""
270
307
  }`;
308
+
271
309
  break;
272
310
 
273
311
  case "conversion":
274
- url = "https://lemonpi.io/"; // TO-DO
312
+ url = `https://content.lemonpi.io/track/event?e=${encodeURIComponent(
313
+ JSON.stringify({
314
+ version: 1,
315
+ type: "conversion",
316
+ name: this.config.label,
317
+ "conversion-attribution-id": data,
318
+ "advertiser-id": this.config.advertiser,
319
+ })
320
+ )}`;
321
+
275
322
  break;
276
323
 
277
324
  default:
@@ -283,6 +330,7 @@ class ChoreographCreatePixel {
283
330
  sku: data,
284
331
  })
285
332
  )}`;
333
+
286
334
  break;
287
335
  }
288
336
 
@@ -1,4 +1,4 @@
1
- /*! choreograph-create-pixel v1.3.3 2022/12/22 */
1
+ /*! choreograph-create-pixel v1.4.0 2023/02/01 */
2
2
  class ChoreographCreatePixel {
3
3
  constructor(userConfig) {
4
4
  this.previouslyScrapedHash = null;
@@ -12,7 +12,7 @@ class ChoreographCreatePixel {
12
12
  basketed: "πŸ›’",
13
13
  purchased: "🧾",
14
14
  attribution: "πŸ”–",
15
- conversion: "πŸ’΅",
15
+ conversion: "πŸ“ˆ",
16
16
  },
17
17
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
18
18
  before: (data, callback) => callback(data),
@@ -83,8 +83,8 @@ class ChoreographCreatePixel {
83
83
  if (!this.config.debug || this.logs.includes(message)) return;
84
84
 
85
85
  const args = [
86
- `%cβΈ¬ create%c ${this.config.type} ${
87
- this.config.icons[this.config.type]
86
+ `%cβΈ¬ create%c ${this.config.icons[this.config.type]} ${this.config.type}${
87
+ this.config.label ? ` (${this.config.label})` : ""
88
88
  } %c${message}`,
89
89
  "background:black;color:white;border-radius:3px;padding:3px 6px",
90
90
  "font-weight:bold",
@@ -102,16 +102,35 @@ class ChoreographCreatePixel {
102
102
  advertiser: this.config.advertiser,
103
103
  });
104
104
  else if (
105
- !["scraper", "viewed", "basketed", "purchased"].includes(this.config.type)
105
+ ![
106
+ "scraper",
107
+ "viewed",
108
+ "basketed",
109
+ "purchased",
110
+ "attribution",
111
+ "conversion",
112
+ ].includes(this.config.type)
106
113
  )
107
- this.log("error", "Please use scraper, viewed, basketed or purchased", {
108
- type: this.config.type,
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,
109
125
  });
110
126
  else if (!(this.config.url instanceof RegExp))
111
127
  this.log("error", "Please use a regular expression", {
112
128
  url: this.config.url,
113
129
  });
114
- else if (!this.config.scrape)
130
+ else if (
131
+ !["attribution", "conversion"].includes(this.config.type) &&
132
+ !this.config.scrape
133
+ )
115
134
  this.log("error", "Please provide something to scrape", {
116
135
  scrape: this.config.scrape,
117
136
  });
@@ -132,7 +151,8 @@ class ChoreographCreatePixel {
132
151
  "This page has been translated by the browser, and won't be scraped"
133
152
  );
134
153
  else if (this.config.trigger) {
135
- const attribute = `create-${this.config.type}-${this.config.trigger.event}`;
154
+ // TO-DO: replace this.config.type with a unique pixel instance ID
155
+ const elementAttribute = `create-${this.config.type}-${this.config.trigger.event}`;
136
156
 
137
157
  try {
138
158
  let elements =
@@ -143,12 +163,14 @@ class ChoreographCreatePixel {
143
163
  if (!elements.forEach) elements = [elements];
144
164
 
145
165
  elements.forEach((element) => {
146
- if (!element.hasAttribute(attribute)) {
166
+ if (!element.hasAttribute(elementAttribute)) {
147
167
  element.addEventListener(this.config.trigger.event, () =>
148
- this.scrape(element)
168
+ this.config.type === "conversion"
169
+ ? this.convert()
170
+ : this.scrape(element)
149
171
  );
150
172
 
151
- element.setAttribute(attribute, "");
173
+ element.setAttribute(elementAttribute, "");
152
174
  }
153
175
  });
154
176
  } catch (error) {
@@ -157,12 +179,53 @@ class ChoreographCreatePixel {
157
179
  });
158
180
  }
159
181
  } else {
160
- this.scrape();
182
+ switch (this.config.type) {
183
+ case "attribution":
184
+ this.attribute();
185
+ break;
186
+
187
+ case "conversion":
188
+ this.convert();
189
+ break;
190
+
191
+ default:
192
+ this.scrape();
193
+ break;
194
+ }
161
195
  }
162
196
 
163
197
  setTimeout(() => this.cycle(), 750);
164
198
  }
165
199
 
200
+ attribute() {
201
+ const ccpid = this.constructor.getQueryParameter("ccpid");
202
+ if (!ccpid) return this.log("warn", "ccpid query parameter not present");
203
+
204
+ if (
205
+ !/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(
206
+ ccpid
207
+ )
208
+ )
209
+ return this.log(
210
+ "error",
211
+ "ccpid query parameter is not formatted correctly"
212
+ );
213
+
214
+ const storageItemLabel = `choreograph-${this.config.label}`;
215
+ const storedCcpid = localStorage.getItem(storageItemLabel);
216
+ if (storedCcpid !== ccpid) localStorage.setItem(storageItemLabel, ccpid);
217
+ this.log("success", "Successful!", ccpid);
218
+ }
219
+
220
+ convert() {
221
+ const ccpid = localStorage.getItem(`choreograph-${this.config.label}`);
222
+
223
+ if (!ccpid)
224
+ return this.log("warn", `"${this.config.label}" attribution not present`);
225
+
226
+ this.send(ccpid);
227
+ }
228
+
166
229
  scrape(element) {
167
230
  let hasErrors = false;
168
231
  let data;
@@ -176,11 +239,9 @@ class ChoreographCreatePixel {
176
239
  } catch (error) {
177
240
  if (this.config.optional.includes(fieldName)) return undefined;
178
241
 
179
- this.log(
180
- this.config.type === "attribution" ? "warn" : "error",
181
- error.message,
182
- { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
183
- );
242
+ this.log("error", error.message, {
243
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
244
+ });
184
245
 
185
246
  hasErrors = true;
186
247
  }
@@ -193,11 +254,9 @@ class ChoreographCreatePixel {
193
254
  (result == null || result === "") &&
194
255
  !this.config.optional.includes(fieldName)
195
256
  ) {
196
- this.log(
197
- this.config.type === "attribution" ? "warn" : "error",
198
- "Value is empty",
199
- { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
200
- );
257
+ this.log("error", "Value is empty", {
258
+ [fieldName ? `scrape.${fieldName}` : "scrape"]: result,
259
+ });
201
260
 
202
261
  hasErrors = true;
203
262
  }
@@ -205,17 +264,7 @@ class ChoreographCreatePixel {
205
264
  return result;
206
265
  };
207
266
 
208
- if (this.config.type === "conversion") {
209
- data = {
210
- conversion: handleField(this.config.scrape),
211
- attribution: localStorage.getItem("create-attribution-id"),
212
- };
213
-
214
- if (typeof data.attribution !== "string") {
215
- this.log("warn", "There's no attribution ID stored yet");
216
- hasErrors = true;
217
- }
218
- } else if (this.config.type !== "scraper") {
267
+ if (this.config.type !== "scraper") {
219
268
  data = handleField(this.config.scrape);
220
269
  } else {
221
270
  data = Object.keys(this.config.scrape).reduce(
@@ -232,19 +281,7 @@ class ChoreographCreatePixel {
232
281
  if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
233
282
  try {
234
283
  this.config.before(data, (newData) => {
235
- if (this.config.type === "attribution") {
236
- localStorage.setItem("create-attribution-id", data);
237
- this.log("success", "Successful!", { attribution: data });
238
-
239
- try {
240
- this.config.after(data);
241
- } catch (error) {
242
- this.log("error", error.message, {
243
- after: this.config.after,
244
- });
245
- }
246
- } else if (Array.isArray(newData))
247
- newData.forEach((id) => this.send(id));
284
+ if (Array.isArray(newData)) newData.forEach((id) => this.send(id));
248
285
  else this.send(newData);
249
286
  });
250
287
  } catch (error) {
@@ -266,10 +303,20 @@ class ChoreographCreatePixel {
266
303
  url = `https://d.lemonpi.io/scrapes${
267
304
  this.config.debug ? "?validate=true" : ""
268
305
  }`;
306
+
269
307
  break;
270
308
 
271
309
  case "conversion":
272
- url = "https://lemonpi.io/"; // TO-DO
310
+ url = `https://content.lemonpi.io/track/event?e=${encodeURIComponent(
311
+ JSON.stringify({
312
+ version: 1,
313
+ type: "conversion",
314
+ name: this.config.label,
315
+ "conversion-attribution-id": data,
316
+ "advertiser-id": this.config.advertiser,
317
+ })
318
+ )}`;
319
+
273
320
  break;
274
321
 
275
322
  default:
@@ -281,6 +328,7 @@ class ChoreographCreatePixel {
281
328
  sku: data,
282
329
  })
283
330
  )}`;
331
+
284
332
  break;
285
333
  }
286
334
 
@@ -1,2 +1,2 @@
1
- /*! choreograph-create-pixel v1.3.3 2022/12/22 */
2
- var ChoreographCreatePixel=function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(t){for(var r=1;r<arguments.length;r++){var i=null!=arguments[r]?arguments[r]:{};r%2?e(Object(i),!0).forEach((function(e){n(t,e,i[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))}))}return t}function r(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,i(n.key),n)}}function n(e,t,r){return(t=i(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}var o=function(){function e(r){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.previouslyScrapedHash=null,this.logs=[],this.config=t({colors:{error:"red",warn:"orange",success:"green"},icons:{scraper:"πŸ“š",viewed:"πŸ‘€",basketed:"πŸ›’",purchased:"🧾",attribution:"πŸ”–",conversion:"πŸ’΅"},debug:/(pixel|lemonpi)_debug/i.test(location.href),before:function(e,t){return t(e)},after:function(){},optional:[]},r),this.validateConfig()&&this.cycle()}var i,o,c;return i=e,o=[{key:"log",value:function(e,t){var r,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(this.config.debug&&!this.logs.includes(t)){var i=["%cβΈ¬ create%c ".concat(this.config.type," ").concat(this.config.icons[this.config.type]," %c").concat(t),"background:black;color:white;border-radius:3px;padding:3px 6px","font-weight:bold","color:".concat(this.config.colors[e])];n&&i.push(n),(r=console).info.apply(r,i),this.logs.push(t)}}},{key:"validateConfig",value:function(){if("number"!=typeof this.config.advertiser)this.log("error","Please use a number",{advertiser:this.config.advertiser});else if(["scraper","viewed","basketed","purchased"].includes(this.config.type))if(this.config.url instanceof RegExp){if(this.config.scrape)return!0;this.log("error","Please provide something to scrape",{scrape:this.config.scrape})}else this.log("error","Please use a regular expression",{url:this.config.url});else this.log("error","Please use scraper, viewed, basketed or purchased",{type:this.config.type})}},{key:"cycle",value:function(){var e=this;if(this.config.url.test(location.href))if("scraper"===this.config.type&&document.querySelector('html[class*="translated-"]'))this.log("warn","This page has been translated by the browser, and won't be scraped");else if(this.config.trigger){var t="create-".concat(this.config.type,"-").concat(this.config.trigger.event);try{var r="string"==typeof this.config.trigger.elements?document.querySelectorAll(this.config.trigger.elements):this.config.trigger.elements();r.forEach||(r=[r]),r.forEach((function(r){r.hasAttribute(t)||(r.addEventListener(e.config.trigger.event,(function(){return e.scrape(r)})),r.setAttribute(t,""))}))}catch(e){this.log("error",e.message,{"trigger.elements":this.config.trigger.elements})}}else this.scrape();else this.log("warn",'URL pattern does not match "'.concat(location.href,'"'),{url:this.config.url});setTimeout((function(){return e.cycle()}),750)}},{key:"scrape",value:function(e){var r,i=this,o=!1,c=function(t,r){var c=t;if("function"==typeof c)try{c=c(e)}catch(e){if(i.config.optional.includes(r))return;i.log("attribution"===i.config.type?"warn":"error",e.message,n({},r?"scrape.".concat(r):"scrape",c)),o=!0}return"string"==typeof c&&(c=c.replace(/\s+/g," ").trim()),null!=c&&""!==c||i.config.optional.includes(r)||(i.log("attribution"===i.config.type?"warn":"error","Value is empty",n({},r?"scrape.".concat(r):"scrape",c)),o=!0),c};"conversion"===this.config.type?"string"!=typeof(r={conversion:c(this.config.scrape),attribution:localStorage.getItem("create-attribution-id")}).attribution&&(this.log("warn","There's no attribution ID stored yet"),o=!0):r="scraper"!==this.config.type?c(this.config.scrape):Object.keys(this.config.scrape).reduce((function(e,r){return t(t({},e),{},n({},r,c(i.config.scrape[r],r)))}),{});var s=JSON.stringify(r);if(!o&&this.previouslyScrapedHash!==s){try{this.config.before(r,(function(e){if("attribution"===i.config.type){localStorage.setItem("create-attribution-id",r),i.log("success","Successful!",{attribution:r});try{i.config.after(r)}catch(e){i.log("error",e.message,{after:i.config.after})}}else Array.isArray(e)?e.forEach((function(e){return i.send(e)})):i.send(e)}))}catch(e){this.log("error",e.message,{before:this.config.before})}this.previouslyScrapedHash=s,this.logs.length=0}}},{key:"send",value:function(e){var r,n=this;switch(this.config.type){case"scraper":r="https://d.lemonpi.io/scrapes".concat(this.config.debug?"?validate=true":"");break;case"conversion":r="https://lemonpi.io/";break;default:r="https://d.lemonpi.io/a/".concat(this.config.advertiser,"/product/event?e=").concat(encodeURIComponent(JSON.stringify({"event-type":"product-".concat(this.config.type),sku:e})))}"scraper"===this.config.type||this.config.debug?fetch(r,"scraper"===this.config.type?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({"advertiser-id":this.config.advertiser,sku:e.sku,fields:t(t({},e),{},{sku:void 0})})}:null).then((function(t){return t.json().then((function(r){if(t.ok){n.log("warn","Successful, with warnings. Details:",r);try{n.config.after(e)}catch(e){n.log("error",e.message,{after:n.config.after})}}else n.log("error","Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),r);n.logs.length=0})).catch((function(){if(t.ok){n.log("success","Successful!",["viewed","basketed","purchased"].includes(n.config.type)?{sku:e}:e);try{n.config.after(e)}catch(e){n.log("error",e.message,{after:n.config.after})}}else n.log("error","Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),t);n.logs.length=0}))})).catch((function(e){n.log("error","Failed: ".concat(e.message)),n.logs.length=0})):(new Image).src=r}}],c=[{key:"getUrl",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.allowedQueryParameters,r=void 0===t?[]:t,n=e.allowHash,i=void 0!==n&&n,o="".concat(location.protocol,"//").concat(location.host).concat(location.pathname),c=new URLSearchParams(location.search);return r.reduce((function(e,t){var r=e?"&":"?",n=encodeURI(t),i=c.get(t);return null!=i?(o+="".concat(r).concat(n).concat(""===i?"":"=".concat(encodeURI(i))),!0):e}),!1),i&&(o+=location.hash),o}},{key:"getAllPathSegments",value:function(){return location.pathname.split("/").filter((function(e){return e})).map((function(e){return decodeURI(e)}))}},{key:"getPathSegment",value:function(e){return this.getAllPathSegments()[e]}},{key:"getAllQueryParameters",value:function(){return location.search.replace(/^\?/,"").split("&").filter((function(e){return e})).reduce((function(e,r){return t(t({},e),{},n({},decodeURI(r.split("=")[0]),decodeURI(r.split("=")[1]||"")))}),{})}},{key:"getQueryParameter",value:function(e){return this.getAllQueryParameters()[e]}}],o&&r(i.prototype,o),c&&r(i,c),Object.defineProperty(i,"prototype",{writable:!1}),e}();return o}();
1
+ /*! choreograph-create-pixel v1.4.0 2023/02/01 */
2
+ var ChoreographCreatePixel=function(){"use strict";function e(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function t(t){for(var r=1;r<arguments.length;r++){var i=null!=arguments[r]?arguments[r]:{};r%2?e(Object(i),!0).forEach((function(e){n(t,e,i[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(i,e))}))}return t}function r(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,i(n.key),n)}}function n(e,t,r){return(t=i(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function i(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!=typeof n)return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:String(t)}var o=function(){function e(r){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.previouslyScrapedHash=null,this.logs=[],this.config=t({colors:{error:"red",warn:"orange",success:"green"},icons:{scraper:"πŸ“š",viewed:"πŸ‘€",basketed:"πŸ›’",purchased:"🧾",attribution:"πŸ”–",conversion:"πŸ“ˆ"},debug:/(pixel|lemonpi)_debug/i.test(location.href),before:function(e,t){return t(e)},after:function(){},optional:[]},r),this.validateConfig()&&this.cycle()}var i,o,c;return i=e,o=[{key:"log",value:function(e,t){var r,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(this.config.debug&&!this.logs.includes(t)){var i=["%cβΈ¬ create%c ".concat(this.config.icons[this.config.type]," ").concat(this.config.type).concat(this.config.label?" (".concat(this.config.label,")"):""," %c").concat(t),"background:black;color:white;border-radius:3px;padding:3px 6px","font-weight:bold","color:".concat(this.config.colors[e])];n&&i.push(n),(r=console).info.apply(r,i),this.logs.push(t)}}},{key:"validateConfig",value:function(){if("number"!=typeof this.config.advertiser)this.log("error","Please use a number",{advertiser:this.config.advertiser});else if(["scraper","viewed","basketed","purchased","attribution","conversion"].includes(this.config.type))if(["attribution","conversion"].includes(this.config.type)&&"string"!=typeof this.config.label)this.log("error","Please use a string",{label:this.config.label});else if(this.config.url instanceof RegExp){if(["attribution","conversion"].includes(this.config.type)||this.config.scrape)return!0;this.log("error","Please provide something to scrape",{scrape:this.config.scrape})}else this.log("error","Please use a regular expression",{url:this.config.url});else this.log("error","Please use scraper, viewed, basketed, purchased, attribution or conversion",{type:this.config.type})}},{key:"cycle",value:function(){var e=this;if(this.config.url.test(location.href))if("scraper"===this.config.type&&document.querySelector('html[class*="translated-"]'))this.log("warn","This page has been translated by the browser, and won't be scraped");else if(this.config.trigger){var t="create-".concat(this.config.type,"-").concat(this.config.trigger.event);try{var r="string"==typeof this.config.trigger.elements?document.querySelectorAll(this.config.trigger.elements):this.config.trigger.elements();r.forEach||(r=[r]),r.forEach((function(r){r.hasAttribute(t)||(r.addEventListener(e.config.trigger.event,(function(){return"conversion"===e.config.type?e.convert():e.scrape(r)})),r.setAttribute(t,""))}))}catch(e){this.log("error",e.message,{"trigger.elements":this.config.trigger.elements})}}else switch(this.config.type){case"attribution":this.attribute();break;case"conversion":this.convert();break;default:this.scrape()}else this.log("warn",'URL pattern does not match "'.concat(location.href,'"'),{url:this.config.url});setTimeout((function(){return e.cycle()}),750)}},{key:"attribute",value:function(){var e=this.constructor.getQueryParameter("ccpid");if(!e)return this.log("warn","ccpid query parameter not present");if(!/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/.test(e))return this.log("error","ccpid query parameter is not formatted correctly");var t="choreograph-".concat(this.config.label);localStorage.getItem(t)!==e&&localStorage.setItem(t,e),this.log("success","Successful!",e)}},{key:"convert",value:function(){var e=localStorage.getItem("choreograph-".concat(this.config.label));if(!e)return this.log("warn",'"'.concat(this.config.label,'" attribution not present'));this.send(e)}},{key:"scrape",value:function(e){var r,i=this,o=!1,c=function(t,r){var c=t;if("function"==typeof c)try{c=c(e)}catch(e){if(i.config.optional.includes(r))return;i.log("error",e.message,n({},r?"scrape.".concat(r):"scrape",c)),o=!0}return"string"==typeof c&&(c=c.replace(/\s+/g," ").trim()),null!=c&&""!==c||i.config.optional.includes(r)||(i.log("error","Value is empty",n({},r?"scrape.".concat(r):"scrape",c)),o=!0),c};r="scraper"!==this.config.type?c(this.config.scrape):Object.keys(this.config.scrape).reduce((function(e,r){return t(t({},e),{},n({},r,c(i.config.scrape[r],r)))}),{});var s=JSON.stringify(r);if(!o&&this.previouslyScrapedHash!==s){try{this.config.before(r,(function(e){Array.isArray(e)?e.forEach((function(e){return i.send(e)})):i.send(e)}))}catch(e){this.log("error",e.message,{before:this.config.before})}this.previouslyScrapedHash=s,this.logs.length=0}}},{key:"send",value:function(e){var r,n=this;switch(this.config.type){case"scraper":r="https://d.lemonpi.io/scrapes".concat(this.config.debug?"?validate=true":"");break;case"conversion":r="https://content.lemonpi.io/track/event?e=".concat(encodeURIComponent(JSON.stringify({version:1,type:"conversion",name:this.config.label,"conversion-attribution-id":e,"advertiser-id":this.config.advertiser})));break;default:r="https://d.lemonpi.io/a/".concat(this.config.advertiser,"/product/event?e=").concat(encodeURIComponent(JSON.stringify({"event-type":"product-".concat(this.config.type),sku:e})))}"scraper"===this.config.type||this.config.debug?fetch(r,"scraper"===this.config.type?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({"advertiser-id":this.config.advertiser,sku:e.sku,fields:t(t({},e),{},{sku:void 0})})}:null).then((function(t){return t.json().then((function(r){if(t.ok){n.log("warn","Successful, with warnings. Details:",r);try{n.config.after(e)}catch(e){n.log("error",e.message,{after:n.config.after})}}else n.log("error","Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),r);n.logs.length=0})).catch((function(){if(t.ok){n.log("success","Successful!",["viewed","basketed","purchased"].includes(n.config.type)?{sku:e}:e);try{n.config.after(e)}catch(e){n.log("error",e.message,{after:n.config.after})}}else n.log("error","Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),t);n.logs.length=0}))})).catch((function(e){n.log("error","Failed: ".concat(e.message)),n.logs.length=0})):(new Image).src=r}}],c=[{key:"getUrl",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.allowedQueryParameters,r=void 0===t?[]:t,n=e.allowHash,i=void 0!==n&&n,o="".concat(location.protocol,"//").concat(location.host).concat(location.pathname),c=new URLSearchParams(location.search);return r.reduce((function(e,t){var r=e?"&":"?",n=encodeURI(t),i=c.get(t);return null!=i?(o+="".concat(r).concat(n).concat(""===i?"":"=".concat(encodeURI(i))),!0):e}),!1),i&&(o+=location.hash),o}},{key:"getAllPathSegments",value:function(){return location.pathname.split("/").filter((function(e){return e})).map((function(e){return decodeURI(e)}))}},{key:"getPathSegment",value:function(e){return this.getAllPathSegments()[e]}},{key:"getAllQueryParameters",value:function(){return location.search.replace(/^\?/,"").split("&").filter((function(e){return e})).reduce((function(e,r){return t(t({},e),{},n({},decodeURI(r.split("=")[0]),decodeURI(r.split("=")[1]||"")))}),{})}},{key:"getQueryParameter",value:function(e){return this.getAllQueryParameters()[e]}}],o&&r(i.prototype,o),c&&r(i,c),Object.defineProperty(i,"prototype",{writable:!1}),e}();return o}();
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "choreograph-create-pixel",
3
3
  "description": "This library lets you apply best practises to Choreograph Create pixel development and implementation.",
4
- "version": "1.3.3",
4
+ "version": "1.4.0",
5
+ "type": "module",
5
6
  "keywords": [
6
7
  "wpp",
7
8
  "groupm",
8
9
  "lemonpi",
9
10
  "odc",
10
- "opendc"
11
+ "opendc",
12
+ "greenhouse"
11
13
  ],
12
14
  "author": "Rick Stevens <rick.stevens@choreograph.com> (https://lemonpi.io)",
13
15
  "repository": "gitlab:2sixty/choreograph-create/solutions/choreograph-create-pixel",
@@ -28,16 +30,18 @@
28
30
  "prepublishOnly": "npm run build"
29
31
  },
30
32
  "devDependencies": {
31
- "@babel/core": "^7.20.7",
33
+ "@babel/core": "^7.20.12",
34
+ "@babel/eslint-parser": "^7.19.1",
35
+ "@babel/plugin-syntax-import-assertions": "^7.20.0",
32
36
  "@babel/preset-env": "^7.20.2",
33
37
  "@rollup/plugin-babel": "^6.0.3",
34
- "@rollup/plugin-eslint": "^9.0.1",
35
- "@rollup/plugin-terser": "^0.2.1",
36
- "eslint": "^8.30.0",
37
- "eslint-config-prettier": "^8.5.0",
38
+ "@rollup/plugin-eslint": "^9.0.3",
39
+ "@rollup/plugin-terser": "^0.4.0",
40
+ "eslint": "^8.33.0",
41
+ "eslint-config-prettier": "^8.6.0",
38
42
  "eslint-plugin-prettier": "^4.2.1",
39
43
  "moment": "^2.29.4",
40
- "prettier": "^2.8.1",
41
- "rollup": "^2.79.1"
44
+ "prettier": "^2.8.3",
45
+ "rollup": "^3.12.0"
42
46
  }
43
47
  }