choreograph-create-pixel 0.1.0 → 0.1.3

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
@@ -6,10 +6,10 @@ This library lets you apply best practises to [Choreograph Create](https://www.l
6
6
 
7
7
  - [x] Supports **scrape**, **view**, **basket**, and **purchase** pixels
8
8
  - [x] Supports dynamic page changes
9
- - [x] Placement-agnostic through continuous URL validation
10
- - [x] Optionally enable pixels through user interaction
9
+ - [x] Continuous URL validation
10
+ - [x] Condition pixels through user interaction
11
11
  - [x] Prevents scraping browser-translated data
12
- - [x] Error and console spam prevention
12
+ - [x] Console debugger
13
13
 
14
14
  ## Quickstart
15
15
 
@@ -22,10 +22,10 @@ The following theoretical snippet could be embedded on every page of _example.co
22
22
  <script>
23
23
  // Scrape pixel
24
24
  new ChoreographCreatePixel({
25
- // Who is the client? (Advertiser ID)
25
+ // Who is the client? (advertiser ID)
26
26
  who: 0,
27
27
 
28
- // What type of pixel is this?
28
+ // What kind of pixel is this?
29
29
  what: "scrape",
30
30
 
31
31
  // Where should the pixel be enabled?
@@ -70,10 +70,10 @@ import Pixel from "choreograph-create-pixel";
70
70
 
71
71
  // Scrape pixel
72
72
  new Pixel({
73
- // Who is the client? (Advertiser ID)
73
+ // Who is the client? (advertiser ID)
74
74
  who: 0,
75
75
 
76
- // What type of pixel is this?
76
+ // What kind of pixel is this?
77
77
  what: "scrape",
78
78
 
79
79
  // Where should the pixel be enabled?
@@ -97,7 +97,7 @@ new Pixel({
97
97
  });
98
98
 
99
99
  // View segment pixel
100
- new ChoreographCreatePixel({
100
+ new Pixel({
101
101
  who: 0,
102
102
  what: "view",
103
103
  where: /example\.com\/products\/\d/,
@@ -160,6 +160,10 @@ Besides **scrape** and **view** pixels, this library also supports **basket** an
160
160
  }
161
161
  ```
162
162
 
163
+ ## Debugging
164
+
165
+ Enable the console debugger by adding `?pixel_debug` or `#pixel_debug` to the page's URL.
166
+
163
167
  ## Configuration
164
168
 
165
169
  | Property | Type | Description | Default |
@@ -176,16 +180,22 @@ Besides **scrape** and **view** pixels, this library also supports **basket** an
176
180
  | `optionalFields` | Array | An array of field names (strings) that are allowed to have empty values. | `[]` |
177
181
  | `beforeSend` | Function | Lifecycle function that's executed just before the pixel's data is sent. | `function (data, callback) { callback(data); }` |
178
182
  | `afterSend` | Function | Lifecycle function that's executed just after the pixel's data is sent. | `function (data) {}` |
179
- | `debug` | Boolean | Enables console debugging. Also enabled by adding `?pixel_debug` or `#pixel_debug` to the page URL. | `false` |
180
183
 
181
184
  ## Static methods (helpers)
182
185
 
183
- ### `getUrl({ allowedParameters, customParameters, allowHash })`
186
+ ### `getUrl({ allowedParameters, allowHash })`
187
+
188
+ Returns the bare page URL without query parameters or location hash. This is recommended to prevent scraping unwanted UTM tagging.
189
+
190
+ | Property | Type | Description | Default |
191
+ | ------------------- | ------- | --------------------------------------------------------------- | ------- |
192
+ | `allowedParameters` | Array | Explicitly allow query parameters in the resulting URL. | `[]` |
193
+ | `allowHash` | Boolean | Wether or not to include the location hash (`#foo`) of the URL. | `false` |
194
+
195
+ ### `getUrlPathSegments()`
196
+
197
+ Retrieves all path segments from the URL as an array. E.g. _http://www.example.com/foo/bar_ returns `["foo", "bar"]`.
184
198
 
185
- Returns the bare page URL without query parameters or location hash. This is recommended to prevent scraping unwanted UTM tagging. Use the configuration to tweak its behavior.
199
+ ### `getUrlQueryParameters()`
186
200
 
187
- | Property | Type | Description | Default |
188
- | ------------------- | ------- | ------------------------------------------------------------------------------------- | ------- |
189
- | `allowedParameters` | Array | Define query parameter keys (as string values) that are allowed in the resulting URL. | `[]` |
190
- | `customParameters` | Object | Manually add custom query parameters to the resulting URL. E.g.: `{ foo: "bar"}` | `{}` |
191
- | `allowHash` | Boolean | Wether or not to include the location hash (`#foo`) of the URL. | `false` |
201
+ Retrieves all query string parameters from the URL as an object. E.g. _http://www.example.com/?foo=bar_ returns `{ foo: "bar" }`.
@@ -1,266 +1,38 @@
1
- /*! choreograph-create-pixel v0.1.0 2022/09/15 */
1
+ /*! choreograph-create-pixel v0.1.3 2022/09/16 */
2
2
  'use strict';
3
3
 
4
- const log = (
5
- config,
6
- message,
7
- { level = "info", key = null, data = undefined }
8
- ) => {
9
- if (!config.debug || config.logs[key] === message) return;
10
-
11
- const colors = {
12
- error: "#f44336",
13
- warn: "#ffa726",
14
- success: "#66bb6a",
15
- info: "#29b6f6",
16
- };
17
-
18
- const icons = { error: "⛔️", warn: "⚠️", success: "✅", info: "ℹ️" };
19
- const titleCss =
20
- "font-family:HelveticaNeue;font-weight:700;margin:1em 0 1.2em 0;font-size:80%;";
21
- const messageCss = `font-family:HelveticaNeue;font-size:120%;margin-bottom:.5em;color:${colors[level]}`;
22
- const keyCss =
23
- "font-family:monospace;font-size:120%;border:1px solid;border-radius:5px;margin-bottom:1em;padding:.2em .6em";
24
-
25
- const args = [
26
- `%cCHOREOGRAPH PIXEL DEBUGGER%c\n%c${icons[level]} ${message}${
27
- key ? `%c\n→ %c${key}` : "%c%c"
28
- }`,
29
- titleCss,
30
- "",
31
- messageCss,
32
- "",
33
- keyCss,
34
- ];
35
-
36
- if (data) args.push(data);
37
- console.info(...args);
38
- if (key) config.logs[key] = message;
39
- };
40
-
41
- const send = (config, data) => {
42
- const eventTypes = {
43
- view: "product-viewed",
44
- basket: "product-basketed",
45
- purchase: "product-purchased",
46
- };
47
-
48
- const fields = { ...data };
49
- delete fields.sku;
50
-
51
- const url =
52
- config.what === "scrape"
53
- ? `https://d.lemonpi.io/scrapes${config.debug ? "?validate=true" : ""}`
54
- : `https://d.lemonpi.io/a/${
55
- config.who
56
- }/product/event?e=${encodeURIComponent(
57
- JSON.stringify({
58
- "event-type": eventTypes[config.what],
59
- sku: data.sku,
60
- })
61
- )}`;
62
-
63
- if (config.what !== "scrape" && !config.debug) new Image().src = url;
64
- else
65
- fetch(
66
- url,
67
- config.what === "scrape"
68
- ? {
69
- method: "POST",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({
72
- "advertiser-id": config.who,
73
- sku: data.sku,
74
- fields,
75
- }),
76
- }
77
- : null
78
- )
79
- .then((response) => {
80
- if (!config.debug) return;
81
-
82
- response
83
- .json()
84
- .then((result) => {
85
- if (response.ok) {
86
- log(
87
- config,
88
- `"${config.what}" pixel successful, with warnings. Details:`,
89
- { level: "warn", data: result }
90
- );
91
-
92
- try {
93
- config.afterSend(data);
94
- } catch (error) {
95
- log(config, error.message, {
96
- level: "error",
97
- key: "afterSend",
98
- });
99
- }
100
- } else
101
- log(
102
- config,
103
- `"${config.what}" pixel failed: ${response.status} (${response.statusText}). Details:`,
104
- { level: "error", data: result }
105
- );
106
- })
107
- .catch(() => {
108
- if (response.ok) {
109
- log(config, `"${config.what}" pixel successful!`, {
110
- level: "success",
111
- data,
112
- });
113
-
114
- try {
115
- config.afterSend(data);
116
- } catch (error) {
117
- log(config, error.message, {
118
- level: "error",
119
- key: "afterSend",
120
- });
121
- }
122
- } else
123
- log(
124
- config,
125
- `"${config.what}" pixel failed: ${response.status} (${response.statusText})`,
126
- { level: "error" }
127
- );
128
- });
129
- })
130
- .catch((error) => {
131
- if (config.debug)
132
- log(config, `"${config.what}" pixel failed: ${error.message}`, {
133
- level: "error",
134
- });
135
- });
136
- };
137
-
138
- const scrape = (config, element) => {
139
- const data = {};
140
- let hasErrors = false;
141
-
142
- Object.keys(config.which).forEach((fieldName) => {
143
- data[fieldName] = config.which[fieldName];
144
-
145
- if (typeof data[fieldName] === "function") {
146
- try {
147
- data[fieldName] = data[fieldName](element);
148
- } catch (error) {
149
- log(config, error.message, {
150
- level: "error",
151
- key: `which.${fieldName}`,
152
- });
153
-
154
- hasErrors = true;
155
- }
156
- }
157
-
158
- if (typeof data[fieldName] === "string")
159
- data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
160
-
161
- if (data[fieldName] == null || data[fieldName] === "")
162
- if (!config.optionalFields.includes(fieldName)) {
163
- log(config, "This required field's value is empty", {
164
- level: "error",
165
- key: `which.${fieldName}`,
166
- });
167
-
168
- hasErrors = true;
169
- } else data[fieldName] = null;
170
- });
171
-
172
- const jsonString = JSON.stringify(data);
173
-
174
- if (!hasErrors && config.lastJsonString !== jsonString) {
175
- log(config, `Scraped data for "${config.what}" pixel:`, { data });
176
-
177
- try {
178
- config.beforeSend(data, (newData) => {
179
- if (config.what !== "scrape" && Array.isArray(newData.sku))
180
- newData.sku.forEach((sku) => send(config, { ...newData, sku }));
181
- else send(config, newData);
182
- });
183
- } catch (error) {
184
- log(config, error.message, { level: "error", key: "beforeSend" });
185
- }
186
-
187
- config.lastJsonString = jsonString;
188
- config.logs = {};
189
- }
190
- };
191
-
192
- const cycle = (config) => {
193
- if (!config.where.test(location.href)) {
194
- log(
195
- config,
196
- `RegExp ${config.where.toString()} does not match ${location.href}`,
197
- { level: "warn", key: "where" }
198
- );
199
- } else if (document.querySelector('html[class*="translated-"]')) {
200
- log(
201
- config,
202
- "This page has been translated by the browser, and will be excluded",
203
- { level: "warn", key: "(browser translation)" }
204
- );
205
- } else if (config.when) {
206
- try {
207
- let elements = config.when.elements();
208
- if (!elements.forEach) elements = [elements];
209
-
210
- elements.forEach((element) => {
211
- if (!element.hasAttribute(`choreograph-${config.when.listener}`)) {
212
- element.addEventListener(config.when.listener, () =>
213
- scrape(config, element)
214
- );
215
-
216
- element.setAttribute(`choreograph-${config.when.listener}`, "");
217
- }
218
- });
219
- } catch (error) {
220
- log(config, error.message, { level: "error", key: "when.elements" });
221
- }
222
- } else {
223
- scrape(config);
224
- }
225
-
226
- setTimeout(() => cycle(config), 750);
227
- };
228
-
229
- const validateConfig = (config) => {
230
- if (typeof config.who !== "number")
231
- log(config, "Please use a number", { level: "error", key: "who" });
232
- else if (!["scrape", "view", "basket", "purchase"].includes(config.what))
233
- log(config, "Please use scrape, view, basket, or purchase", {
234
- level: "error",
235
- key: "what",
236
- });
237
- else if (!(config.where instanceof RegExp))
238
- log(config, "Please use a regular expression", {
239
- level: "error",
240
- key: "where",
241
- });
242
- else if (typeof config.which !== "object" || !config.which.sku)
243
- log(config, "Please provide an SKU", { level: "error", key: "which.sku" });
244
- else return true;
245
- };
246
-
247
4
  class ChoreographCreatePixel {
248
5
  constructor(userConfig) {
249
- const config = {
6
+ this.previouslyScrapedJsonString = null;
7
+ this.logs = [];
8
+
9
+ this.settings = {
10
+ colors: { error: "#f44336", warn: "#ffa726", success: "#66bb6a" },
11
+ icons: { scrape: "🔍", view: "👀", basket: "🛒", purchase: "🧾" },
12
+ levels: { error: "⛔️", warn: "⚠️", success: "✅", info: "ℹ️" },
13
+ titleCss: "font:700 80% HelveticaNeue;margin:12px 0 6px",
14
+ messageCss: "font:120% HelveticaNeue;margin-bottom:12px",
15
+ eventTypes: {
16
+ view: "product-viewed",
17
+ basket: "product-basketed",
18
+ purchase: "product-purchased",
19
+ },
20
+ };
21
+
22
+ this.config = {
250
23
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
251
24
  beforeSend: (data, callback) => callback(data),
252
25
  afterSend: () => {},
253
26
  optionalFields: [],
254
27
  ...userConfig,
255
- logs: {},
256
28
  };
257
29
 
258
- if (validateConfig(config)) cycle(config);
30
+ if (this.validateConfig()) this.cycle();
259
31
  }
260
32
 
261
33
  static getUrl(config) {
262
34
  let url = `${location.protocol}//${location.host}${location.pathname}`;
263
- const parameters = new URLSearchParams(window.location.search);
35
+ const parameters = new URLSearchParams(location.search);
264
36
 
265
37
  if (config) {
266
38
  let paramAdded = false;
@@ -278,24 +50,271 @@ class ChoreographCreatePixel {
278
50
  });
279
51
  }
280
52
 
281
- if (config.customParameters) {
282
- const parameters = Object.keys(config.customParameters);
53
+ if (config.allowHash) {
54
+ url += location.hash;
55
+ }
56
+ }
57
+
58
+ return url;
59
+ }
283
60
 
284
- parameters.forEach((parameter) => {
285
- const separator = paramAdded ? "&" : "?";
286
- const key = encodeURI(parameter);
287
- const value = encodeURI(config.customParameters[parameter]);
288
- url += `${separator}${key}=${value}`;
289
- paramAdded = true;
61
+ static getUrlPathSegments() {
62
+ return location.pathname
63
+ .split("/")
64
+ .filter((segment) => segment)
65
+ .map((segment) => decodeURI(segment));
66
+ }
67
+
68
+ static getUrlQueryParameters() {
69
+ return location.search
70
+ .replace(/^\?/, "")
71
+ .split("&")
72
+ .filter((parameter) => parameter)
73
+ .reduce(
74
+ (parameters, parameter) => ({
75
+ ...parameters,
76
+ [decodeURI(parameter.split("=")[0])]: decodeURI(
77
+ parameter.split("=")[1] || ""
78
+ ),
79
+ }),
80
+ {}
81
+ );
82
+ }
83
+
84
+ log(message, { level = "info", data = null }) {
85
+ if (!this.config.debug || this.logs.includes(message)) return;
86
+
87
+ const args = [
88
+ `%cCHOREOGRAPH ${this.config.what.toUpperCase()} PIXEL%c ${
89
+ this.settings.icons[this.config.what]
90
+ }\n${this.settings.levels[level]} %c${message}`,
91
+ this.settings.titleCss,
92
+ "",
93
+ this.settings.colors[level]
94
+ ? `${this.settings.messageCss};color:${this.settings.colors[level]}`
95
+ : this.settings.messageCss,
96
+ ];
97
+
98
+ if (data) args.push(data);
99
+ console.info(...args);
100
+ this.logs.push(message);
101
+ }
102
+
103
+ validateConfig() {
104
+ if (typeof this.config.who !== "number")
105
+ this.log("Please use a number", {
106
+ level: "error",
107
+ data: { who: this.config.who },
108
+ });
109
+ else if (
110
+ !["scrape", "view", "basket", "purchase"].includes(this.config.what)
111
+ )
112
+ this.log("Please use scrape, view, basket, or purchase", {
113
+ level: "error",
114
+ data: { what: this.config.what },
115
+ });
116
+ else if (!(this.config.where instanceof RegExp))
117
+ this.log("Please use a regular expression", {
118
+ level: "error",
119
+ data: { where: this.config.where },
120
+ });
121
+ else if (typeof this.config.which !== "object" || !this.config.which.sku)
122
+ this.log("Please provide an SKU", {
123
+ level: "error",
124
+ data: { which: this.config.which },
125
+ });
126
+ else return true;
127
+ }
128
+
129
+ cycle() {
130
+ if (!this.config.where.test(location.href)) {
131
+ this.log(`Pattern does not match ${location.href}`, {
132
+ level: "warn",
133
+ data: { where: this.config.where },
134
+ });
135
+ } else if (
136
+ this.config.what === "scrape" &&
137
+ document.querySelector('html[class*="translated-"]')
138
+ ) {
139
+ this.log(
140
+ "This page has been translated by the browser, and will be excluded",
141
+ { level: "warn" }
142
+ );
143
+ } else if (this.config.when) {
144
+ try {
145
+ let elements = this.config.when.elements();
146
+ if (!elements.forEach) elements = [elements];
147
+
148
+ elements.forEach((element) => {
149
+ if (
150
+ !element.hasAttribute(`choreograph-${this.config.when.listener}`)
151
+ ) {
152
+ element.addEventListener(this.config.when.listener, () =>
153
+ this.scrape(element)
154
+ );
155
+
156
+ element.setAttribute(
157
+ `choreograph-${this.config.when.listener}`,
158
+ ""
159
+ );
160
+ }
161
+ });
162
+ } catch (error) {
163
+ this.log(error.message, {
164
+ level: "error",
165
+ data: { when: { elements: this.config.when.elements } },
290
166
  });
291
167
  }
168
+ } else {
169
+ this.scrape();
170
+ }
292
171
 
293
- if (config.allowHash) {
294
- url += window.location.hash;
172
+ setTimeout(() => this.cycle(), 750);
173
+ }
174
+
175
+ scrape(element) {
176
+ const data = {};
177
+ let hasErrors = false;
178
+
179
+ Object.keys(this.config.which).forEach((fieldName) => {
180
+ data[fieldName] = this.config.which[fieldName];
181
+
182
+ if (typeof data[fieldName] === "function") {
183
+ try {
184
+ data[fieldName] = data[fieldName](element);
185
+ } catch (error) {
186
+ this.log(error.message, {
187
+ level: "error",
188
+ data: { which: { [fieldName]: this.config.which[fieldName] } },
189
+ });
190
+
191
+ hasErrors = true;
192
+ }
295
193
  }
194
+
195
+ if (typeof data[fieldName] === "string")
196
+ data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
197
+
198
+ if (data[fieldName] == null || data[fieldName] === "")
199
+ if (!this.config.optionalFields.includes(fieldName)) {
200
+ this.log("This required field's value is empty", {
201
+ level: "error",
202
+ data: { which: { [fieldName]: data[fieldName] } },
203
+ });
204
+
205
+ hasErrors = true;
206
+ } else data[fieldName] = null;
207
+ });
208
+
209
+ const jsonString = JSON.stringify(data);
210
+
211
+ if (!hasErrors && this.previouslyScrapedJsonString !== jsonString) {
212
+ this.log("Scraped data:", { data });
213
+
214
+ try {
215
+ this.config.beforeSend(data, (newData) => {
216
+ if (this.config.what !== "scrape" && Array.isArray(newData.sku))
217
+ newData.sku.forEach((sku) => this.send({ ...newData, sku }));
218
+ else this.send(newData);
219
+ });
220
+ } catch (error) {
221
+ this.log(error.message, {
222
+ level: "error",
223
+ data: { beforeSend: this.config.beforeSend },
224
+ });
225
+ }
226
+
227
+ this.previouslyScrapedJsonString = jsonString;
228
+ this.logs.length = 0;
296
229
  }
230
+ }
297
231
 
298
- return url;
232
+ send(data) {
233
+ const fields = { ...data };
234
+ delete fields.sku;
235
+
236
+ const url =
237
+ this.config.what === "scrape"
238
+ ? `https://d.lemonpi.io/scrapes${
239
+ this.config.debug ? "?validate=true" : ""
240
+ }`
241
+ : `https://d.lemonpi.io/a/${
242
+ this.config.who
243
+ }/product/event?e=${encodeURIComponent(
244
+ JSON.stringify({
245
+ "event-type": this.settings.eventTypes[this.config.what],
246
+ sku: data.sku,
247
+ })
248
+ )}`;
249
+
250
+ if (this.config.what !== "scrape" && !this.config.debug)
251
+ new Image().src = url;
252
+ else
253
+ fetch(
254
+ url,
255
+ this.config.what === "scrape"
256
+ ? {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/json" },
259
+ body: JSON.stringify({
260
+ "advertiser-id": this.config.who,
261
+ sku: data.sku,
262
+ fields,
263
+ }),
264
+ }
265
+ : null
266
+ )
267
+ .then((response) => {
268
+ if (!this.config.debug) return;
269
+
270
+ response
271
+ .json()
272
+ .then((result) => {
273
+ if (response.ok) {
274
+ this.log("Successful, with warnings. Details:", {
275
+ level: "warn",
276
+ data: result,
277
+ });
278
+
279
+ try {
280
+ this.config.afterSend(data);
281
+ } catch (error) {
282
+ this.log(error.message, {
283
+ level: "error",
284
+ data: { afterSend: this.config.afterSend },
285
+ });
286
+ }
287
+ } else
288
+ this.log(
289
+ `Failed: ${response.status} (${response.statusText}). Details:`,
290
+ { level: "error", data: result }
291
+ );
292
+ })
293
+ .catch(() => {
294
+ if (response.ok) {
295
+ this.log("Successful!", { level: "success", data });
296
+
297
+ try {
298
+ this.config.afterSend(data);
299
+ } catch (error) {
300
+ this.log(error.message, {
301
+ level: "error",
302
+ data: { afterSend: this.config.afterSend },
303
+ });
304
+ }
305
+ } else
306
+ this.log(
307
+ `Failed: ${response.status} (${response.statusText}). Details:`,
308
+ { level: "error", data: response }
309
+ );
310
+ });
311
+ })
312
+ .catch((error) => {
313
+ if (this.config.debug)
314
+ this.log(`Failed: ${error.message}`, {
315
+ level: "error",
316
+ });
317
+ });
299
318
  }
300
319
  }
301
320
 
@@ -1,264 +1,36 @@
1
- /*! choreograph-create-pixel v0.1.0 2022/09/15 */
2
- const log = (
3
- config,
4
- message,
5
- { level = "info", key = null, data = undefined }
6
- ) => {
7
- if (!config.debug || config.logs[key] === message) return;
8
-
9
- const colors = {
10
- error: "#f44336",
11
- warn: "#ffa726",
12
- success: "#66bb6a",
13
- info: "#29b6f6",
14
- };
15
-
16
- const icons = { error: "⛔️", warn: "⚠️", success: "✅", info: "ℹ️" };
17
- const titleCss =
18
- "font-family:HelveticaNeue;font-weight:700;margin:1em 0 1.2em 0;font-size:80%;";
19
- const messageCss = `font-family:HelveticaNeue;font-size:120%;margin-bottom:.5em;color:${colors[level]}`;
20
- const keyCss =
21
- "font-family:monospace;font-size:120%;border:1px solid;border-radius:5px;margin-bottom:1em;padding:.2em .6em";
22
-
23
- const args = [
24
- `%cCHOREOGRAPH PIXEL DEBUGGER%c\n%c${icons[level]} ${message}${
25
- key ? `%c\n→ %c${key}` : "%c%c"
26
- }`,
27
- titleCss,
28
- "",
29
- messageCss,
30
- "",
31
- keyCss,
32
- ];
33
-
34
- if (data) args.push(data);
35
- console.info(...args);
36
- if (key) config.logs[key] = message;
37
- };
38
-
39
- const send = (config, data) => {
40
- const eventTypes = {
41
- view: "product-viewed",
42
- basket: "product-basketed",
43
- purchase: "product-purchased",
44
- };
45
-
46
- const fields = { ...data };
47
- delete fields.sku;
48
-
49
- const url =
50
- config.what === "scrape"
51
- ? `https://d.lemonpi.io/scrapes${config.debug ? "?validate=true" : ""}`
52
- : `https://d.lemonpi.io/a/${
53
- config.who
54
- }/product/event?e=${encodeURIComponent(
55
- JSON.stringify({
56
- "event-type": eventTypes[config.what],
57
- sku: data.sku,
58
- })
59
- )}`;
60
-
61
- if (config.what !== "scrape" && !config.debug) new Image().src = url;
62
- else
63
- fetch(
64
- url,
65
- config.what === "scrape"
66
- ? {
67
- method: "POST",
68
- headers: { "Content-Type": "application/json" },
69
- body: JSON.stringify({
70
- "advertiser-id": config.who,
71
- sku: data.sku,
72
- fields,
73
- }),
74
- }
75
- : null
76
- )
77
- .then((response) => {
78
- if (!config.debug) return;
79
-
80
- response
81
- .json()
82
- .then((result) => {
83
- if (response.ok) {
84
- log(
85
- config,
86
- `"${config.what}" pixel successful, with warnings. Details:`,
87
- { level: "warn", data: result }
88
- );
89
-
90
- try {
91
- config.afterSend(data);
92
- } catch (error) {
93
- log(config, error.message, {
94
- level: "error",
95
- key: "afterSend",
96
- });
97
- }
98
- } else
99
- log(
100
- config,
101
- `"${config.what}" pixel failed: ${response.status} (${response.statusText}). Details:`,
102
- { level: "error", data: result }
103
- );
104
- })
105
- .catch(() => {
106
- if (response.ok) {
107
- log(config, `"${config.what}" pixel successful!`, {
108
- level: "success",
109
- data,
110
- });
111
-
112
- try {
113
- config.afterSend(data);
114
- } catch (error) {
115
- log(config, error.message, {
116
- level: "error",
117
- key: "afterSend",
118
- });
119
- }
120
- } else
121
- log(
122
- config,
123
- `"${config.what}" pixel failed: ${response.status} (${response.statusText})`,
124
- { level: "error" }
125
- );
126
- });
127
- })
128
- .catch((error) => {
129
- if (config.debug)
130
- log(config, `"${config.what}" pixel failed: ${error.message}`, {
131
- level: "error",
132
- });
133
- });
134
- };
135
-
136
- const scrape = (config, element) => {
137
- const data = {};
138
- let hasErrors = false;
139
-
140
- Object.keys(config.which).forEach((fieldName) => {
141
- data[fieldName] = config.which[fieldName];
142
-
143
- if (typeof data[fieldName] === "function") {
144
- try {
145
- data[fieldName] = data[fieldName](element);
146
- } catch (error) {
147
- log(config, error.message, {
148
- level: "error",
149
- key: `which.${fieldName}`,
150
- });
151
-
152
- hasErrors = true;
153
- }
154
- }
155
-
156
- if (typeof data[fieldName] === "string")
157
- data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
158
-
159
- if (data[fieldName] == null || data[fieldName] === "")
160
- if (!config.optionalFields.includes(fieldName)) {
161
- log(config, "This required field's value is empty", {
162
- level: "error",
163
- key: `which.${fieldName}`,
164
- });
165
-
166
- hasErrors = true;
167
- } else data[fieldName] = null;
168
- });
169
-
170
- const jsonString = JSON.stringify(data);
171
-
172
- if (!hasErrors && config.lastJsonString !== jsonString) {
173
- log(config, `Scraped data for "${config.what}" pixel:`, { data });
174
-
175
- try {
176
- config.beforeSend(data, (newData) => {
177
- if (config.what !== "scrape" && Array.isArray(newData.sku))
178
- newData.sku.forEach((sku) => send(config, { ...newData, sku }));
179
- else send(config, newData);
180
- });
181
- } catch (error) {
182
- log(config, error.message, { level: "error", key: "beforeSend" });
183
- }
184
-
185
- config.lastJsonString = jsonString;
186
- config.logs = {};
187
- }
188
- };
189
-
190
- const cycle = (config) => {
191
- if (!config.where.test(location.href)) {
192
- log(
193
- config,
194
- `RegExp ${config.where.toString()} does not match ${location.href}`,
195
- { level: "warn", key: "where" }
196
- );
197
- } else if (document.querySelector('html[class*="translated-"]')) {
198
- log(
199
- config,
200
- "This page has been translated by the browser, and will be excluded",
201
- { level: "warn", key: "(browser translation)" }
202
- );
203
- } else if (config.when) {
204
- try {
205
- let elements = config.when.elements();
206
- if (!elements.forEach) elements = [elements];
207
-
208
- elements.forEach((element) => {
209
- if (!element.hasAttribute(`choreograph-${config.when.listener}`)) {
210
- element.addEventListener(config.when.listener, () =>
211
- scrape(config, element)
212
- );
213
-
214
- element.setAttribute(`choreograph-${config.when.listener}`, "");
215
- }
216
- });
217
- } catch (error) {
218
- log(config, error.message, { level: "error", key: "when.elements" });
219
- }
220
- } else {
221
- scrape(config);
222
- }
223
-
224
- setTimeout(() => cycle(config), 750);
225
- };
226
-
227
- const validateConfig = (config) => {
228
- if (typeof config.who !== "number")
229
- log(config, "Please use a number", { level: "error", key: "who" });
230
- else if (!["scrape", "view", "basket", "purchase"].includes(config.what))
231
- log(config, "Please use scrape, view, basket, or purchase", {
232
- level: "error",
233
- key: "what",
234
- });
235
- else if (!(config.where instanceof RegExp))
236
- log(config, "Please use a regular expression", {
237
- level: "error",
238
- key: "where",
239
- });
240
- else if (typeof config.which !== "object" || !config.which.sku)
241
- log(config, "Please provide an SKU", { level: "error", key: "which.sku" });
242
- else return true;
243
- };
244
-
1
+ /*! choreograph-create-pixel v0.1.3 2022/09/16 */
245
2
  class ChoreographCreatePixel {
246
3
  constructor(userConfig) {
247
- const config = {
4
+ this.previouslyScrapedJsonString = null;
5
+ this.logs = [];
6
+
7
+ this.settings = {
8
+ colors: { error: "#f44336", warn: "#ffa726", success: "#66bb6a" },
9
+ icons: { scrape: "🔍", view: "👀", basket: "🛒", purchase: "🧾" },
10
+ levels: { error: "⛔️", warn: "⚠️", success: "✅", info: "ℹ️" },
11
+ titleCss: "font:700 80% HelveticaNeue;margin:12px 0 6px",
12
+ messageCss: "font:120% HelveticaNeue;margin-bottom:12px",
13
+ eventTypes: {
14
+ view: "product-viewed",
15
+ basket: "product-basketed",
16
+ purchase: "product-purchased",
17
+ },
18
+ };
19
+
20
+ this.config = {
248
21
  debug: /(pixel|lemonpi)_debug/i.test(location.href),
249
22
  beforeSend: (data, callback) => callback(data),
250
23
  afterSend: () => {},
251
24
  optionalFields: [],
252
25
  ...userConfig,
253
- logs: {},
254
26
  };
255
27
 
256
- if (validateConfig(config)) cycle(config);
28
+ if (this.validateConfig()) this.cycle();
257
29
  }
258
30
 
259
31
  static getUrl(config) {
260
32
  let url = `${location.protocol}//${location.host}${location.pathname}`;
261
- const parameters = new URLSearchParams(window.location.search);
33
+ const parameters = new URLSearchParams(location.search);
262
34
 
263
35
  if (config) {
264
36
  let paramAdded = false;
@@ -276,24 +48,271 @@ class ChoreographCreatePixel {
276
48
  });
277
49
  }
278
50
 
279
- if (config.customParameters) {
280
- const parameters = Object.keys(config.customParameters);
51
+ if (config.allowHash) {
52
+ url += location.hash;
53
+ }
54
+ }
55
+
56
+ return url;
57
+ }
281
58
 
282
- parameters.forEach((parameter) => {
283
- const separator = paramAdded ? "&" : "?";
284
- const key = encodeURI(parameter);
285
- const value = encodeURI(config.customParameters[parameter]);
286
- url += `${separator}${key}=${value}`;
287
- paramAdded = true;
59
+ static getUrlPathSegments() {
60
+ return location.pathname
61
+ .split("/")
62
+ .filter((segment) => segment)
63
+ .map((segment) => decodeURI(segment));
64
+ }
65
+
66
+ static getUrlQueryParameters() {
67
+ return location.search
68
+ .replace(/^\?/, "")
69
+ .split("&")
70
+ .filter((parameter) => parameter)
71
+ .reduce(
72
+ (parameters, parameter) => ({
73
+ ...parameters,
74
+ [decodeURI(parameter.split("=")[0])]: decodeURI(
75
+ parameter.split("=")[1] || ""
76
+ ),
77
+ }),
78
+ {}
79
+ );
80
+ }
81
+
82
+ log(message, { level = "info", data = null }) {
83
+ if (!this.config.debug || this.logs.includes(message)) return;
84
+
85
+ const args = [
86
+ `%cCHOREOGRAPH ${this.config.what.toUpperCase()} PIXEL%c ${
87
+ this.settings.icons[this.config.what]
88
+ }\n${this.settings.levels[level]} %c${message}`,
89
+ this.settings.titleCss,
90
+ "",
91
+ this.settings.colors[level]
92
+ ? `${this.settings.messageCss};color:${this.settings.colors[level]}`
93
+ : this.settings.messageCss,
94
+ ];
95
+
96
+ if (data) args.push(data);
97
+ console.info(...args);
98
+ this.logs.push(message);
99
+ }
100
+
101
+ validateConfig() {
102
+ if (typeof this.config.who !== "number")
103
+ this.log("Please use a number", {
104
+ level: "error",
105
+ data: { who: this.config.who },
106
+ });
107
+ else if (
108
+ !["scrape", "view", "basket", "purchase"].includes(this.config.what)
109
+ )
110
+ this.log("Please use scrape, view, basket, or purchase", {
111
+ level: "error",
112
+ data: { what: this.config.what },
113
+ });
114
+ else if (!(this.config.where instanceof RegExp))
115
+ this.log("Please use a regular expression", {
116
+ level: "error",
117
+ data: { where: this.config.where },
118
+ });
119
+ else if (typeof this.config.which !== "object" || !this.config.which.sku)
120
+ this.log("Please provide an SKU", {
121
+ level: "error",
122
+ data: { which: this.config.which },
123
+ });
124
+ else return true;
125
+ }
126
+
127
+ cycle() {
128
+ if (!this.config.where.test(location.href)) {
129
+ this.log(`Pattern does not match ${location.href}`, {
130
+ level: "warn",
131
+ data: { where: this.config.where },
132
+ });
133
+ } else if (
134
+ this.config.what === "scrape" &&
135
+ document.querySelector('html[class*="translated-"]')
136
+ ) {
137
+ this.log(
138
+ "This page has been translated by the browser, and will be excluded",
139
+ { level: "warn" }
140
+ );
141
+ } else if (this.config.when) {
142
+ try {
143
+ let elements = this.config.when.elements();
144
+ if (!elements.forEach) elements = [elements];
145
+
146
+ elements.forEach((element) => {
147
+ if (
148
+ !element.hasAttribute(`choreograph-${this.config.when.listener}`)
149
+ ) {
150
+ element.addEventListener(this.config.when.listener, () =>
151
+ this.scrape(element)
152
+ );
153
+
154
+ element.setAttribute(
155
+ `choreograph-${this.config.when.listener}`,
156
+ ""
157
+ );
158
+ }
159
+ });
160
+ } catch (error) {
161
+ this.log(error.message, {
162
+ level: "error",
163
+ data: { when: { elements: this.config.when.elements } },
288
164
  });
289
165
  }
166
+ } else {
167
+ this.scrape();
168
+ }
290
169
 
291
- if (config.allowHash) {
292
- url += window.location.hash;
170
+ setTimeout(() => this.cycle(), 750);
171
+ }
172
+
173
+ scrape(element) {
174
+ const data = {};
175
+ let hasErrors = false;
176
+
177
+ Object.keys(this.config.which).forEach((fieldName) => {
178
+ data[fieldName] = this.config.which[fieldName];
179
+
180
+ if (typeof data[fieldName] === "function") {
181
+ try {
182
+ data[fieldName] = data[fieldName](element);
183
+ } catch (error) {
184
+ this.log(error.message, {
185
+ level: "error",
186
+ data: { which: { [fieldName]: this.config.which[fieldName] } },
187
+ });
188
+
189
+ hasErrors = true;
190
+ }
293
191
  }
192
+
193
+ if (typeof data[fieldName] === "string")
194
+ data[fieldName] = data[fieldName].replace(/\s+/g, " ").trim();
195
+
196
+ if (data[fieldName] == null || data[fieldName] === "")
197
+ if (!this.config.optionalFields.includes(fieldName)) {
198
+ this.log("This required field's value is empty", {
199
+ level: "error",
200
+ data: { which: { [fieldName]: data[fieldName] } },
201
+ });
202
+
203
+ hasErrors = true;
204
+ } else data[fieldName] = null;
205
+ });
206
+
207
+ const jsonString = JSON.stringify(data);
208
+
209
+ if (!hasErrors && this.previouslyScrapedJsonString !== jsonString) {
210
+ this.log("Scraped data:", { data });
211
+
212
+ try {
213
+ this.config.beforeSend(data, (newData) => {
214
+ if (this.config.what !== "scrape" && Array.isArray(newData.sku))
215
+ newData.sku.forEach((sku) => this.send({ ...newData, sku }));
216
+ else this.send(newData);
217
+ });
218
+ } catch (error) {
219
+ this.log(error.message, {
220
+ level: "error",
221
+ data: { beforeSend: this.config.beforeSend },
222
+ });
223
+ }
224
+
225
+ this.previouslyScrapedJsonString = jsonString;
226
+ this.logs.length = 0;
294
227
  }
228
+ }
295
229
 
296
- return url;
230
+ send(data) {
231
+ const fields = { ...data };
232
+ delete fields.sku;
233
+
234
+ const url =
235
+ this.config.what === "scrape"
236
+ ? `https://d.lemonpi.io/scrapes${
237
+ this.config.debug ? "?validate=true" : ""
238
+ }`
239
+ : `https://d.lemonpi.io/a/${
240
+ this.config.who
241
+ }/product/event?e=${encodeURIComponent(
242
+ JSON.stringify({
243
+ "event-type": this.settings.eventTypes[this.config.what],
244
+ sku: data.sku,
245
+ })
246
+ )}`;
247
+
248
+ if (this.config.what !== "scrape" && !this.config.debug)
249
+ new Image().src = url;
250
+ else
251
+ fetch(
252
+ url,
253
+ this.config.what === "scrape"
254
+ ? {
255
+ method: "POST",
256
+ headers: { "Content-Type": "application/json" },
257
+ body: JSON.stringify({
258
+ "advertiser-id": this.config.who,
259
+ sku: data.sku,
260
+ fields,
261
+ }),
262
+ }
263
+ : null
264
+ )
265
+ .then((response) => {
266
+ if (!this.config.debug) return;
267
+
268
+ response
269
+ .json()
270
+ .then((result) => {
271
+ if (response.ok) {
272
+ this.log("Successful, with warnings. Details:", {
273
+ level: "warn",
274
+ data: result,
275
+ });
276
+
277
+ try {
278
+ this.config.afterSend(data);
279
+ } catch (error) {
280
+ this.log(error.message, {
281
+ level: "error",
282
+ data: { afterSend: this.config.afterSend },
283
+ });
284
+ }
285
+ } else
286
+ this.log(
287
+ `Failed: ${response.status} (${response.statusText}). Details:`,
288
+ { level: "error", data: result }
289
+ );
290
+ })
291
+ .catch(() => {
292
+ if (response.ok) {
293
+ this.log("Successful!", { level: "success", data });
294
+
295
+ try {
296
+ this.config.afterSend(data);
297
+ } catch (error) {
298
+ this.log(error.message, {
299
+ level: "error",
300
+ data: { afterSend: this.config.afterSend },
301
+ });
302
+ }
303
+ } else
304
+ this.log(
305
+ `Failed: ${response.status} (${response.statusText}). Details:`,
306
+ { level: "error", data: response }
307
+ );
308
+ });
309
+ })
310
+ .catch((error) => {
311
+ if (this.config.debug)
312
+ this.log(`Failed: ${error.message}`, {
313
+ level: "error",
314
+ });
315
+ });
297
316
  }
298
317
  }
299
318
 
@@ -1,2 +1,2 @@
1
- /*! choreograph-create-pixel v0.1.0 2022/09/15 */
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 n=null!=arguments[r]?arguments[r]:{};r%2?e(Object(n),!0).forEach((function(e){o(t,e,n[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):e(Object(n)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(n,e))}))}return t}function r(e){return r="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},r(e)}function n(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 o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var a=function(e,t,r){var n,o=r.level,a=void 0===o?"info":o,c=r.key,i=void 0===c?null:c,s=r.data,l=void 0===s?void 0:s;if(e.debug&&e.logs[i]!==t){var u="font-family:HelveticaNeue;font-size:120%;margin-bottom:.5em;color:".concat({error:"#f44336",warn:"#ffa726",success:"#66bb6a",info:"#29b6f6"}[a]),f=["%cCHOREOGRAPH PIXEL DEBUGGER%c\n%c".concat({error:"⛔️",warn:"⚠️",success:"✅",info:"ℹ️"}[a]," ").concat(t).concat(i?"%c\n→ %c".concat(i):"%c%c"),"font-family:HelveticaNeue;font-weight:700;margin:1em 0 1.2em 0;font-size:80%;","",u,"","font-family:monospace;font-size:120%;border:1px solid;border-radius:5px;margin-bottom:1em;padding:.2em .6em"];l&&f.push(l),(n=console).info.apply(n,f),i&&(e.logs[i]=t)}},c=function(e,r){var n=t({},r);delete n.sku;var o="scrape"===e.what?"https://d.lemonpi.io/scrapes".concat(e.debug?"?validate=true":""):"https://d.lemonpi.io/a/".concat(e.who,"/product/event?e=").concat(encodeURIComponent(JSON.stringify({"event-type":{view:"product-viewed",basket:"product-basketed",purchase:"product-purchased"}[e.what],sku:r.sku})));"scrape"===e.what||e.debug?fetch(o,"scrape"===e.what?{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({"advertiser-id":e.who,sku:r.sku,fields:n})}:null).then((function(t){e.debug&&t.json().then((function(n){if(t.ok){a(e,'"'.concat(e.what,'" pixel successful, with warnings. Details:'),{level:"warn",data:n});try{e.afterSend(r)}catch(t){a(e,t.message,{level:"error",key:"afterSend"})}}else a(e,'"'.concat(e.what,'" pixel failed: ').concat(t.status," (").concat(t.statusText,"). Details:"),{level:"error",data:n})})).catch((function(){if(t.ok){a(e,'"'.concat(e.what,'" pixel successful!'),{level:"success",data:r});try{e.afterSend(r)}catch(t){a(e,t.message,{level:"error",key:"afterSend"})}}else a(e,'"'.concat(e.what,'" pixel failed: ').concat(t.status," (").concat(t.statusText,")"),{level:"error"})}))})).catch((function(t){e.debug&&a(e,'"'.concat(e.what,'" pixel failed: ').concat(t.message),{level:"error"})})):(new Image).src=o},i=function(e,r){var n={},o=!1;Object.keys(e.which).forEach((function(t){if(n[t]=e.which[t],"function"==typeof n[t])try{n[t]=n[t](r)}catch(r){a(e,r.message,{level:"error",key:"which.".concat(t)}),o=!0}"string"==typeof n[t]&&(n[t]=n[t].replace(/\s+/g," ").trim()),null!=n[t]&&""!==n[t]||(e.optionalFields.includes(t)?n[t]=null:(a(e,"This required field's value is empty",{level:"error",key:"which.".concat(t)}),o=!0))}));var i=JSON.stringify(n);if(!o&&e.lastJsonString!==i){a(e,'Scraped data for "'.concat(e.what,'" pixel:'),{data:n});try{e.beforeSend(n,(function(r){"scrape"!==e.what&&Array.isArray(r.sku)?r.sku.forEach((function(n){return c(e,t(t({},r),{},{sku:n}))})):c(e,r)}))}catch(t){a(e,t.message,{level:"error",key:"beforeSend"})}e.lastJsonString=i,e.logs={}}},s=function e(t){if(t.where.test(location.href))if(document.querySelector('html[class*="translated-"]'))a(t,"This page has been translated by the browser, and will be excluded",{level:"warn",key:"(browser translation)"});else if(t.when)try{var r=t.when.elements();r.forEach||(r=[r]),r.forEach((function(e){e.hasAttribute("choreograph-".concat(t.when.listener))||(e.addEventListener(t.when.listener,(function(){return i(t,e)})),e.setAttribute("choreograph-".concat(t.when.listener),""))}))}catch(e){a(t,e.message,{level:"error",key:"when.elements"})}else i(t);else a(t,"RegExp ".concat(t.where.toString()," does not match ").concat(location.href),{level:"warn",key:"where"});setTimeout((function(){return e(t)}),750)};return function(){function e(n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e);var o=t(t({debug:/(pixel|lemonpi)_debug/i.test(location.href),beforeSend:function(e,t){return t(e)},afterSend:function(){},optionalFields:[]},n),{},{logs:{}});(function(e){if("number"!=typeof e.who)a(e,"Please use a number",{level:"error",key:"who"});else if(["scrape","view","basket","purchase"].includes(e.what))if(e.where instanceof RegExp){if("object"===r(e.which)&&e.which.sku)return!0;a(e,"Please provide an SKU",{level:"error",key:"which.sku"})}else a(e,"Please use a regular expression",{level:"error",key:"where"});else a(e,"Please use scrape, view, basket, or purchase",{level:"error",key:"what"})})(o)&&s(o)}var o,c,i;return o=e,i=[{key:"getUrl",value:function(e){var t="".concat(location.protocol,"//").concat(location.host).concat(location.pathname),r=new URLSearchParams(window.location.search);if(e){var n=!1;e.allowedParameters&&e.allowedParameters.length&&e.allowedParameters.forEach((function(e){var o=n?"&":"?",a=encodeURI(e),c=r.get(e);null!=c&&(t+="".concat(o).concat(a,"=").concat(encodeURI(c)),n=!0)})),e.customParameters&&Object.keys(e.customParameters).forEach((function(r){var o=n?"&":"?",a=encodeURI(r),c=encodeURI(e.customParameters[r]);t+="".concat(o).concat(a,"=").concat(c),n=!0})),e.allowHash&&(t+=window.location.hash)}return t}}],(c=null)&&n(o.prototype,c),i&&n(o,i),Object.defineProperty(o,"prototype",{writable:!1}),e}()}();
1
+ /*! choreograph-create-pixel v0.1.3 2022/09/16 */
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}return 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:"🧾"},levels:{error:"⛔️",warn:"⚠️",success:"✅",info:"ℹ️"},titleCss:"font:700 80% HelveticaNeue;margin:12px 0 6px",messageCss:"font: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),beforeSend:function(e,t){return t(e)},afterSend:function(){},optionalFields:[]},n),this.validateConfig()&&this.cycle()}var i,s,c;return i=e,c=[{key:"getUrl",value:function(e){var t="".concat(location.protocol,"//").concat(location.host).concat(location.pathname),n=new URLSearchParams(location.search);if(e){var o=!1;e.allowedParameters&&e.allowedParameters.length&&e.allowedParameters.forEach((function(e){var r=o?"&":"?",i=encodeURI(e),s=n.get(e);null!=s&&(t+="".concat(r).concat(i,"=").concat(encodeURI(s)),o=!0)})),e.allowHash&&(t+=location.hash)}return t}},{key:"getUrlPathSegments",value:function(){return location.pathname.split("/").filter((function(e){return e})).map((function(e){return decodeURI(e)}))}},{key:"getUrlQueryParameters",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]||"")))}),{})}}],(s=[{key:"log",value:function(e,t){var n,o=t.level,r=void 0===o?"info":o,i=t.data,s=void 0===i?null:i;if(this.config.debug&&!this.logs.includes(e)){var c=["%cCHOREOGRAPH ".concat(this.config.what.toUpperCase()," PIXEL%c ").concat(this.settings.icons[this.config.what],"\n").concat(this.settings.levels[r]," %c").concat(e),this.settings.titleCss,"",this.settings.colors[r]?"".concat(this.settings.messageCss,";color:").concat(this.settings.colors[r]):this.settings.messageCss];s&&c.push(s),(n=console).info.apply(n,c),this.logs.push(e)}}},{key:"validateConfig",value:function(){if("number"!=typeof this.config.who)this.log("Please use a number",{level:"error",data:{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("Please provide an SKU",{level:"error",data:{which:this.config.which}})}else this.log("Please use a regular expression",{level:"error",data:{where:this.config.where}});else this.log("Please use scrape, view, basket, or purchase",{level:"error",data:{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("This page has been translated by the browser, and will be excluded",{level:"warn"});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(e.message,{level:"error",data:{when:{elements:this.config.when.elements}}})}else this.scrape();else this.log("Pattern does not match ".concat(location.href),{level:"warn",data:{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.log(e.message,{level:"error",data:{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.optionalFields.includes(t)?o[t]=null:(n.log("This required field's value is empty",{level:"error",data:{which:r({},t,o[t])}}),i=!0))}));var s=JSON.stringify(o);if(!i&&this.previouslyScrapedJsonString!==s){this.log("Scraped data:",{data:o});try{this.config.beforeSend(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(e.message,{level:"error",data:{beforeSend:this.config.beforeSend}})}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){n.config.debug&&t.json().then((function(o){if(t.ok){n.log("Successful, with warnings. Details:",{level:"warn",data:o});try{n.config.afterSend(e)}catch(e){n.log(e.message,{level:"error",data:{afterSend:n.config.afterSend}})}}else n.log("Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),{level:"error",data:o})})).catch((function(){if(t.ok){n.log("Successful!",{level:"success",data:e});try{n.config.afterSend(e)}catch(e){n.log(e.message,{level:"error",data:{afterSend:n.config.afterSend}})}}else n.log("Failed: ".concat(t.status," (").concat(t.statusText,"). Details:"),{level:"error",data:t})}))})).catch((function(e){n.config.debug&&n.log("Failed: ".concat(e.message),{level:"error"})})):(new Image).src=r}}])&&o(i.prototype,s),c&&o(i,c),Object.defineProperty(i,"prototype",{writable:!1}),e}()}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "choreograph-create-pixel",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "author": "Rick Stevens <rick.stevens@choreograph.com> (https://www.lemonpi.io/)",
5
5
  "repository": "github:rick-stevens/choreograph-create-pixel",
6
6
  "license": "ISC",