@optiqstudio/track 0.1.0 → 0.2.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/dist/browser.d.ts +19 -1
- package/dist/hash.d.ts +10 -0
- package/dist/index.cjs +95 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +95 -10
- package/dist/server.cjs +15 -2
- package/dist/server.d.ts +12 -0
- package/dist/server.js +15 -2
- package/dist/t.js +2 -2
- package/package.json +19 -4
package/dist/browser.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export interface InitOptions {
|
|
|
11
11
|
/** The visitor's choice, if the site already knows it. */
|
|
12
12
|
consent?: Consent;
|
|
13
13
|
}
|
|
14
|
+
/** One thing in a basket or an order. */
|
|
15
|
+
export interface Item {
|
|
16
|
+
id: string;
|
|
17
|
+
name?: string;
|
|
18
|
+
category?: string;
|
|
19
|
+
brand?: string;
|
|
20
|
+
variant?: string;
|
|
21
|
+
price?: number;
|
|
22
|
+
quantity?: number;
|
|
23
|
+
}
|
|
14
24
|
export interface Properties {
|
|
15
25
|
/** A sale's or deposit's amount. */
|
|
16
26
|
value?: number;
|
|
@@ -20,7 +30,9 @@ export interface Properties {
|
|
|
20
30
|
email?: string;
|
|
21
31
|
/** Hashed in the browser before anything is sent (digits only). */
|
|
22
32
|
phone?: string | number;
|
|
23
|
-
|
|
33
|
+
/** What was viewed, put in the basket or bought (view_content, add_to_cart, begin_checkout, purchase). */
|
|
34
|
+
items?: Item[];
|
|
35
|
+
[property: string]: string | number | boolean | Item[] | undefined;
|
|
24
36
|
}
|
|
25
37
|
export declare const COLLECTOR = "https://optiq-collect-951694748196.europe-west2.run.app/e";
|
|
26
38
|
/** One tracker on one page. Most sites use the module functions below, which share one. */
|
|
@@ -33,6 +45,7 @@ export declare class Tracker {
|
|
|
33
45
|
private clicks;
|
|
34
46
|
private visit;
|
|
35
47
|
private person;
|
|
48
|
+
private personKind;
|
|
36
49
|
private queue;
|
|
37
50
|
private timer;
|
|
38
51
|
private lastPath;
|
|
@@ -53,6 +66,11 @@ export declare class Tracker {
|
|
|
53
66
|
consent(state: Consent): void;
|
|
54
67
|
/** A page view (called by itself in auto mode, on load and on every move of a single-page site). */
|
|
55
68
|
page(): void;
|
|
69
|
+
/**
|
|
70
|
+
* What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
|
|
71
|
+
* its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
|
|
72
|
+
*/
|
|
73
|
+
private shopSignals;
|
|
56
74
|
private wire;
|
|
57
75
|
}
|
|
58
76
|
/** Which kind of contact a link begins, or null: tel: is a call, mailto: with an address an email, wa.me with a number WhatsApp. */
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** SHA-256 in hex, by Web Crypto (browsers, Node 20+, Deno, Bun, edge runtimes); null where it is missing. */
|
|
2
2
|
export declare function sha256(s: string): Promise<string | null>;
|
|
3
|
+
/** Which a person hash is of: the ad platforms take emails and phones apart. */
|
|
4
|
+
export type PersonKind = "email" | "phone";
|
|
5
|
+
/** The hash and what it is of, or null. */
|
|
6
|
+
export declare function personOf(p: {
|
|
7
|
+
email?: unknown;
|
|
8
|
+
phone?: unknown;
|
|
9
|
+
} | null | undefined): Promise<{
|
|
10
|
+
hash: string;
|
|
11
|
+
kind: PersonKind;
|
|
12
|
+
} | null>;
|
|
3
13
|
/** An email (trimmed, lower-case) or a phone (digits only), as a SHA-256: the only way a person is ever sent. */
|
|
4
14
|
export declare function hashPerson(p: {
|
|
5
15
|
email?: unknown;
|
package/dist/index.cjs
CHANGED
|
@@ -42,6 +42,18 @@ function sha256(s) {
|
|
|
42
42
|
() => null
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
+
async function personOf(p) {
|
|
46
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
|
|
47
|
+
const hash = await sha256(p.email.trim().toLowerCase());
|
|
48
|
+
return hash ? { hash, kind: "email" } : null;
|
|
49
|
+
}
|
|
50
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
51
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
52
|
+
const hash = digits.length >= 7 ? await sha256(digits) : null;
|
|
53
|
+
return hash ? { hash, kind: "phone" } : null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
45
57
|
function hashPerson(p) {
|
|
46
58
|
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
|
|
47
59
|
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
@@ -131,6 +143,7 @@ var Tracker = class {
|
|
|
131
143
|
ref: osr && /^[A-Za-z0-9]{4,16}$/.test(osr) ? osr : null
|
|
132
144
|
};
|
|
133
145
|
this.person = get("ph");
|
|
146
|
+
this.personKind = get("pk") || null;
|
|
134
147
|
if (o.auto !== false) this.wire();
|
|
135
148
|
}
|
|
136
149
|
visitor() {
|
|
@@ -186,7 +199,7 @@ var Tracker = class {
|
|
|
186
199
|
/** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
|
|
187
200
|
track(name, props = {}) {
|
|
188
201
|
const rest = {};
|
|
189
|
-
for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone"].includes(k) && v !== void 0) rest[k] = v;
|
|
202
|
+
for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone", "items"].includes(k) && v !== void 0 && !Array.isArray(v)) rest[k] = v;
|
|
190
203
|
const fresh = Date.now();
|
|
191
204
|
this.visit.last = fresh;
|
|
192
205
|
if (this.consentState !== "denied") put("visit", JSON.stringify(this.visit));
|
|
@@ -200,7 +213,7 @@ var Tracker = class {
|
|
|
200
213
|
page_url: window.location.origin + window.location.pathname,
|
|
201
214
|
referrer: document.referrer ? document.referrer.split("?")[0].slice(0, 300) : null,
|
|
202
215
|
utm: this.utm,
|
|
203
|
-
click_ids: denied ? {} : this.clicks,
|
|
216
|
+
click_ids: denied ? {} : { ...this.clicks, ...platformCookies() },
|
|
204
217
|
first_ref: this.touch.first ? this.touch.first.ref : null,
|
|
205
218
|
first_ref_at: this.touch.first ? this.touch.first.at : null,
|
|
206
219
|
last_ref: last ? last.ref : null,
|
|
@@ -208,24 +221,30 @@ var Tracker = class {
|
|
|
208
221
|
value: props.value === void 0 ? null : Number(props.value),
|
|
209
222
|
currency: props.currency || null,
|
|
210
223
|
properties: rest,
|
|
211
|
-
consent: this.consentState
|
|
224
|
+
consent: this.consentState,
|
|
225
|
+
...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
|
|
212
226
|
};
|
|
213
|
-
void
|
|
227
|
+
void personOf(denied ? null : props).then((h) => {
|
|
214
228
|
if (h) {
|
|
215
|
-
this.person = h;
|
|
216
|
-
|
|
229
|
+
this.person = h.hash;
|
|
230
|
+
this.personKind = h.kind;
|
|
231
|
+
put("ph", h.hash);
|
|
232
|
+
put("pk", h.kind);
|
|
217
233
|
}
|
|
218
234
|
ev.person_hash = denied ? null : this.person;
|
|
235
|
+
ev.person_kind = denied || !this.person ? null : this.personKind;
|
|
219
236
|
this.queue.push(ev);
|
|
220
237
|
if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
|
|
221
238
|
});
|
|
222
239
|
}
|
|
223
240
|
/** Who the visitor is, from now on: their email or phone, hashed here. */
|
|
224
241
|
identify(p) {
|
|
225
|
-
void
|
|
242
|
+
void personOf(p).then((h) => {
|
|
226
243
|
if (h && this.consentState !== "denied") {
|
|
227
|
-
this.person = h;
|
|
228
|
-
|
|
244
|
+
this.person = h.hash;
|
|
245
|
+
this.personKind = h.kind;
|
|
246
|
+
put("ph", h.hash);
|
|
247
|
+
put("pk", h.kind);
|
|
229
248
|
}
|
|
230
249
|
});
|
|
231
250
|
}
|
|
@@ -234,8 +253,9 @@ var Tracker = class {
|
|
|
234
253
|
this.consentState = state === "granted" || state === "denied" ? state : "unknown";
|
|
235
254
|
put("consent", this.consentState);
|
|
236
255
|
if (this.consentState === "denied") {
|
|
237
|
-
for (const k of ["vid", "ph", "clicks", "visit"]) put(k, null);
|
|
256
|
+
for (const k of ["vid", "ph", "pk", "clicks", "visit"]) put(k, null);
|
|
238
257
|
this.person = null;
|
|
258
|
+
this.personKind = null;
|
|
239
259
|
this.clicks = {};
|
|
240
260
|
}
|
|
241
261
|
}
|
|
@@ -245,6 +265,19 @@ var Tracker = class {
|
|
|
245
265
|
if (path === this.lastPath) return;
|
|
246
266
|
this.lastPath = path;
|
|
247
267
|
this.track("page_view");
|
|
268
|
+
if (this.wired) setTimeout(() => this.shopSignals(path), 1200);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
|
|
272
|
+
* its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
|
|
273
|
+
*/
|
|
274
|
+
shopSignals(path) {
|
|
275
|
+
if (path !== window.location.pathname + window.location.search) return;
|
|
276
|
+
const q = new URLSearchParams(window.location.search);
|
|
277
|
+
const query = ["q", "s", "search", "query", "keyword"].map((k) => q.get(k)).find((v) => v && v.trim());
|
|
278
|
+
if (query) this.track("search", { query: query.trim().slice(0, 200) });
|
|
279
|
+
const product = pageProduct();
|
|
280
|
+
if (product) this.track("view_content", { items: [product.item], value: product.item.price, currency: product.currency });
|
|
248
281
|
}
|
|
249
282
|
wire() {
|
|
250
283
|
if (this.wired) return;
|
|
@@ -297,6 +330,58 @@ var Tracker = class {
|
|
|
297
330
|
window.addEventListener("pagehide", () => this.flush());
|
|
298
331
|
}
|
|
299
332
|
};
|
|
333
|
+
function platformCookies() {
|
|
334
|
+
const out = {};
|
|
335
|
+
try {
|
|
336
|
+
for (const part of document.cookie.split(";")) {
|
|
337
|
+
const [k, ...v] = part.trim().split("=");
|
|
338
|
+
const value = decodeURIComponent(v.join("=") || "").slice(0, 300);
|
|
339
|
+
if (!value) continue;
|
|
340
|
+
if (k === "_fbp") out.fbp = value;
|
|
341
|
+
else if (k === "_fbc") out.fbc = value;
|
|
342
|
+
else if (k === "_ttp") out.ttp = value;
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function pageProduct() {
|
|
349
|
+
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
350
|
+
for (const el of Array.from(scripts)) {
|
|
351
|
+
let data;
|
|
352
|
+
try {
|
|
353
|
+
data = JSON.parse(el.textContent || "null");
|
|
354
|
+
} catch {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const queue = [data];
|
|
358
|
+
while (queue.length) {
|
|
359
|
+
const n = queue.shift();
|
|
360
|
+
if (Array.isArray(n)) {
|
|
361
|
+
queue.push(...n);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (!n || typeof n !== "object") continue;
|
|
365
|
+
const o = n;
|
|
366
|
+
if (o["@graph"]) queue.push(o["@graph"]);
|
|
367
|
+
const type = o["@type"];
|
|
368
|
+
if (type !== "Product" && !(Array.isArray(type) && type.includes("Product"))) continue;
|
|
369
|
+
const offers = Array.isArray(o.offers) ? o.offers[0] : o.offers;
|
|
370
|
+
const id = String(o.sku || o.productID || o.gtin13 || o.mpn || o["@id"] || o.url || o.name || "").slice(0, 100);
|
|
371
|
+
if (!id) continue;
|
|
372
|
+
const brand = typeof o.brand === "string" ? o.brand : o.brand?.name;
|
|
373
|
+
const price = Number(offers?.price ?? offers?.lowPrice);
|
|
374
|
+
const item = { id };
|
|
375
|
+
if (typeof o.name === "string") item.name = o.name.slice(0, 200);
|
|
376
|
+
if (typeof o.category === "string") item.category = o.category.slice(0, 120);
|
|
377
|
+
if (brand) item.brand = String(brand).slice(0, 120);
|
|
378
|
+
if (Number.isFinite(price)) item.price = price;
|
|
379
|
+
const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
|
|
380
|
+
return { item, currency };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
300
385
|
function contactChannel(href) {
|
|
301
386
|
if (/^tel:/i.test(href)) return "call";
|
|
302
387
|
if (/^mailto:[^?]/i.test(href)) return "email";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { COLLECTOR, Tracker, consent, contactChannel, flush, hashPerson, identify, init, page, track } from "./browser";
|
|
2
|
-
export type { Consent, InitOptions, Properties } from "./browser";
|
|
2
|
+
export type { Consent, InitOptions, Item, Properties } from "./browser";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,18 @@ function sha256(s) {
|
|
|
7
7
|
() => null
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
|
+
async function personOf(p) {
|
|
11
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
|
|
12
|
+
const hash = await sha256(p.email.trim().toLowerCase());
|
|
13
|
+
return hash ? { hash, kind: "email" } : null;
|
|
14
|
+
}
|
|
15
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
16
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
17
|
+
const hash = digits.length >= 7 ? await sha256(digits) : null;
|
|
18
|
+
return hash ? { hash, kind: "phone" } : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
10
22
|
function hashPerson(p) {
|
|
11
23
|
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
|
|
12
24
|
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
@@ -96,6 +108,7 @@ var Tracker = class {
|
|
|
96
108
|
ref: osr && /^[A-Za-z0-9]{4,16}$/.test(osr) ? osr : null
|
|
97
109
|
};
|
|
98
110
|
this.person = get("ph");
|
|
111
|
+
this.personKind = get("pk") || null;
|
|
99
112
|
if (o.auto !== false) this.wire();
|
|
100
113
|
}
|
|
101
114
|
visitor() {
|
|
@@ -151,7 +164,7 @@ var Tracker = class {
|
|
|
151
164
|
/** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
|
|
152
165
|
track(name, props = {}) {
|
|
153
166
|
const rest = {};
|
|
154
|
-
for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone"].includes(k) && v !== void 0) rest[k] = v;
|
|
167
|
+
for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone", "items"].includes(k) && v !== void 0 && !Array.isArray(v)) rest[k] = v;
|
|
155
168
|
const fresh = Date.now();
|
|
156
169
|
this.visit.last = fresh;
|
|
157
170
|
if (this.consentState !== "denied") put("visit", JSON.stringify(this.visit));
|
|
@@ -165,7 +178,7 @@ var Tracker = class {
|
|
|
165
178
|
page_url: window.location.origin + window.location.pathname,
|
|
166
179
|
referrer: document.referrer ? document.referrer.split("?")[0].slice(0, 300) : null,
|
|
167
180
|
utm: this.utm,
|
|
168
|
-
click_ids: denied ? {} : this.clicks,
|
|
181
|
+
click_ids: denied ? {} : { ...this.clicks, ...platformCookies() },
|
|
169
182
|
first_ref: this.touch.first ? this.touch.first.ref : null,
|
|
170
183
|
first_ref_at: this.touch.first ? this.touch.first.at : null,
|
|
171
184
|
last_ref: last ? last.ref : null,
|
|
@@ -173,24 +186,30 @@ var Tracker = class {
|
|
|
173
186
|
value: props.value === void 0 ? null : Number(props.value),
|
|
174
187
|
currency: props.currency || null,
|
|
175
188
|
properties: rest,
|
|
176
|
-
consent: this.consentState
|
|
189
|
+
consent: this.consentState,
|
|
190
|
+
...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
|
|
177
191
|
};
|
|
178
|
-
void
|
|
192
|
+
void personOf(denied ? null : props).then((h) => {
|
|
179
193
|
if (h) {
|
|
180
|
-
this.person = h;
|
|
181
|
-
|
|
194
|
+
this.person = h.hash;
|
|
195
|
+
this.personKind = h.kind;
|
|
196
|
+
put("ph", h.hash);
|
|
197
|
+
put("pk", h.kind);
|
|
182
198
|
}
|
|
183
199
|
ev.person_hash = denied ? null : this.person;
|
|
200
|
+
ev.person_kind = denied || !this.person ? null : this.personKind;
|
|
184
201
|
this.queue.push(ev);
|
|
185
202
|
if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
|
|
186
203
|
});
|
|
187
204
|
}
|
|
188
205
|
/** Who the visitor is, from now on: their email or phone, hashed here. */
|
|
189
206
|
identify(p) {
|
|
190
|
-
void
|
|
207
|
+
void personOf(p).then((h) => {
|
|
191
208
|
if (h && this.consentState !== "denied") {
|
|
192
|
-
this.person = h;
|
|
193
|
-
|
|
209
|
+
this.person = h.hash;
|
|
210
|
+
this.personKind = h.kind;
|
|
211
|
+
put("ph", h.hash);
|
|
212
|
+
put("pk", h.kind);
|
|
194
213
|
}
|
|
195
214
|
});
|
|
196
215
|
}
|
|
@@ -199,8 +218,9 @@ var Tracker = class {
|
|
|
199
218
|
this.consentState = state === "granted" || state === "denied" ? state : "unknown";
|
|
200
219
|
put("consent", this.consentState);
|
|
201
220
|
if (this.consentState === "denied") {
|
|
202
|
-
for (const k of ["vid", "ph", "clicks", "visit"]) put(k, null);
|
|
221
|
+
for (const k of ["vid", "ph", "pk", "clicks", "visit"]) put(k, null);
|
|
203
222
|
this.person = null;
|
|
223
|
+
this.personKind = null;
|
|
204
224
|
this.clicks = {};
|
|
205
225
|
}
|
|
206
226
|
}
|
|
@@ -210,6 +230,19 @@ var Tracker = class {
|
|
|
210
230
|
if (path === this.lastPath) return;
|
|
211
231
|
this.lastPath = path;
|
|
212
232
|
this.track("page_view");
|
|
233
|
+
if (this.wired) setTimeout(() => this.shopSignals(path), 1200);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
|
|
237
|
+
* its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
|
|
238
|
+
*/
|
|
239
|
+
shopSignals(path) {
|
|
240
|
+
if (path !== window.location.pathname + window.location.search) return;
|
|
241
|
+
const q = new URLSearchParams(window.location.search);
|
|
242
|
+
const query = ["q", "s", "search", "query", "keyword"].map((k) => q.get(k)).find((v) => v && v.trim());
|
|
243
|
+
if (query) this.track("search", { query: query.trim().slice(0, 200) });
|
|
244
|
+
const product = pageProduct();
|
|
245
|
+
if (product) this.track("view_content", { items: [product.item], value: product.item.price, currency: product.currency });
|
|
213
246
|
}
|
|
214
247
|
wire() {
|
|
215
248
|
if (this.wired) return;
|
|
@@ -262,6 +295,58 @@ var Tracker = class {
|
|
|
262
295
|
window.addEventListener("pagehide", () => this.flush());
|
|
263
296
|
}
|
|
264
297
|
};
|
|
298
|
+
function platformCookies() {
|
|
299
|
+
const out = {};
|
|
300
|
+
try {
|
|
301
|
+
for (const part of document.cookie.split(";")) {
|
|
302
|
+
const [k, ...v] = part.trim().split("=");
|
|
303
|
+
const value = decodeURIComponent(v.join("=") || "").slice(0, 300);
|
|
304
|
+
if (!value) continue;
|
|
305
|
+
if (k === "_fbp") out.fbp = value;
|
|
306
|
+
else if (k === "_fbc") out.fbc = value;
|
|
307
|
+
else if (k === "_ttp") out.ttp = value;
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
function pageProduct() {
|
|
314
|
+
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
315
|
+
for (const el of Array.from(scripts)) {
|
|
316
|
+
let data;
|
|
317
|
+
try {
|
|
318
|
+
data = JSON.parse(el.textContent || "null");
|
|
319
|
+
} catch {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const queue = [data];
|
|
323
|
+
while (queue.length) {
|
|
324
|
+
const n = queue.shift();
|
|
325
|
+
if (Array.isArray(n)) {
|
|
326
|
+
queue.push(...n);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!n || typeof n !== "object") continue;
|
|
330
|
+
const o = n;
|
|
331
|
+
if (o["@graph"]) queue.push(o["@graph"]);
|
|
332
|
+
const type = o["@type"];
|
|
333
|
+
if (type !== "Product" && !(Array.isArray(type) && type.includes("Product"))) continue;
|
|
334
|
+
const offers = Array.isArray(o.offers) ? o.offers[0] : o.offers;
|
|
335
|
+
const id = String(o.sku || o.productID || o.gtin13 || o.mpn || o["@id"] || o.url || o.name || "").slice(0, 100);
|
|
336
|
+
if (!id) continue;
|
|
337
|
+
const brand = typeof o.brand === "string" ? o.brand : o.brand?.name;
|
|
338
|
+
const price = Number(offers?.price ?? offers?.lowPrice);
|
|
339
|
+
const item = { id };
|
|
340
|
+
if (typeof o.name === "string") item.name = o.name.slice(0, 200);
|
|
341
|
+
if (typeof o.category === "string") item.category = o.category.slice(0, 120);
|
|
342
|
+
if (brand) item.brand = String(brand).slice(0, 120);
|
|
343
|
+
if (Number.isFinite(price)) item.price = price;
|
|
344
|
+
const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
|
|
345
|
+
return { item, currency };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
265
350
|
function contactChannel(href) {
|
|
266
351
|
if (/^tel:/i.test(href)) return "call";
|
|
267
352
|
if (/^mailto:[^?]/i.test(href)) return "email";
|
package/dist/server.cjs
CHANGED
|
@@ -34,6 +34,18 @@ function sha256(s) {
|
|
|
34
34
|
() => null
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
|
+
async function personOf(p) {
|
|
38
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
|
|
39
|
+
const hash = await sha256(p.email.trim().toLowerCase());
|
|
40
|
+
return hash ? { hash, kind: "email" } : null;
|
|
41
|
+
}
|
|
42
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
43
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
44
|
+
const hash = digits.length >= 7 ? await sha256(digits) : null;
|
|
45
|
+
return hash ? { hash, kind: "phone" } : null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
37
49
|
function hashPerson(p) {
|
|
38
50
|
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
|
|
39
51
|
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
@@ -91,14 +103,15 @@ var Optiq = class {
|
|
|
91
103
|
}
|
|
92
104
|
async prepare(e) {
|
|
93
105
|
const { email, phone, ...rest } = e;
|
|
94
|
-
const
|
|
106
|
+
const who = e.person_hash ? { hash: e.person_hash, kind: e.person_kind || "email" } : await personOf({ email, phone });
|
|
95
107
|
const iso = (v) => v instanceof Date ? v.toISOString() : v;
|
|
96
108
|
return {
|
|
97
109
|
...rest,
|
|
98
110
|
event_id: e.event_id || newId(),
|
|
99
111
|
occurred_at: iso(e.occurred_at),
|
|
100
112
|
last_ref_at: iso(e.last_ref_at),
|
|
101
|
-
person_hash: hash || void 0
|
|
113
|
+
person_hash: who?.hash || void 0,
|
|
114
|
+
person_kind: who?.kind || void 0
|
|
102
115
|
};
|
|
103
116
|
}
|
|
104
117
|
/** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
|
package/dist/server.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export interface ServerEvent {
|
|
|
12
12
|
phone?: string | number;
|
|
13
13
|
/** Who, if you already hash: SHA-256 of the trimmed, lower-case email, or of the phone's digits. */
|
|
14
14
|
person_hash?: string;
|
|
15
|
+
/** What person_hash is of, so the ad platforms match it as an email or a phone. Default "email". */
|
|
16
|
+
person_kind?: "email" | "phone";
|
|
17
|
+
/** What was bought: each with its id, and its name, category, price and quantity where known. */
|
|
18
|
+
items?: {
|
|
19
|
+
id: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
category?: string;
|
|
22
|
+
brand?: string;
|
|
23
|
+
variant?: string;
|
|
24
|
+
price?: number;
|
|
25
|
+
quantity?: number;
|
|
26
|
+
}[];
|
|
15
27
|
/** The browser's visitor id, if your site passes it to your server (localStorage optiq_vid). */
|
|
16
28
|
visitor_id?: string;
|
|
17
29
|
value?: number;
|
package/dist/server.js
CHANGED
|
@@ -7,6 +7,18 @@ function sha256(s) {
|
|
|
7
7
|
() => null
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
|
+
async function personOf(p) {
|
|
11
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
|
|
12
|
+
const hash = await sha256(p.email.trim().toLowerCase());
|
|
13
|
+
return hash ? { hash, kind: "email" } : null;
|
|
14
|
+
}
|
|
15
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
16
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
17
|
+
const hash = digits.length >= 7 ? await sha256(digits) : null;
|
|
18
|
+
return hash ? { hash, kind: "phone" } : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
10
22
|
function hashPerson(p) {
|
|
11
23
|
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
|
|
12
24
|
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
@@ -64,14 +76,15 @@ var Optiq = class {
|
|
|
64
76
|
}
|
|
65
77
|
async prepare(e) {
|
|
66
78
|
const { email, phone, ...rest } = e;
|
|
67
|
-
const
|
|
79
|
+
const who = e.person_hash ? { hash: e.person_hash, kind: e.person_kind || "email" } : await personOf({ email, phone });
|
|
68
80
|
const iso = (v) => v instanceof Date ? v.toISOString() : v;
|
|
69
81
|
return {
|
|
70
82
|
...rest,
|
|
71
83
|
event_id: e.event_id || newId(),
|
|
72
84
|
occurred_at: iso(e.occurred_at),
|
|
73
85
|
last_ref_at: iso(e.last_ref_at),
|
|
74
|
-
person_hash: hash || void 0
|
|
86
|
+
person_hash: who?.hash || void 0,
|
|
87
|
+
person_kind: who?.kind || void 0
|
|
75
88
|
};
|
|
76
89
|
}
|
|
77
90
|
/** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
|
package/dist/t.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/* Optiq Studio Enterprise tracking 0.
|
|
2
|
-
"use strict";(()=>{function k(n){let t=typeof globalThis!="undefined"?globalThis.crypto:void 0;return!t||!t.subtle||typeof TextEncoder=="undefined"?Promise.resolve(null):t.subtle.digest("SHA-256",new TextEncoder().encode(n)).then(e=>Array.from(new Uint8Array(e),r=>r.toString(16).padStart(2,"0")).join(""),()=>null)}function m(n){if(n&&typeof n.email=="string"&&n.email.indexOf("@")>0)return k(n.email.trim().toLowerCase());if(n&&(typeof n.phone=="string"||typeof n.phone=="number")){let t=String(n.phone).replace(/[^0-9]/g,"");if(t.length>=7)return k(t)}return Promise.resolve(null)}var A="https://optiq-collect-951694748196.europe-west2.run.app/e",b=864e5,P=30*6e4,S=()=>typeof window!="undefined"&&typeof document!="undefined";function p(n){try{return window.localStorage.getItem(`optiq_${n}`)}catch(t){return null}}function d(n,t){try{t===null?window.localStorage.removeItem(`optiq_${n}`):window.localStorage.setItem(`optiq_${n}`,t)}catch(e){}}function h(n){try{return JSON.parse(p(n)||"{}")||{}}catch(t){return{}}}function w(n){let t="abcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(n);typeof crypto!="undefined"&&crypto.getRandomValues&&crypto.getRandomValues(e);let r="";for(let s=0;s<n;s++)r+=t.charAt((e[s]||Math.floor(Math.random()*256))%36);return r}var y=class{constructor(t){this.queue=[];this.timer=null;this.lastPath=null;this.wired=!1;this.key=t.key,this.endpoint=t.endpoint||A,this.consentState=t.consent||p("consent")||"unknown";let e=new URLSearchParams(window.location.search),r=Date.now();this.touch=h("touch");let s=e.get("osr");if(s&&/^[A-Za-z0-9]{4,16}$/.test(s)){let l=new Date(r).toISOString();(!this.touch.first||r-Date.parse(this.touch.first.at)>90*b)&&(this.touch.first={ref:s,at:l}),this.touch.last={ref:s,at:l},d("touch",JSON.stringify(this.touch))}let i={};for(let l of["source","medium","campaign","content","term"]){let f=e.get(`utm_${l}`);f&&(i[l]=f.slice(0,200))}Object.keys(i).length&&d("utm",JSON.stringify(i)),this.utm=Object.keys(i).length?i:h("utm");let o={};for(let l of["gclid","fbclid","ttclid","wbraid","gbraid"]){let f=e.get(l);f&&(o[l]=f.slice(0,500))}Object.keys(o).length&&d("clicks",JSON.stringify(o)),this.clicks=Object.keys(o).length?o:h("clicks");let u=this.consentState==="denied"?{}:h("visit"),c=`${e.get("utm_campaign")||""}|${s||""}`;this.visit=u.id&&r-(u.last||0)<=P&&(c==="|"||c===u.from)?u:{id:w(20),started:new Date(r).toISOString(),from:c,landing:window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:i,click:e.get("gclid")?"gclid":e.get("fbclid")?"fbclid":e.get("ttclid")?"ttclid":null,ref:s&&/^[A-Za-z0-9]{4,16}$/.test(s)?s:null},this.person=p("ph"),t.auto!==!1&&this.wire()}visitor(){if(this.consentState==="denied")return null;let t=p("vid");return t||(t=w(20),d("vid",t)),t}context(){let t=null;try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch(r){t=null}let e=this.visit;return{id:e.id,started_at:e.started,landing:e.landing,referrer:e.referrer,utm:e.utm,click:e.click,ref:e.ref,lang:(navigator.language||"").slice(0,20)||null,tz:t,screen:window.screen?`${window.screen.width}x${window.screen.height}`:null}}flush(){for(this.timer&&clearTimeout(this.timer),this.timer=null;this.queue.length;){let t=JSON.stringify({key:this.key,visit:this.context(),events:this.queue.splice(0,50)}),e=!1;try{e=!!(navigator.sendBeacon&&navigator.sendBeacon(this.endpoint,new Blob([t],{type:"text/plain"})))}catch(r){e=!1}if(!e)try{fetch(this.endpoint,{method:"POST",body:t,keepalive:!0,mode:"cors",headers:{"Content-Type":"text/plain"}}).catch(()=>{})}catch(r){}}}track(t,e={}){let r={};for(let[c,l]of Object.entries(e))!["value","currency","email","phone"].includes(c)&&l!==void 0&&(r[c]=l);let s=Date.now();this.visit.last=s,this.consentState!=="denied"&&d("visit",JSON.stringify(this.visit));let i=this.touch.last&&s-Date.parse(this.touch.last.at)<=7*b?this.touch.last:null,o=this.consentState==="denied",u={event_id:w(18),name:String(t||"").toLowerCase(),occurred_at:new Date(s).toISOString(),visitor_id:this.visitor(),page_url:window.location.origin+window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:this.utm,click_ids:o?{}:this.clicks,first_ref:this.touch.first?this.touch.first.ref:null,first_ref_at:this.touch.first?this.touch.first.at:null,last_ref:i?i.ref:null,last_ref_at:i?i.at:null,value:e.value===void 0?null:Number(e.value),currency:e.currency||null,properties:r,consent:this.consentState};m(o?null:e).then(c=>{c&&(this.person=c,d("ph",c)),u.person_hash=o?null:this.person,this.queue.push(u),this.timer||(this.timer=setTimeout(()=>this.flush(),400))})}identify(t){m(t).then(e=>{e&&this.consentState!=="denied"&&(this.person=e,d("ph",e))})}consent(t){if(this.consentState=t==="granted"||t==="denied"?t:"unknown",d("consent",this.consentState),this.consentState==="denied"){for(let e of["vid","ph","clicks","visit"])d(e,null);this.person=null,this.clicks={}}}page(){let t=window.location.pathname+window.location.search;t!==this.lastPath&&(this.lastPath=t,this.track("page_view"))}wire(){if(this.wired)return;this.wired=!0,this.page();for(let e of["pushState","replaceState"]){let r=history[e];typeof r=="function"&&(history[e]=function(...s){let i=r.apply(this,s);return setTimeout(()=>a==null?void 0:a.page(),0),i})}window.addEventListener("popstate",()=>this.page());let t=typeof WeakSet=="function"?new WeakSet:null;document.addEventListener("focusin",e=>{var s,i;let r=(i=(s=e.target)==null?void 0:s.closest)==null?void 0:i.call(s,"form");!r||!t||t.has(r)||(t.add(r),this.track("form_start",{form:(r.getAttribute("name")||r.id||"").slice(0,60)||void 0}))}),document.addEventListener("submit",e=>{let r=e.target;if(!r||!r.getAttribute||r.getAttribute("data-optiq")!=="lead")return;let s=r.querySelector('input[type="email"]'),i=r.querySelector('input[type="tel"]');this.track("lead",{form:(r.getAttribute("name")||r.id||"").slice(0,60)||void 0,email:s?s.value:void 0,phone:i?i.value:void 0}),this.flush()}),document.addEventListener("click",e=>{var i,o;let r=(o=(i=e.target)==null?void 0:i.closest)==null?void 0:o.call(i,"a[href]");if(!r)return;let s=L(r.getAttribute("href")||"");s&&(this.track("contact",{channel:s}),this.flush())},!0),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}};function L(n){return/^tel:/i.test(n)?"call":/^mailto:[^?]/i.test(n)?"email":/^https?:\/\/(wa\.me\/\+?[0-9]|api\.whatsapp\.com\/send\/?\?(.*&)?phone=)/i.test(n)?"whatsapp":null}var a=null,_=[];function q(n){return S()?a||(!n||typeof n.key!="string"||!/^pk_[A-Za-z0-9]{20,40}$/.test(n.key)?(console.warn("[optiq] init needs the source's publishable key (pk_...), from the Developer page."),null):(a=new y(n),_.splice(0).forEach(t=>t()),a)):null}var g=n=>{S()&&(a?n(a):_.push(()=>a&&n(a)))},v=(n,t)=>g(e=>e.track(n,t)),T=n=>g(t=>t.identify(n)),O=n=>g(t=>t.consent(n)),x=()=>g(n=>n.page());(function(){if(window.__optiqLoaded)return;window.__optiqLoaded=!0;let n=document.currentScript||document.querySelector('script[src*="/t.js"][data-key]');if(!n)return;let t=(window.optiq&&window.optiq.q||[]).map(i=>Array.prototype.slice.call(i));function e(i,o,u){i==="track"?v(String(o),u||{}):i==="identify"?T(o||{}):i==="consent"?O(o):i==="page"&&x()}let r=(...i)=>e(...i);r.track=(i,o)=>v(i,o),window.optiq=r;let s=t.filter(i=>i[0]==="consent").pop();q({key:n.getAttribute("data-key")||"",endpoint:n.getAttribute("data-endpoint")||void 0,consent:s&&(s[1]==="granted"||s[1]==="denied")?s[1]:void 0}),t.forEach(i=>e(...i))})();})();
|
|
1
|
+
/* Optiq Studio Enterprise tracking 0.2.0: <script async src="https://enterprise.optiq.studio/t.js" data-key="pk_..."></script>. Built from @optiqstudio/track (packages/track); docs: https://enterprise.optiq.studio/developers */
|
|
2
|
+
"use strict";(()=>{var j=Object.defineProperty;var q=Object.getOwnPropertySymbols;var D=Object.prototype.hasOwnProperty,K=Object.prototype.propertyIsEnumerable;var A=(n,t,e)=>t in n?j(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,g=(n,t)=>{for(var e in t||(t={}))D.call(t,e)&&A(n,e,t[e]);if(q)for(var e of q(t))K.call(t,e)&&A(n,e,t[e]);return n};function P(n){let t=typeof globalThis!="undefined"?globalThis.crypto:void 0;return!t||!t.subtle||typeof TextEncoder=="undefined"?Promise.resolve(null):t.subtle.digest("SHA-256",new TextEncoder().encode(n)).then(e=>Array.from(new Uint8Array(e),s=>s.toString(16).padStart(2,"0")).join(""),()=>null)}async function w(n){if(n&&typeof n.email=="string"&&n.email.indexOf("@")>0){let t=await P(n.email.trim().toLowerCase());return t?{hash:t,kind:"email"}:null}if(n&&(typeof n.phone=="string"||typeof n.phone=="number")){let t=String(n.phone).replace(/[^0-9]/g,""),e=t.length>=7?await P(t):null;return e?{hash:e,kind:"phone"}:null}return null}var $="https://optiq-collect-951694748196.europe-west2.run.app/e",x=864e5,N=30*6e4,O=()=>typeof window!="undefined"&&typeof document!="undefined";function h(n){try{return window.localStorage.getItem(`optiq_${n}`)}catch(t){return null}}function u(n,t){try{t===null?window.localStorage.removeItem(`optiq_${n}`):window.localStorage.setItem(`optiq_${n}`,t)}catch(e){}}function m(n){try{return JSON.parse(h(n)||"{}")||{}}catch(t){return{}}}function k(n){let t="abcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(n);typeof crypto!="undefined"&&crypto.getRandomValues&&crypto.getRandomValues(e);let s="";for(let r=0;r<n;r++)s+=t.charAt((e[r]||Math.floor(Math.random()*256))%36);return s}var v=class{constructor(t){this.queue=[];this.timer=null;this.lastPath=null;this.wired=!1;this.key=t.key,this.endpoint=t.endpoint||$,this.consentState=t.consent||h("consent")||"unknown";let e=new URLSearchParams(window.location.search),s=Date.now();this.touch=m("touch");let r=e.get("osr");if(r&&/^[A-Za-z0-9]{4,16}$/.test(r)){let c=new Date(s).toISOString();(!this.touch.first||s-Date.parse(this.touch.first.at)>90*x)&&(this.touch.first={ref:r,at:c}),this.touch.last={ref:r,at:c},u("touch",JSON.stringify(this.touch))}let i={};for(let c of["source","medium","campaign","content","term"]){let p=e.get(`utm_${c}`);p&&(i[c]=p.slice(0,200))}Object.keys(i).length&&u("utm",JSON.stringify(i)),this.utm=Object.keys(i).length?i:m("utm");let a={};for(let c of["gclid","fbclid","ttclid","wbraid","gbraid"]){let p=e.get(c);p&&(a[c]=p.slice(0,500))}Object.keys(a).length&&u("clicks",JSON.stringify(a)),this.clicks=Object.keys(a).length?a:m("clicks");let o=this.consentState==="denied"?{}:m("visit"),l=`${e.get("utm_campaign")||""}|${r||""}`;this.visit=o.id&&s-(o.last||0)<=N&&(l==="|"||l===o.from)?o:{id:k(20),started:new Date(s).toISOString(),from:l,landing:window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:i,click:e.get("gclid")?"gclid":e.get("fbclid")?"fbclid":e.get("ttclid")?"ttclid":null,ref:r&&/^[A-Za-z0-9]{4,16}$/.test(r)?r:null},this.person=h("ph"),this.personKind=h("pk")||null,t.auto!==!1&&this.wire()}visitor(){if(this.consentState==="denied")return null;let t=h("vid");return t||(t=k(20),u("vid",t)),t}context(){let t=null;try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch(s){t=null}let e=this.visit;return{id:e.id,started_at:e.started,landing:e.landing,referrer:e.referrer,utm:e.utm,click:e.click,ref:e.ref,lang:(navigator.language||"").slice(0,20)||null,tz:t,screen:window.screen?`${window.screen.width}x${window.screen.height}`:null}}flush(){for(this.timer&&clearTimeout(this.timer),this.timer=null;this.queue.length;){let t=JSON.stringify({key:this.key,visit:this.context(),events:this.queue.splice(0,50)}),e=!1;try{e=!!(navigator.sendBeacon&&navigator.sendBeacon(this.endpoint,new Blob([t],{type:"text/plain"})))}catch(s){e=!1}if(!e)try{fetch(this.endpoint,{method:"POST",body:t,keepalive:!0,mode:"cors",headers:{"Content-Type":"text/plain"}}).catch(()=>{})}catch(s){}}}track(t,e={}){let s={};for(let[l,c]of Object.entries(e))!["value","currency","email","phone","items"].includes(l)&&c!==void 0&&!Array.isArray(c)&&(s[l]=c);let r=Date.now();this.visit.last=r,this.consentState!=="denied"&&u("visit",JSON.stringify(this.visit));let i=this.touch.last&&r-Date.parse(this.touch.last.at)<=7*x?this.touch.last:null,a=this.consentState==="denied",o=g({event_id:k(18),name:String(t||"").toLowerCase(),occurred_at:new Date(r).toISOString(),visitor_id:this.visitor(),page_url:window.location.origin+window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:this.utm,click_ids:a?{}:g(g({},this.clicks),H()),first_ref:this.touch.first?this.touch.first.ref:null,first_ref_at:this.touch.first?this.touch.first.at:null,last_ref:i?i.ref:null,last_ref_at:i?i.at:null,value:e.value===void 0?null:Number(e.value),currency:e.currency||null,properties:s,consent:this.consentState},Array.isArray(e.items)&&e.items.length?{items:e.items.slice(0,50)}:{});w(a?null:e).then(l=>{l&&(this.person=l.hash,this.personKind=l.kind,u("ph",l.hash),u("pk",l.kind)),o.person_hash=a?null:this.person,o.person_kind=a||!this.person?null:this.personKind,this.queue.push(o),this.timer||(this.timer=setTimeout(()=>this.flush(),400))})}identify(t){w(t).then(e=>{e&&this.consentState!=="denied"&&(this.person=e.hash,this.personKind=e.kind,u("ph",e.hash),u("pk",e.kind))})}consent(t){if(this.consentState=t==="granted"||t==="denied"?t:"unknown",u("consent",this.consentState),this.consentState==="denied"){for(let e of["vid","ph","pk","clicks","visit"])u(e,null);this.person=null,this.personKind=null,this.clicks={}}}page(){let t=window.location.pathname+window.location.search;t!==this.lastPath&&(this.lastPath=t,this.track("page_view"),this.wired&&setTimeout(()=>this.shopSignals(t),1200))}shopSignals(t){if(t!==window.location.pathname+window.location.search)return;let e=new URLSearchParams(window.location.search),s=["q","s","search","query","keyword"].map(i=>e.get(i)).find(i=>i&&i.trim());s&&this.track("search",{query:s.trim().slice(0,200)});let r=J();r&&this.track("view_content",{items:[r.item],value:r.item.price,currency:r.currency})}wire(){if(this.wired)return;this.wired=!0,this.page();for(let e of["pushState","replaceState"]){let s=history[e];typeof s=="function"&&(history[e]=function(...r){let i=s.apply(this,r);return setTimeout(()=>d==null?void 0:d.page(),0),i})}window.addEventListener("popstate",()=>this.page());let t=typeof WeakSet=="function"?new WeakSet:null;document.addEventListener("focusin",e=>{var r,i;let s=(i=(r=e.target)==null?void 0:r.closest)==null?void 0:i.call(r,"form");!s||!t||t.has(s)||(t.add(s),this.track("form_start",{form:(s.getAttribute("name")||s.id||"").slice(0,60)||void 0}))}),document.addEventListener("submit",e=>{let s=e.target;if(!s||!s.getAttribute||s.getAttribute("data-optiq")!=="lead")return;let r=s.querySelector('input[type="email"]'),i=s.querySelector('input[type="tel"]');this.track("lead",{form:(s.getAttribute("name")||s.id||"").slice(0,60)||void 0,email:r?r.value:void 0,phone:i?i.value:void 0}),this.flush()}),document.addEventListener("click",e=>{var i,a;let s=(a=(i=e.target)==null?void 0:i.closest)==null?void 0:a.call(i,"a[href]");if(!s)return;let r=M(s.getAttribute("href")||"");r&&(this.track("contact",{channel:r}),this.flush())},!0),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}};function H(){let n={};try{for(let t of document.cookie.split(";")){let[e,...s]=t.trim().split("="),r=decodeURIComponent(s.join("=")||"").slice(0,300);r&&(e==="_fbp"?n.fbp=r:e==="_fbc"?n.fbc=r:e==="_ttp"&&(n.ttp=r))}}catch(t){}return n}function J(){var t,e;let n=document.querySelectorAll('script[type="application/ld+json"]');for(let s of Array.from(n)){let r;try{r=JSON.parse(s.textContent||"null")}catch(a){continue}let i=[r];for(;i.length;){let a=i.shift();if(Array.isArray(a)){i.push(...a);continue}if(!a||typeof a!="object")continue;let o=a;o["@graph"]&&i.push(o["@graph"]);let l=o["@type"];if(l!=="Product"&&!(Array.isArray(l)&&l.includes("Product")))continue;let c=Array.isArray(o.offers)?o.offers[0]:o.offers,p=String(o.sku||o.productID||o.gtin13||o.mpn||o["@id"]||o.url||o.name||"").slice(0,100);if(!p)continue;let S=typeof o.brand=="string"?o.brand:(t=o.brand)==null?void 0:t.name,_=Number((e=c==null?void 0:c.price)!=null?e:c==null?void 0:c.lowPrice),f={id:p};typeof o.name=="string"&&(f.name=o.name.slice(0,200)),typeof o.category=="string"&&(f.category=o.category.slice(0,120)),S&&(f.brand=String(S).slice(0,120)),Number.isFinite(_)&&(f.price=_);let E=typeof(c==null?void 0:c.priceCurrency)=="string"?c.priceCurrency.toUpperCase().slice(0,3):void 0;return{item:f,currency:E}}}return null}function M(n){return/^tel:/i.test(n)?"call":/^mailto:[^?]/i.test(n)?"email":/^https?:\/\/(wa\.me\/\+?[0-9]|api\.whatsapp\.com\/send\/?\?(.*&)?phone=)/i.test(n)?"whatsapp":null}var d=null,T=[];function R(n){return O()?d||(!n||typeof n.key!="string"||!/^pk_[A-Za-z0-9]{20,40}$/.test(n.key)?(console.warn("[optiq] init needs the source's publishable key (pk_...), from the Developer page."),null):(d=new v(n),T.splice(0).forEach(t=>t()),d)):null}var y=n=>{O()&&(d?n(d):T.push(()=>d&&n(d)))},b=(n,t)=>y(e=>e.track(n,t)),C=n=>y(t=>t.identify(n)),L=n=>y(t=>t.consent(n)),I=()=>y(n=>n.page());(function(){if(window.__optiqLoaded)return;window.__optiqLoaded=!0;let n=document.currentScript||document.querySelector('script[src*="/t.js"][data-key]');if(!n)return;let t=(window.optiq&&window.optiq.q||[]).map(i=>Array.prototype.slice.call(i));function e(i,a,o){i==="track"?b(String(a),o||{}):i==="identify"?C(a||{}):i==="consent"?L(a):i==="page"&&I()}let s=(...i)=>e(...i);s.track=(i,a)=>b(i,a),window.optiq=s;let r=t.filter(i=>i[0]==="consent").pop();R({key:n.getAttribute("data-key")||"",endpoint:n.getAttribute("data-endpoint")||void 0,consent:r&&(r[1]==="granted"||r[1]==="denied")?r[1]:void 0}),t.forEach(i=>e(...i))})();})();
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optiqstudio/track",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Optiq Studio Enterprise tracking: join every lead, sale and deposit to the video, post or advert that brought it. Browser and server.",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"optiq",
|
|
7
|
+
"optiq studio",
|
|
8
|
+
"attribution",
|
|
9
|
+
"tracking",
|
|
10
|
+
"conversions",
|
|
11
|
+
"analytics",
|
|
12
|
+
"marketing",
|
|
13
|
+
"whatsapp"
|
|
14
|
+
],
|
|
6
15
|
"homepage": "https://enterprise.optiq.studio/developers",
|
|
7
16
|
"license": "MIT",
|
|
8
17
|
"author": "DaveLabs <info@davelabs.co> (https://davelabs.co)",
|
|
@@ -24,8 +33,14 @@
|
|
|
24
33
|
},
|
|
25
34
|
"./package.json": "./package.json"
|
|
26
35
|
},
|
|
27
|
-
"files": [
|
|
28
|
-
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
29
44
|
"scripts": {
|
|
30
45
|
"build": "node build.mjs",
|
|
31
46
|
"test": "node --test test/*.test.mjs",
|