choreograph-create-pixel 1.1.0 β†’ 1.3.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
@@ -1,175 +1,166 @@
1
1
  # Choreograph Create Pixel
2
2
 
3
- This library lets you apply best practises to [Choreograph Create](https://www.lemonpi.io/) pixel development and implementation.
3
+ This library lets you apply best practises to [Create](https://www.lemonpi.io/) pixel development and implementation.
4
4
 
5
5
  ## Features
6
6
 
7
- - [x] Supports **scrape**, **view**, **basket**, and **purchase** pixels
8
- - [x] Supports dynamic page changes
7
+ - [x] Supports [scraper](#scraper-pixels), [segment](#segment-pixels) and [conversion](#conversion-pixels) (coming soon) pixels
8
+ - [x] Supports dynamic page content updates
9
+ - [x] Prevents scraping browser-translated content
9
10
  - [x] Continuous URL validation
10
- - [x] Optionally trigger by user interaction
11
- - [x] Prevents scraping browser-translated pages
12
- - [x] Console debugger
11
+ - [x] User interaction triggers
12
+ - [x] [Console debugger](#debugging)
13
13
 
14
- ## Quickstart
14
+ ### Scraper pixels
15
15
 
16
- The following theoretical snippet could be embedded on every page of _example.com_, and would automatically determine when to enable through URL validation.
16
+ <small>Type: `scraper`</small>
17
17
 
18
- ### Browser example
18
+ Scraper pixels are used to scrape (read) data from websites, and store that data as products within an advertiser's product store.
19
19
 
20
- ```html
21
- <script src="https://cdn.jsdelivr.net/npm/choreograph-create-pixel@1"></script>
22
- <script>
23
- // Scrape pixel
24
- new ChoreographCreatePixel({
25
- // Who is the client? (advertiser ID)
26
- who: 0,
27
-
28
- // What kind of pixel is this?
29
- what: "scrape",
30
-
31
- // Where should the pixel be enabled?
32
- where: /example\.com\/products\/\d/,
33
-
34
- // Which data should be scraped?
35
- which: {
36
- // Data layer example
37
- sku: function () {
38
- return window.dataLayer[0].product.sku;
39
- },
40
-
41
- // DOM example
42
- name: function () {
43
- return document.querySelector("#product-name").innerText;
44
- },
45
-
46
- // Helper example
47
- url: ChoreographCreatePixel.getUrl,
48
- },
49
- });
20
+ ### Segment pixels
50
21
 
51
- // View segment pixel
52
- new ChoreographCreatePixel({
53
- who: 0,
54
- what: "view",
55
- where: /example\.com\/products\/\d/,
56
-
57
- which: {
58
- sku: function () {
59
- return window.dataLayer[0].product.sku;
60
- },
61
- },
62
- });
22
+ <small>Types: `viewed`, `basketed` and `purchased`</small>
63
23
 
64
- // Basket segment pixel
65
- new ChoreographCreatePixel({
66
- who: 0,
67
- what: "basket",
68
- where: /example\.com/,
69
-
70
- // (Optional) When should the pixel be enabled?
71
- when: {
72
- listener: "click",
73
-
74
- elements: function () {
75
- return document.querySelectorAll("button.basket[data-sku]");
76
- },
77
- },
78
-
79
- which: {
80
- // The "element" parameter only exists while using a "when" condition
81
- sku: function (element) {
82
- return element.getAttribute("data-sku");
83
- },
84
- },
85
- });
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.
86
25
 
87
- // Purchase segment pixel
88
- new ChoreographCreatePixel({
89
- who: 0,
90
- what: "purchase",
91
- where: /example\.com\/cart/,
92
-
93
- when: {
94
- listener: "click",
95
-
96
- elements: function () {
97
- return document.querySelectorAll("button.checkout");
98
- },
99
- },
100
-
101
- which: {
102
- // which.sku supports returning an array of strings
103
- sku: function () {
104
- return window.dataLayer[0].cart.map(function (product) {
105
- return product.sku;
106
- });
107
- },
108
- },
109
- });
110
- </script>
111
- ```
26
+ ### Conversion pixels
27
+
28
+ _(coming soon)_
29
+
30
+ ## Quickstart
112
31
 
113
- ### ES module example
32
+ The following theoretical snippet includes all pixel types, can be embedded on every page of _example.com_, and will automatically determine which pixel to enable and disable through continuous URL validation.
33
+
34
+ ### ES module
114
35
 
115
36
  ```js
116
37
  import Pixel from "choreograph-create-pixel";
117
38
 
39
+ // Scraper pixel
40
+ new Pixel({
41
+ advertiser: 0,
42
+ type: "scraper",
43
+
44
+ // Where on the website should this pixel be enabled?
45
+ url: /example\.com\/products\/\d/,
46
+
47
+ // Functions here are continuously being evaluated for value changes
48
+ scrape: {
49
+ // Data layer example
50
+ sku: () => window.dataLayer[0].product.sku,
51
+
52
+ // DOM example
53
+ name: () => document.querySelector("#product-name").innerText,
54
+
55
+ // Helper example
56
+ url: Pixel.getUrl,
57
+ },
58
+ });
59
+
60
+ // Viewed segment pixel
118
61
  new Pixel({
119
- ...
62
+ advertiser: 0,
63
+ type: "viewed",
64
+ url: /example\.com\/products\/\d/,
65
+
66
+ // Segment pixels only require an SKU to be scraped
67
+ scrape: () => window.dataLayer[0].product.sku,
120
68
  });
121
69
 
70
+ // Basketed segment pixel
122
71
  new Pixel({
123
- ...
72
+ advertiser: 0,
73
+ type: "basketed",
74
+ url: /example\.com/,
75
+
76
+ // [Optional] Trigger by DOM events, rather than scrape content updates
77
+ trigger: {
78
+ event: "click",
79
+ elements: () => document.querySelectorAll("button.basket[data-sku]"),
80
+ },
81
+
82
+ // The "element" parameter only becomes available when using a trigger
83
+ scrape: (element) => element.getAttribute("data-sku"),
84
+ });
85
+
86
+ // Purchased segment pixel
87
+ new Pixel({
88
+ advertiser: 0,
89
+ type: "purchased",
90
+ url: /example\.com\/cart/,
91
+
92
+ trigger: {
93
+ event: "click",
94
+ elements: () => document.querySelectorAll("button.checkout"),
95
+ },
96
+
97
+ // Segment pixels support multiple SKUs
98
+ scrape: () => window.dataLayer[0].cart.map((product) => product.sku),
124
99
  });
125
100
  ```
126
101
 
127
- > ES modules require manual bundling and transpiling before they can be safely used as browser scripts.
102
+ > ES modules require manual bundling and transpiling before they can be safely embedded on websites.
103
+
104
+ ### CDN library
105
+
106
+ This implementation method allows for direct embedding on pages without the need for prior bundling or transpiling.
107
+
108
+ ```html
109
+ <script src="https://cdn.jsdelivr.net/npm/choreograph-create-pixel@1"></script>
110
+ <script>
111
+ new ChoreographCreatePixel({
112
+ // ...
113
+ });
114
+
115
+ new ChoreographCreatePixel({
116
+ // ...
117
+ });
118
+ </script>
119
+ ```
128
120
 
129
121
  ## Debugging
130
122
 
131
- Enable the console debugger by adding `?pixel_debug` or `#pixel_debug` to the page's URL.
123
+ Enable browser console debugging by adding `?pixel_debug` or `#pixel_debug` to the page's URL.
132
124
 
133
125
  ## Configuration
134
126
 
135
- | Property | Type | Description | Default |
136
- | --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
137
- | `who` | Number | You can retrieve the advertiser ID from the URL in the [Manage platform](https://manage.lemonpi.io/) _(manage.lemonpi.io/r/AGENCY_ID/advertiser/`ADVERTISER_ID`)_. | _Required_ |
138
- | `what` | String | Pixel type. One of: `"scrape"`, `"view"`, `"basket"`, or `"purchase"`. | _Required_ |
139
- | `where` | RegExp | Disables pixels on page URLs that are not matched by this pattern. | _Required_ |
140
- | `which` | Object | An object holding the data fields to be scraped. | _Required_ |
141
- | `which.sku` | String, Array, or Function | `sku` is the only required data field, as it's the product's unique identifier. In case of segment pixels (`what` equals `"view"`, `"basket"`, or `"purchase"`), an array of strings is allowed to trigger multiple pixel executions. | _Required_ |
142
- | `which.*` | String, Number, Boolean, or Function | `*` should always match with existing field names in the advertiser's product store. | `undefined` |
143
- | `when` | Object | Use this property when you want to enable the pixel only after a specific DOM event. | `undefined` |
144
- | `when.listener` | String | DOM event type. [List of supported values.](https://www.w3schools.com/jsref/dom_obj_event.asp) | _Required_ when using `when` |
145
- | `when.elements` | Function | This function should return DOM element(s) to apply the listener to. | _Required_ when using `when` |
146
- | `optional` | Array | An array of field names (strings) that are allowed to have empty values. | `[]` |
147
- | `before` | Function | Lifecycle function that's executed just before the pixel's data is sent. | `function (data, callback) { callback(data); }` |
148
- | `after` | Function | Lifecycle function that's executed just after the pixel's data is sent. | `function (data) {}` |
127
+ | Property | Type | Description | Default |
128
+ | ------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
129
+ | `advertiser` | Number | You can retrieve the advertiser ID from the URL in Create: _manage.lemonpi.io/r/AGENCY_ID/advertiser/`ADVERTISER_ID`_. | _Required_ |
130
+ | `type` | String | Pixel type. One of: `"scraper"`, `"viewed"`, `"basketed"` or `"purchased"`. | _Required_ |
131
+ | `url` | RegExp | Only enables the pixel on page URLs that are matched by this pattern. | _Required_ |
132
+ | `scrape`<br>_(segments only)_ | String, Array or Function | Scrapes the product's SKU for segments. | _Required_ |
133
+ | `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_ |
134
+ | `trigger` | Object | Adds an event listener to enable the pixel only on a specific DOM event, instead of on scrape content updates. | `undefined` |
135
+ | `trigger.event` | String | DOM event type. [List of supported values.](https://www.w3schools.com/jsref/dom_obj_event.asp) | _Required_ |
136
+ | `trigger.elements` | Function | This function should return DOM element(s) to apply the event listener to. | _Required_ |
137
+ | `optional`<br>_(scrapers only)_ | Array | An array of field names (as used in `scrape.*`) that are allowed to have empty values. | `[]` |
138
+ | `before` | Function | A custom function that's executed just before the pixel is executed. | `(scrapedData, callback) => { callback(scrapedData); }` |
139
+ | `after` | Function | A custom function that's executed just after the pixel has been executed. | `(scrapedData) => {}` |
149
140
 
150
141
  ## Static methods (helpers)
151
142
 
152
- ### `getUrl({ allowedQueryParameters, allowHash })`
143
+ ### `.getUrl({ allowedQueryParameters, allowHash })`
153
144
 
154
- Returns the bare page URL without query parameters or location hash. This is recommended to prevent scraping unwanted UTM tagging.
145
+ Returns the bare page URL without query parameters or location hash. This is highly recommended to exclude (UTM) tagging, or other unwanted side effects.
155
146
 
156
- | Property | Type | Description | Default |
157
- | ------------------------ | ------- | --------------------------------------------------------------- | ------- |
158
- | `allowedQueryParameters` | Array | Explicitly allow query parameters in the resulting URL. | `[]` |
159
- | `allowHash` | Boolean | Wether or not to include the location hash (`#foo`) of the URL. | `false` |
147
+ | Property | Type | Description | Default |
148
+ | ------------------------ | ------- | ----------------------------------------------------------------- | ------- |
149
+ | `allowedQueryParameters` | Array | Explicitly allow query parameters in the URL. | `[]` |
150
+ | `allowHash` | Boolean | Explicitly allow including the location hash (`#foo`) in the URL. | `false` |
160
151
 
161
- ### `getAllPathSegments()`
152
+ ### `.getAllPathSegments()`
162
153
 
163
154
  Retrieves all path segments from the URL as an array. E.g. _http://www.example.com/foo/bar_ returns `["foo", "bar"]`.
164
155
 
165
- ### `getPathSegment(index)`
156
+ ### `.getPathSegment(index)`
166
157
 
167
- Retrieves a specific segment from the URL. E.g. `getPathSegment(0)` with URL _http://www.example.com/foo/bar_ returns `"foo"`.
158
+ Retrieves a specific segment from the URL. E.g. `getPathSegment(0)` on _http://www.example.com/foo/bar_ returns `"foo"`.
168
159
 
169
- ### `getAllQueryParameters()`
160
+ ### `.getAllQueryParameters()`
170
161
 
171
162
  Retrieves all query string parameters from the URL as an object. E.g. _http://www.example.com/?foo=bar_ returns `{ foo: "bar" }`.
172
163
 
173
- ### `getQueryParameter(key)`
164
+ ### `.getQueryParameter(key)`
174
165
 
175
- Retrieves a specific query parameter from the URL. E.g. `getQueryParameter('foo')` with URL _http://www.example.com/?foo=bar_ returns `"bar"`.
166
+ Retrieves a specific query parameter from the URL. E.g. `getQueryParameter('foo')` on _http://www.example.com/?foo=bar_ returns `"bar"`.
@@ -1,24 +1,21 @@
1
- /*! choreograph-create-pixel v1.1.0 2022/09/26 */
1
+ /*! choreograph-create-pixel v1.3.0 2022/10/28 */
2
2
  'use strict';
3
3
 
4
4
  class ChoreographCreatePixel {
5
5
  constructor(userConfig) {
6
- this.previouslyScrapedJsonString = null;
6
+ this.previouslyScrapedHash = null;
7
7
  this.logs = [];
8
8
 
9
- this.settings = {
10
- colors: { error: "#f44336", warn: "#ffa726", success: "#66bb6a" },
11
- icons: { scrape: "πŸ”", view: "πŸ‘€", basket: "πŸ›’", purchase: "🧾" },
12
- titleCss: "font:700 80% HelveticaNeue;margin:12px 0 8px",
13
- messageCss: "font:500 120% HelveticaNeue;margin-bottom:12px",
14
- eventTypes: {
15
- view: "product-viewed",
16
- basket: "product-basketed",
17
- purchase: "product-purchased",
18
- },
19
- };
20
-
21
9
  this.config = {
10
+ colors: { error: "red", warn: "orange", success: "green" },
11
+ icons: {
12
+ scraper: "πŸ“š",
13
+ viewed: "πŸ‘€",
14
+ basketed: "πŸ›’",
15
+ purchased: "🧾",
16
+ attribution: "πŸ”–",
17
+ conversion: "πŸ’΅",
18
+ },
22
19
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
23
20
  before: (data, callback) => callback(data),
24
21
  after: () => {},
@@ -88,14 +85,12 @@ class ChoreographCreatePixel {
88
85
  if (!this.config.debug || this.logs.includes(message)) return;
89
86
 
90
87
  const args = [
91
- `%c${
92
- this.settings.icons[this.config.what]
93
- } ${this.config.what.toUpperCase()} PIXEL DEBUG%c\n%c${message}`,
94
- this.settings.titleCss,
95
- "",
96
- this.settings.colors[level]
97
- ? `${this.settings.messageCss};color:${this.settings.colors[level]}`
98
- : this.settings.messageCss,
88
+ `%cβΈ¬ create%c ${this.config.type} ${
89
+ this.config.icons[this.config.type]
90
+ } %c${message}`,
91
+ "background:black;color:white;border-radius:3px;padding:3px 6px",
92
+ "font-weight:bold",
93
+ `color:${this.config.colors[level]}`,
99
94
  ];
100
95
 
101
96
  if (data) args.push(data);
@@ -104,58 +99,59 @@ class ChoreographCreatePixel {
104
99
  }
105
100
 
106
101
  validateConfig() {
107
- if (typeof this.config.who !== "number")
108
- this.log("error", "Please use a number", { who: this.config.who });
102
+ if (typeof this.config.advertiser !== "number")
103
+ this.log("error", "Please use a number", {
104
+ advertiser: this.config.advertiser,
105
+ });
109
106
  else if (
110
- !["scrape", "view", "basket", "purchase"].includes(this.config.what)
107
+ !["scraper", "viewed", "basketed", "purchased"].includes(this.config.type)
111
108
  )
112
- this.log("error", "Please use scrape, view, basket, or purchase", {
113
- what: this.config.what,
109
+ this.log("error", "Please use scraper, viewed, basketed or purchased", {
110
+ type: this.config.type,
114
111
  });
115
- else if (!(this.config.where instanceof RegExp))
112
+ else if (!(this.config.url instanceof RegExp))
116
113
  this.log("error", "Please use a regular expression", {
117
- where: this.config.where,
114
+ url: this.config.url,
115
+ });
116
+ else if (!this.config.scrape)
117
+ this.log("error", "Please provide something to scrape", {
118
+ scrape: this.config.scrape,
118
119
  });
119
- else if (typeof this.config.which !== "object" || !this.config.which.sku)
120
- this.log("error", "Please provide an SKU", { which: this.config.which });
121
120
  else return true;
122
121
  }
123
122
 
124
123
  cycle() {
125
- if (!this.config.where.test(location.href)) {
126
- this.log("warn", `Pattern does not match "${location.href}"`, {
127
- where: this.config.where,
124
+ if (!this.config.url.test(location.href))
125
+ this.log("warn", `URL pattern does not match "${location.href}"`, {
126
+ url: this.config.url,
128
127
  });
129
- } else if (
130
- this.config.what === "scrape" &&
128
+ else if (
129
+ this.config.type === "scraper" &&
131
130
  document.querySelector('html[class*="translated-"]')
132
- ) {
131
+ )
133
132
  this.log(
134
133
  "warn",
135
- "This page has been translated by the browser, and will be excluded"
134
+ "This page has been translated by the browser, and won't be scraped"
136
135
  );
137
- } else if (this.config.when) {
136
+ else if (this.config.trigger) {
137
+ const attribute = `create-${this.config.type}-${this.config.trigger.event}`;
138
+
138
139
  try {
139
- let elements = this.config.when.elements();
140
+ let elements = this.config.trigger.elements();
140
141
  if (!elements.forEach) elements = [elements];
141
142
 
142
143
  elements.forEach((element) => {
143
- if (
144
- !element.hasAttribute(`choreograph-${this.config.when.listener}`)
145
- ) {
146
- element.addEventListener(this.config.when.listener, () =>
144
+ if (!element.hasAttribute(attribute)) {
145
+ element.addEventListener(this.config.trigger.event, () =>
147
146
  this.scrape(element)
148
147
  );
149
148
 
150
- element.setAttribute(
151
- `choreograph-${this.config.when.listener}`,
152
- ""
153
- );
149
+ element.setAttribute(attribute, "");
154
150
  }
155
151
  });
156
152
  } catch (error) {
157
153
  this.log("error", error.message, {
158
- when: { elements: this.config.when.elements },
154
+ "trigger.elements": this.config.trigger.elements,
159
155
  });
160
156
  }
161
157
  } else {
@@ -166,46 +162,86 @@ class ChoreographCreatePixel {
166
162
  }
167
163
 
168
164
  scrape(element) {
169
- const data = {};
170
165
  let hasErrors = false;
166
+ let data;
171
167
 
172
- Object.keys(this.config.which).forEach((fieldName) => {
173
- data[fieldName] = this.config.which[fieldName];
168
+ const handleField = (field, fieldName) => {
169
+ let result = field;
174
170
 
175
- if (typeof data[fieldName] === "function") {
171
+ if (typeof result === "function") {
176
172
  try {
177
- data[fieldName] = data[fieldName](element);
173
+ result = result(element);
178
174
  } catch (error) {
179
- if (!this.config.optional.includes(fieldName)) {
180
- this.log("error", error.message, {
181
- which: { [fieldName]: this.config.which[fieldName] },
182
- });
175
+ if (this.config.optional.includes(fieldName)) return null;
183
176
 
184
- hasErrors = true;
185
- } else delete data[fieldName];
177
+ this.log(
178
+ this.config.type === "attribution" ? "warn" : "error",
179
+ error.message,
180
+ { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
181
+ );
182
+
183
+ hasErrors = true;
186
184
  }
187
185
  }
188
186
 
189
- if (typeof data[fieldName] === "string")
190
- data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
187
+ if (typeof result === "string")
188
+ result = result.replace(/\s+/g, " ").trim();
191
189
 
192
- if (data[fieldName] == null || data[fieldName] === "")
193
- if (!this.config.optional.includes(fieldName)) {
194
- this.log("error", "This required field's value is empty", {
195
- which: { [fieldName]: data[fieldName] },
196
- });
190
+ if (result == null || result === "") {
191
+ if (this.config.optional.includes(fieldName)) return null;
197
192
 
198
- hasErrors = true;
199
- } else data[fieldName] = null;
200
- });
193
+ this.log(
194
+ this.config.type === "attribution" ? "warn" : "error",
195
+ "Value is empty",
196
+ { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
197
+ );
198
+
199
+ hasErrors = true;
200
+ }
201
+
202
+ return result;
203
+ };
204
+
205
+ if (this.config.type === "conversion") {
206
+ data = {
207
+ conversion: handleField(this.config.scrape),
208
+ attribution: localStorage.getItem("create-attribution-id"),
209
+ };
210
+
211
+ if (typeof data.attribution !== "string") {
212
+ this.log("warn", "There's no attribution ID stored yet");
213
+ hasErrors = true;
214
+ }
215
+ } else if (this.config.type !== "scraper") {
216
+ data = handleField(this.config.scrape);
217
+ } else {
218
+ data = Object.keys(this.config.scrape).reduce(
219
+ (acc, fieldName) => ({
220
+ ...acc,
221
+ [fieldName]: handleField(this.config.scrape[fieldName], fieldName),
222
+ }),
223
+ {}
224
+ );
225
+ }
201
226
 
202
- const jsonString = JSON.stringify(data);
227
+ const dataHash = JSON.stringify(data);
203
228
 
204
- if (!hasErrors && this.previouslyScrapedJsonString !== jsonString) {
229
+ if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
205
230
  try {
206
231
  this.config.before(data, (newData) => {
207
- if (this.config.what !== "scrape" && Array.isArray(newData.sku))
208
- newData.sku.forEach((sku) => this.send({ ...newData, sku }));
232
+ if (this.config.type === "attribution") {
233
+ localStorage.setItem("create-attribution-id", data);
234
+ this.log("success", "Successful!", { attribution: data });
235
+
236
+ try {
237
+ this.config.after(data);
238
+ } catch (error) {
239
+ this.log("error", error.message, {
240
+ after: this.config.after,
241
+ });
242
+ }
243
+ } else if (Array.isArray(newData))
244
+ newData.forEach((id) => this.send(id));
209
245
  else this.send(newData);
210
246
  });
211
247
  } catch (error) {
@@ -214,42 +250,50 @@ class ChoreographCreatePixel {
214
250
  });
215
251
  }
216
252
 
217
- this.previouslyScrapedJsonString = jsonString;
253
+ this.previouslyScrapedHash = dataHash;
218
254
  this.logs.length = 0;
219
255
  }
220
256
  }
221
257
 
222
258
  send(data) {
223
- const fields = { ...data };
224
- delete fields.sku;
225
-
226
- const url =
227
- this.config.what === "scrape"
228
- ? `https://d.lemonpi.io/scrapes${
229
- this.config.debug ? "?validate=true" : ""
230
- }`
231
- : `https://d.lemonpi.io/a/${
232
- this.config.who
233
- }/product/event?e=${encodeURIComponent(
234
- JSON.stringify({
235
- "event-type": this.settings.eventTypes[this.config.what],
236
- sku: data.sku,
237
- })
238
- )}`;
259
+ let url;
260
+
261
+ switch (this.config.type) {
262
+ case "scraper":
263
+ url = `https://d.lemonpi.io/scrapes${
264
+ this.config.debug ? "?validate=true" : ""
265
+ }`;
266
+ break;
267
+
268
+ case "conversion":
269
+ url = "https://lemonpi.io/"; // TO-DO
270
+ break;
271
+
272
+ default:
273
+ url = `https://d.lemonpi.io/a/${
274
+ this.config.advertiser
275
+ }/product/event?e=${encodeURIComponent(
276
+ JSON.stringify({
277
+ "event-type": `product-${this.config.type}`,
278
+ sku: data,
279
+ })
280
+ )}`;
281
+ break;
282
+ }
239
283
 
240
- if (this.config.what !== "scrape" && !this.config.debug)
284
+ if (this.config.type !== "scraper" && !this.config.debug)
241
285
  new Image().src = url;
242
286
  else
243
287
  fetch(
244
288
  url,
245
- this.config.what === "scrape"
289
+ this.config.type === "scraper"
246
290
  ? {
247
291
  method: "POST",
248
292
  headers: { "Content-Type": "application/json" },
249
293
  body: JSON.stringify({
250
- "advertiser-id": this.config.who,
294
+ "advertiser-id": this.config.advertiser,
251
295
  sku: data.sku,
252
- fields,
296
+ fields: { ...data, sku: undefined },
253
297
  }),
254
298
  }
255
299
  : null
@@ -279,7 +323,13 @@ class ChoreographCreatePixel {
279
323
  })
280
324
  .catch(() => {
281
325
  if (response.ok) {
282
- this.log("success", "Successful!", data);
326
+ this.log(
327
+ "success",
328
+ "Successful!",
329
+ ["viewed", "basketed", "purchased"].includes(this.config.type)
330
+ ? { sku: data }
331
+ : data
332
+ );
283
333
 
284
334
  try {
285
335
  this.config.after(data);
@@ -1,22 +1,19 @@
1
- /*! choreograph-create-pixel v1.1.0 2022/09/26 */
1
+ /*! choreograph-create-pixel v1.3.0 2022/10/28 */
2
2
  class ChoreographCreatePixel {
3
3
  constructor(userConfig) {
4
- this.previouslyScrapedJsonString = null;
4
+ this.previouslyScrapedHash = null;
5
5
  this.logs = [];
6
6
 
7
- this.settings = {
8
- colors: { error: "#f44336", warn: "#ffa726", success: "#66bb6a" },
9
- icons: { scrape: "πŸ”", view: "πŸ‘€", basket: "πŸ›’", purchase: "🧾" },
10
- titleCss: "font:700 80% HelveticaNeue;margin:12px 0 8px",
11
- messageCss: "font:500 120% HelveticaNeue;margin-bottom:12px",
12
- eventTypes: {
13
- view: "product-viewed",
14
- basket: "product-basketed",
15
- purchase: "product-purchased",
16
- },
17
- };
18
-
19
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
+ },
20
17
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
21
18
  before: (data, callback) => callback(data),
22
19
  after: () => {},
@@ -86,14 +83,12 @@ class ChoreographCreatePixel {
86
83
  if (!this.config.debug || this.logs.includes(message)) return;
87
84
 
88
85
  const args = [
89
- `%c${
90
- this.settings.icons[this.config.what]
91
- } ${this.config.what.toUpperCase()} PIXEL DEBUG%c\n%c${message}`,
92
- this.settings.titleCss,
93
- "",
94
- this.settings.colors[level]
95
- ? `${this.settings.messageCss};color:${this.settings.colors[level]}`
96
- : this.settings.messageCss,
86
+ `%cβΈ¬ create%c ${this.config.type} ${
87
+ this.config.icons[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]}`,
97
92
  ];
98
93
 
99
94
  if (data) args.push(data);
@@ -102,58 +97,59 @@ class ChoreographCreatePixel {
102
97
  }
103
98
 
104
99
  validateConfig() {
105
- if (typeof this.config.who !== "number")
106
- this.log("error", "Please use a number", { who: this.config.who });
100
+ if (typeof this.config.advertiser !== "number")
101
+ this.log("error", "Please use a number", {
102
+ advertiser: this.config.advertiser,
103
+ });
107
104
  else if (
108
- !["scrape", "view", "basket", "purchase"].includes(this.config.what)
105
+ !["scraper", "viewed", "basketed", "purchased"].includes(this.config.type)
109
106
  )
110
- this.log("error", "Please use scrape, view, basket, or purchase", {
111
- what: this.config.what,
107
+ this.log("error", "Please use scraper, viewed, basketed or purchased", {
108
+ type: this.config.type,
112
109
  });
113
- else if (!(this.config.where instanceof RegExp))
110
+ else if (!(this.config.url instanceof RegExp))
114
111
  this.log("error", "Please use a regular expression", {
115
- where: this.config.where,
112
+ url: this.config.url,
113
+ });
114
+ else if (!this.config.scrape)
115
+ this.log("error", "Please provide something to scrape", {
116
+ scrape: this.config.scrape,
116
117
  });
117
- else if (typeof this.config.which !== "object" || !this.config.which.sku)
118
- this.log("error", "Please provide an SKU", { which: this.config.which });
119
118
  else return true;
120
119
  }
121
120
 
122
121
  cycle() {
123
- if (!this.config.where.test(location.href)) {
124
- this.log("warn", `Pattern does not match "${location.href}"`, {
125
- where: this.config.where,
122
+ if (!this.config.url.test(location.href))
123
+ this.log("warn", `URL pattern does not match "${location.href}"`, {
124
+ url: this.config.url,
126
125
  });
127
- } else if (
128
- this.config.what === "scrape" &&
126
+ else if (
127
+ this.config.type === "scraper" &&
129
128
  document.querySelector('html[class*="translated-"]')
130
- ) {
129
+ )
131
130
  this.log(
132
131
  "warn",
133
- "This page has been translated by the browser, and will be excluded"
132
+ "This page has been translated by the browser, and won't be scraped"
134
133
  );
135
- } else if (this.config.when) {
134
+ else if (this.config.trigger) {
135
+ const attribute = `create-${this.config.type}-${this.config.trigger.event}`;
136
+
136
137
  try {
137
- let elements = this.config.when.elements();
138
+ let elements = this.config.trigger.elements();
138
139
  if (!elements.forEach) elements = [elements];
139
140
 
140
141
  elements.forEach((element) => {
141
- if (
142
- !element.hasAttribute(`choreograph-${this.config.when.listener}`)
143
- ) {
144
- element.addEventListener(this.config.when.listener, () =>
142
+ if (!element.hasAttribute(attribute)) {
143
+ element.addEventListener(this.config.trigger.event, () =>
145
144
  this.scrape(element)
146
145
  );
147
146
 
148
- element.setAttribute(
149
- `choreograph-${this.config.when.listener}`,
150
- ""
151
- );
147
+ element.setAttribute(attribute, "");
152
148
  }
153
149
  });
154
150
  } catch (error) {
155
151
  this.log("error", error.message, {
156
- when: { elements: this.config.when.elements },
152
+ "trigger.elements": this.config.trigger.elements,
157
153
  });
158
154
  }
159
155
  } else {
@@ -164,46 +160,86 @@ class ChoreographCreatePixel {
164
160
  }
165
161
 
166
162
  scrape(element) {
167
- const data = {};
168
163
  let hasErrors = false;
164
+ let data;
169
165
 
170
- Object.keys(this.config.which).forEach((fieldName) => {
171
- data[fieldName] = this.config.which[fieldName];
166
+ const handleField = (field, fieldName) => {
167
+ let result = field;
172
168
 
173
- if (typeof data[fieldName] === "function") {
169
+ if (typeof result === "function") {
174
170
  try {
175
- data[fieldName] = data[fieldName](element);
171
+ result = result(element);
176
172
  } catch (error) {
177
- if (!this.config.optional.includes(fieldName)) {
178
- this.log("error", error.message, {
179
- which: { [fieldName]: this.config.which[fieldName] },
180
- });
173
+ if (this.config.optional.includes(fieldName)) return null;
181
174
 
182
- hasErrors = true;
183
- } else delete data[fieldName];
175
+ this.log(
176
+ this.config.type === "attribution" ? "warn" : "error",
177
+ error.message,
178
+ { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
179
+ );
180
+
181
+ hasErrors = true;
184
182
  }
185
183
  }
186
184
 
187
- if (typeof data[fieldName] === "string")
188
- data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
185
+ if (typeof result === "string")
186
+ result = result.replace(/\s+/g, " ").trim();
189
187
 
190
- if (data[fieldName] == null || data[fieldName] === "")
191
- if (!this.config.optional.includes(fieldName)) {
192
- this.log("error", "This required field's value is empty", {
193
- which: { [fieldName]: data[fieldName] },
194
- });
188
+ if (result == null || result === "") {
189
+ if (this.config.optional.includes(fieldName)) return null;
195
190
 
196
- hasErrors = true;
197
- } else data[fieldName] = null;
198
- });
191
+ this.log(
192
+ this.config.type === "attribution" ? "warn" : "error",
193
+ "Value is empty",
194
+ { [fieldName ? `scrape.${fieldName}` : "scrape"]: result }
195
+ );
196
+
197
+ hasErrors = true;
198
+ }
199
+
200
+ return result;
201
+ };
202
+
203
+ if (this.config.type === "conversion") {
204
+ data = {
205
+ conversion: handleField(this.config.scrape),
206
+ attribution: localStorage.getItem("create-attribution-id"),
207
+ };
208
+
209
+ if (typeof data.attribution !== "string") {
210
+ this.log("warn", "There's no attribution ID stored yet");
211
+ hasErrors = true;
212
+ }
213
+ } else if (this.config.type !== "scraper") {
214
+ data = handleField(this.config.scrape);
215
+ } else {
216
+ data = Object.keys(this.config.scrape).reduce(
217
+ (acc, fieldName) => ({
218
+ ...acc,
219
+ [fieldName]: handleField(this.config.scrape[fieldName], fieldName),
220
+ }),
221
+ {}
222
+ );
223
+ }
199
224
 
200
- const jsonString = JSON.stringify(data);
225
+ const dataHash = JSON.stringify(data);
201
226
 
202
- if (!hasErrors && this.previouslyScrapedJsonString !== jsonString) {
227
+ if (!hasErrors && this.previouslyScrapedHash !== dataHash) {
203
228
  try {
204
229
  this.config.before(data, (newData) => {
205
- if (this.config.what !== "scrape" && Array.isArray(newData.sku))
206
- newData.sku.forEach((sku) => this.send({ ...newData, sku }));
230
+ if (this.config.type === "attribution") {
231
+ localStorage.setItem("create-attribution-id", data);
232
+ this.log("success", "Successful!", { attribution: data });
233
+
234
+ try {
235
+ this.config.after(data);
236
+ } catch (error) {
237
+ this.log("error", error.message, {
238
+ after: this.config.after,
239
+ });
240
+ }
241
+ } else if (Array.isArray(newData))
242
+ newData.forEach((id) => this.send(id));
207
243
  else this.send(newData);
208
244
  });
209
245
  } catch (error) {
@@ -212,42 +248,50 @@ class ChoreographCreatePixel {
212
248
  });
213
249
  }
214
250
 
215
- this.previouslyScrapedJsonString = jsonString;
251
+ this.previouslyScrapedHash = dataHash;
216
252
  this.logs.length = 0;
217
253
  }
218
254
  }
219
255
 
220
256
  send(data) {
221
- const fields = { ...data };
222
- delete fields.sku;
223
-
224
- const url =
225
- this.config.what === "scrape"
226
- ? `https://d.lemonpi.io/scrapes${
227
- this.config.debug ? "?validate=true" : ""
228
- }`
229
- : `https://d.lemonpi.io/a/${
230
- this.config.who
231
- }/product/event?e=${encodeURIComponent(
232
- JSON.stringify({
233
- "event-type": this.settings.eventTypes[this.config.what],
234
- sku: data.sku,
235
- })
236
- )}`;
257
+ let url;
258
+
259
+ switch (this.config.type) {
260
+ case "scraper":
261
+ url = `https://d.lemonpi.io/scrapes${
262
+ this.config.debug ? "?validate=true" : ""
263
+ }`;
264
+ break;
265
+
266
+ case "conversion":
267
+ url = "https://lemonpi.io/"; // TO-DO
268
+ break;
269
+
270
+ default:
271
+ url = `https://d.lemonpi.io/a/${
272
+ this.config.advertiser
273
+ }/product/event?e=${encodeURIComponent(
274
+ JSON.stringify({
275
+ "event-type": `product-${this.config.type}`,
276
+ sku: data,
277
+ })
278
+ )}`;
279
+ break;
280
+ }
237
281
 
238
- if (this.config.what !== "scrape" && !this.config.debug)
282
+ if (this.config.type !== "scraper" && !this.config.debug)
239
283
  new Image().src = url;
240
284
  else
241
285
  fetch(
242
286
  url,
243
- this.config.what === "scrape"
287
+ this.config.type === "scraper"
244
288
  ? {
245
289
  method: "POST",
246
290
  headers: { "Content-Type": "application/json" },
247
291
  body: JSON.stringify({
248
- "advertiser-id": this.config.who,
292
+ "advertiser-id": this.config.advertiser,
249
293
  sku: data.sku,
250
- fields,
294
+ fields: { ...data, sku: undefined },
251
295
  }),
252
296
  }
253
297
  : null
@@ -277,7 +321,13 @@ class ChoreographCreatePixel {
277
321
  })
278
322
  .catch(() => {
279
323
  if (response.ok) {
280
- this.log("success", "Successful!", data);
324
+ this.log(
325
+ "success",
326
+ "Successful!",
327
+ ["viewed", "basketed", "purchased"].includes(this.config.type)
328
+ ? { sku: data }
329
+ : data
330
+ );
281
331
 
282
332
  try {
283
333
  this.config.after(data);
@@ -1,2 +1,2 @@
1
- /*! choreograph-create-pixel v1.1.0 2022/09/26 */
2
- var ChoreographCreatePixel=function(){"use strict";function e(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);t&&(o=o.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,o)}return n}function t(t){for(var n=1;n<arguments.length;n++){var o=null!=arguments[n]?arguments[n]:{};n%2?e(Object(o),!0).forEach((function(e){r(t,e,o[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(o)):e(Object(o)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(o,e))}))}return t}function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}function o(e,t){for(var n=0;n<t.length;n++){var o=t[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(e,o.key,o)}}function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var i=function(){function e(n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.previouslyScrapedJsonString=null,this.logs=[],this.settings={colors:{error:"#f44336",warn:"#ffa726",success:"#66bb6a"},icons:{scrape:"πŸ”",view:"πŸ‘€",basket:"πŸ›’",purchase:"🧾"},titleCss:"font:700 80% HelveticaNeue;margin:12px 0 8px",messageCss:"font:500 120% HelveticaNeue;margin-bottom:12px",eventTypes:{view:"product-viewed",basket:"product-basketed",purchase:"product-purchased"}},this.config=t({debug:/(pixel|lemonpi)_debug/i.test(location.href),before:function(e,t){return t(e)},after:function(){},optional:[]},n),this.validateConfig()&&this.cycle()}var i,s,c;return i=e,s=[{key:"log",value:function(e,t){var n,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(this.config.debug&&!this.logs.includes(t)){var r=["%c".concat(this.settings.icons[this.config.what]," ").concat(this.config.what.toUpperCase()," PIXEL DEBUG%c\n%c").concat(t),this.settings.titleCss,"",this.settings.colors[e]?"".concat(this.settings.messageCss,";color:").concat(this.settings.colors[e]):this.settings.messageCss];o&&r.push(o),(n=console).info.apply(n,r),this.logs.push(t)}}},{key:"validateConfig",value:function(){if("number"!=typeof this.config.who)this.log("error","Please use a number",{who:this.config.who});else if(["scrape","view","basket","purchase"].includes(this.config.what))if(this.config.where instanceof RegExp){if("object"===n(this.config.which)&&this.config.which.sku)return!0;this.log("error","Please provide an SKU",{which:this.config.which})}else this.log("error","Please use a regular expression",{where:this.config.where});else this.log("error","Please use scrape, view, basket, or purchase",{what:this.config.what})}},{key:"cycle",value:function(){var e=this;if(this.config.where.test(location.href))if("scrape"===this.config.what&&document.querySelector('html[class*="translated-"]'))this.log("warn","This page has been translated by the browser, and will be excluded");else if(this.config.when)try{var t=this.config.when.elements();t.forEach||(t=[t]),t.forEach((function(t){t.hasAttribute("choreograph-".concat(e.config.when.listener))||(t.addEventListener(e.config.when.listener,(function(){return e.scrape(t)})),t.setAttribute("choreograph-".concat(e.config.when.listener),""))}))}catch(e){this.log("error",e.message,{when:{elements:this.config.when.elements}})}else this.scrape();else this.log("warn",'Pattern does not match "'.concat(location.href,'"'),{where:this.config.where});setTimeout((function(){return e.cycle()}),750)}},{key:"scrape",value:function(e){var n=this,o={},i=!1;Object.keys(this.config.which).forEach((function(t){if(o[t]=n.config.which[t],"function"==typeof o[t])try{o[t]=o[t](e)}catch(e){n.config.optional.includes(t)?delete o[t]:(n.log("error",e.message,{which:r({},t,n.config.which[t])}),i=!0)}"string"==typeof o[t]&&(o[t]=o[t].replace(/\s+/g," ").trim()),null!=o[t]&&""!==o[t]||(n.config.optional.includes(t)?o[t]=null:(n.log("error","This required field's value is empty",{which:r({},t,o[t])}),i=!0))}));var s=JSON.stringify(o);if(!i&&this.previouslyScrapedJsonString!==s){try{this.config.before(o,(function(e){"scrape"!==n.config.what&&Array.isArray(e.sku)?e.sku.forEach((function(o){return n.send(t(t({},e),{},{sku:o}))})):n.send(e)}))}catch(e){this.log("error",e.message,{before:this.config.before})}this.previouslyScrapedJsonString=s,this.logs.length=0}}},{key:"send",value:function(e){var n=this,o=t({},e);delete o.sku;var r="scrape"===this.config.what?"https://d.lemonpi.io/scrapes".concat(this.config.debug?"?validate=true":""):"https://d.lemonpi.io/a/".concat(this.config.who,"/product/event?e=").concat(encodeURIComponent(JSON.stringify({"event-type":this.settings.eventTypes[this.config.what],sku:e.sku})));"scrape"===this.config.what||this.config.debug?fetch(r,"scrape"===this.config.what?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({"advertiser-id":this.config.who,sku:e.sku,fields:o})}:null).then((function(t){return t.json().then((function(o){if(t.ok){n.log("warn","Successful, with warnings. Details:",o);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:"),o);n.logs.length=0})).catch((function(){if(t.ok){n.log("success","Successful!",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,n=void 0===t?[]:t,o=e.allowHash,r=void 0!==o&&o,i="".concat(location.protocol,"//").concat(location.host).concat(location.pathname),s=new URLSearchParams(location.search);return n.reduce((function(e,t){var n=e?"&":"?",o=encodeURI(t),r=s.get(t);return null!=r?(i+="".concat(n).concat(o).concat(""===r?"":"=".concat(encodeURI(r))),!0):e}),!1),r&&(i+=location.hash),i}},{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,n){return t(t({},e),{},r({},decodeURI(n.split("=")[0]),decodeURI(n.split("=")[1]||"")))}),{})}},{key:"getQueryParameter",value:function(e){return this.getAllQueryParameters()[e]}}],s&&o(i.prototype,s),c&&o(i,c),Object.defineProperty(i,"prototype",{writable:!1}),e}();return i}();
1
+ /*! choreograph-create-pixel v1.3.0 2022/10/28 */
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 o=null!=arguments[r]?arguments[r]:{};r%2?e(Object(o),!0).forEach((function(e){n(t,e,o[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(o)):e(Object(o)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(o,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,n.key,n)}}function n(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}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 o,i,c;return o=e,i=[{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 o=["%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&&o.push(n),(r=console).info.apply(r,o),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=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,o=this,i=!1,c=function(t,r){var c=t;if("function"==typeof c)try{c=c(e)}catch(e){if(o.config.optional.includes(r))return null;o.log("attribution"===o.config.type?"warn":"error",e.message,n({},r?"scrape.".concat(r):"scrape",c)),i=!0}if("string"==typeof c&&(c=c.replace(/\s+/g," ").trim()),null==c||""===c){if(o.config.optional.includes(r))return null;o.log("attribution"===o.config.type?"warn":"error","Value is empty",n({},r?"scrape.".concat(r):"scrape",c)),i=!0}return 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"),i=!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(o.config.scrape[r],r)))}),{});var s=JSON.stringify(r);if(!i&&this.previouslyScrapedHash!==s){try{this.config.before(r,(function(e){if("attribution"===o.config.type){localStorage.setItem("create-attribution-id",r),o.log("success","Successful!",{attribution:r});try{o.config.after(r)}catch(e){o.log("error",e.message,{after:o.config.after})}}else Array.isArray(e)?e.forEach((function(e){return o.send(e)})):o.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,o=void 0!==n&&n,i="".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),o=c.get(t);return null!=o?(i+="".concat(r).concat(n).concat(""===o?"":"=".concat(encodeURI(o))),!0):e}),!1),o&&(i+=location.hash),i}},{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]}}],i&&r(o.prototype,i),c&&r(o,c),Object.defineProperty(o,"prototype",{writable:!1}),e}();return o}();
package/package.json CHANGED
@@ -1,7 +1,14 @@
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.1.0",
4
+ "version": "1.3.0",
5
+ "keywords": [
6
+ "wpp",
7
+ "groupm",
8
+ "lemonpi",
9
+ "odc",
10
+ "opendc"
11
+ ],
5
12
  "author": "Rick Stevens <rick.stevens@choreograph.com> (https://lemonpi.io)",
6
13
  "repository": "gitlab:GreenhouseGroup/lemonpi/solutions/choreograph-create-pixel",
7
14
  "homepage": "https://lemonpi.io",
@@ -18,14 +25,14 @@
18
25
  "format": "prettier --ignore-path .gitignore --check . '!**/*.{js,jsx,vue}'",
19
26
  "build": "rollup -c",
20
27
  "dev": "rollup -cw",
21
- "prepare": "npm run build"
28
+ "prepublishOnly": "npm run build"
22
29
  },
23
30
  "devDependencies": {
24
- "@babel/core": "^7.19.1",
25
- "@babel/preset-env": "^7.19.1",
26
- "@rollup/plugin-babel": "^5.3.1",
27
- "@rollup/plugin-eslint": "^8.0.2",
28
- "eslint": "^8.23.1",
31
+ "@babel/core": "^7.19.6",
32
+ "@babel/preset-env": "^7.19.4",
33
+ "@rollup/plugin-babel": "^6.0.2",
34
+ "@rollup/plugin-eslint": "^9.0.1",
35
+ "eslint": "^8.26.0",
29
36
  "eslint-config-prettier": "^8.5.0",
30
37
  "eslint-plugin-prettier": "^4.2.1",
31
38
  "moment": "^2.29.4",