choreograph-create-pixel 0.1.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 ADDED
@@ -0,0 +1,191 @@
1
+ # Choreograph Create Pixel
2
+
3
+ This library lets you apply best practises to [Choreograph Create](https://www.lemonpi.io/) pixel development and implementation.
4
+
5
+ ## Features
6
+
7
+ - [x] Supports **scrape**, **view**, **basket**, and **purchase** pixels
8
+ - [x] Supports dynamic page changes
9
+ - [x] Placement-agnostic through continuous URL validation
10
+ - [x] Optionally enable pixels through user interaction
11
+ - [x] Prevents scraping browser-translated data
12
+ - [x] Error and console spam prevention
13
+
14
+ ## Quickstart
15
+
16
+ The following theoretical snippet could be embedded on every page of _example.com_, and would automatically determine when to enable, through URL detection.
17
+
18
+ ### Browser
19
+
20
+ ```html
21
+ <script src="https://unpkg.com/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 type 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
+ });
50
+
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
+ });
63
+ </script>
64
+ ```
65
+
66
+ ### ES module
67
+
68
+ ```js
69
+ import Pixel from "choreograph-create-pixel";
70
+
71
+ // Scrape pixel
72
+ new Pixel({
73
+ // Who is the client? (Advertiser ID)
74
+ who: 0,
75
+
76
+ // What type of pixel is this?
77
+ what: "scrape",
78
+
79
+ // Where should the pixel be enabled?
80
+ where: /example\.com\/products\/\d/,
81
+
82
+ // Which data should be scraped?
83
+ which: {
84
+ // Data layer example
85
+ sku: function () {
86
+ return window.dataLayer[0].product.sku;
87
+ },
88
+
89
+ // DOM example
90
+ name: function () {
91
+ return document.querySelector("#product-name").innerText;
92
+ },
93
+
94
+ // Helper example
95
+ url: Pixel.getUrl,
96
+ },
97
+ });
98
+
99
+ // View segment pixel
100
+ new ChoreographCreatePixel({
101
+ who: 0,
102
+ what: "view",
103
+ where: /example\.com\/products\/\d/,
104
+
105
+ which: {
106
+ sku: function () {
107
+ return window.dataLayer[0].product.sku;
108
+ },
109
+ },
110
+ });
111
+ ```
112
+
113
+ > ES modules require manual bundling and transpiling before they can be safely used as browser scripts.
114
+
115
+ ## Other scenarios
116
+
117
+ Besides **scrape** and **view** pixels, this library also supports **basket** and **purchase** segment pixels.
118
+
119
+ ### Basket segment
120
+
121
+ ```js
122
+ {
123
+ who: 0,
124
+ what: "basket",
125
+ where: /example\.com/,
126
+
127
+ // (Optional) When should the pixel be enabled?
128
+ when: {
129
+ listener: "click",
130
+ elements: function () {
131
+ return document.querySelectorAll("button.basket[data-sku]");
132
+ },
133
+ },
134
+
135
+ which: {
136
+ // The "element" parameter only exists while using a "when" condition
137
+ sku: function (element) {
138
+ return element.getAttribute("data-sku");
139
+ },
140
+ },
141
+ }
142
+ ```
143
+
144
+ ### Purchase segment
145
+
146
+ ```js
147
+ {
148
+ who: 0,
149
+ what: "purchase",
150
+ where: /example\.com\/thank-you-for-purchasing/,
151
+
152
+ which: {
153
+ // sku accepts an array of strings to enable multiple segment pixels at once
154
+ sku: function () {
155
+ return [].map.call(document.querySelectorAll('.purchased-product[data-sku]'), function (element) {
156
+ return element.getAttribute("data-sku");
157
+ });
158
+ },
159
+ },
160
+ }
161
+ ```
162
+
163
+ ## Configuration
164
+
165
+ | Property | Type | Description | Default |
166
+ | ---------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
167
+ | `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_ |
168
+ | `what` | String | Pixel type. One of: `"scrape"`, `"view"`, `"basket"`, or `"purchase"`. | _Required_ |
169
+ | `where` | RegExp | Disables pixels on page URLs that are not matched by this pattern. | _Required_ |
170
+ | `which` | Object | An object holding the data fields to be scraped. | _Required_ |
171
+ | `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_ |
172
+ | `which.*` | String, Number, Boolean, or Function | `*` should always match with existing field names in the advertiser's product store. | `undefined` |
173
+ | `when` | Object | Use this property when you want to enable the pixel after a specific DOM event. | `undefined` |
174
+ | `when.listener` | String | DOM event type. [List of supported values.](https://www.w3schools.com/jsref/dom_obj_event.asp) | _Required_ when using `when` |
175
+ | `when.elements` | Function | This function should return DOM element(s) to apply the listener to. | _Required_ when using `when` |
176
+ | `optionalFields` | Array | An array of field names (strings) that are allowed to have empty values. | `[]` |
177
+ | `beforeSend` | Function | Lifecycle function that's executed just before the pixel's data is sent. | `function (data, callback) { callback(data); }` |
178
+ | `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
+
181
+ ## Static methods (helpers)
182
+
183
+ ### `getUrl({ allowedParameters, customParameters, allowHash })`
184
+
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.
186
+
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` |
@@ -0,0 +1,302 @@
1
+ /*! choreograph-create-pixel v0.1.0 2022/09/15 */
2
+ 'use strict';
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
+ class ChoreographCreatePixel {
248
+ constructor(userConfig) {
249
+ const config = {
250
+ debug: /(pixel|lemonpi)_debug/i.test(location.href),
251
+ beforeSend: (data, callback) => callback(data),
252
+ afterSend: () => {},
253
+ optionalFields: [],
254
+ ...userConfig,
255
+ logs: {},
256
+ };
257
+
258
+ if (validateConfig(config)) cycle(config);
259
+ }
260
+
261
+ static getUrl(config) {
262
+ let url = `${location.protocol}//${location.host}${location.pathname}`;
263
+ const parameters = new URLSearchParams(window.location.search);
264
+
265
+ if (config) {
266
+ let paramAdded = false;
267
+
268
+ if (config.allowedParameters && config.allowedParameters.length) {
269
+ config.allowedParameters.forEach((parameter) => {
270
+ const separator = paramAdded ? "&" : "?";
271
+ const key = encodeURI(parameter);
272
+ const value = parameters.get(parameter);
273
+
274
+ if (value != null) {
275
+ url += `${separator}${key}=${encodeURI(value)}`;
276
+ paramAdded = true;
277
+ }
278
+ });
279
+ }
280
+
281
+ if (config.customParameters) {
282
+ const parameters = Object.keys(config.customParameters);
283
+
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;
290
+ });
291
+ }
292
+
293
+ if (config.allowHash) {
294
+ url += window.location.hash;
295
+ }
296
+ }
297
+
298
+ return url;
299
+ }
300
+ }
301
+
302
+ module.exports = ChoreographCreatePixel;
@@ -0,0 +1,300 @@
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
+
245
+ class ChoreographCreatePixel {
246
+ constructor(userConfig) {
247
+ const config = {
248
+ debug: /(pixel|lemonpi)_debug/i.test(location.href),
249
+ beforeSend: (data, callback) => callback(data),
250
+ afterSend: () => {},
251
+ optionalFields: [],
252
+ ...userConfig,
253
+ logs: {},
254
+ };
255
+
256
+ if (validateConfig(config)) cycle(config);
257
+ }
258
+
259
+ static getUrl(config) {
260
+ let url = `${location.protocol}//${location.host}${location.pathname}`;
261
+ const parameters = new URLSearchParams(window.location.search);
262
+
263
+ if (config) {
264
+ let paramAdded = false;
265
+
266
+ if (config.allowedParameters && config.allowedParameters.length) {
267
+ config.allowedParameters.forEach((parameter) => {
268
+ const separator = paramAdded ? "&" : "?";
269
+ const key = encodeURI(parameter);
270
+ const value = parameters.get(parameter);
271
+
272
+ if (value != null) {
273
+ url += `${separator}${key}=${encodeURI(value)}`;
274
+ paramAdded = true;
275
+ }
276
+ });
277
+ }
278
+
279
+ if (config.customParameters) {
280
+ const parameters = Object.keys(config.customParameters);
281
+
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;
288
+ });
289
+ }
290
+
291
+ if (config.allowHash) {
292
+ url += window.location.hash;
293
+ }
294
+ }
295
+
296
+ return url;
297
+ }
298
+ }
299
+
300
+ export { ChoreographCreatePixel as default };
@@ -0,0 +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}()}();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "choreograph-create-pixel",
3
+ "version": "0.1.0",
4
+ "author": "Rick Stevens <rick.stevens@choreograph.com> (https://www.lemonpi.io/)",
5
+ "repository": "github:rick-stevens/choreograph-create-pixel",
6
+ "license": "ISC",
7
+ "main": "dist/bundle.cjs.js",
8
+ "module": "dist/bundle.esm.js",
9
+ "browser": "dist/bundle.iife.js",
10
+ "unpkg": "dist/bundle.iife.js",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "lint": "eslint --ignore-path .gitignore .",
16
+ "format": "prettier --ignore-path .gitignore --check . '!**/*.{js,jsx,vue}'",
17
+ "build": "rollup -c",
18
+ "dev": "rollup -cw",
19
+ "prepare": "npm run lint && npm run format && npm run build"
20
+ },
21
+ "devDependencies": {
22
+ "@babel/core": "^7.19.1",
23
+ "@babel/preset-env": "^7.19.1",
24
+ "@rollup/plugin-babel": "^5.3.1",
25
+ "@rollup/plugin-eslint": "^8.0.2",
26
+ "eslint": "^8.23.1",
27
+ "eslint-config-prettier": "^8.5.0",
28
+ "eslint-plugin-prettier": "^4.2.1",
29
+ "moment": "^2.29.4",
30
+ "prettier": "^2.7.1",
31
+ "rollup": "^2.79.0",
32
+ "rollup-plugin-terser": "^7.0.2"
33
+ }
34
+ }