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 +26 -16
- package/dist/bundle.cjs.js +278 -259
- package/dist/bundle.esm.js +278 -259
- package/dist/bundle.iife.js +2 -2
- package/package.json +1 -1
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]
|
|
10
|
-
- [x]
|
|
9
|
+
- [x] Continuous URL validation
|
|
10
|
+
- [x] Condition pixels through user interaction
|
|
11
11
|
- [x] Prevents scraping browser-translated data
|
|
12
|
-
- [x]
|
|
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? (
|
|
25
|
+
// Who is the client? (advertiser ID)
|
|
26
26
|
who: 0,
|
|
27
27
|
|
|
28
|
-
// What
|
|
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? (
|
|
73
|
+
// Who is the client? (advertiser ID)
|
|
74
74
|
who: 0,
|
|
75
75
|
|
|
76
|
-
// What
|
|
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
|
|
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,
|
|
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
|
-
|
|
199
|
+
### `getUrlQueryParameters()`
|
|
186
200
|
|
|
187
|
-
|
|
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" }`.
|
package/dist/bundle.cjs.js
CHANGED
|
@@ -1,266 +1,38 @@
|
|
|
1
|
-
/*! choreograph-create-pixel v0.1.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
282
|
-
|
|
53
|
+
if (config.allowHash) {
|
|
54
|
+
url += location.hash;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
283
60
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/bundle.esm.js
CHANGED
|
@@ -1,264 +1,36 @@
|
|
|
1
|
-
/*! choreograph-create-pixel v0.1.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
280
|
-
|
|
51
|
+
if (config.allowHash) {
|
|
52
|
+
url += location.hash;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
281
58
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/bundle.iife.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! choreograph-create-pixel v0.1.
|
|
2
|
-
var ChoreographCreatePixel=function(){"use strict";function e(e,t){var
|
|
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