@optiqstudio/track 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/browser.d.ts +44 -0
- package/dist/index.cjs +134 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +134 -1
- package/dist/server.cjs +28 -1
- package/dist/server.d.ts +24 -0
- package/dist/server.js +28 -1
- package/dist/t.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,11 @@ Sent by itself:
|
|
|
28
28
|
- page views, including single-page moves;
|
|
29
29
|
- form starts;
|
|
30
30
|
- leads from forms marked `data-optiq="lead"`;
|
|
31
|
-
- presses on WhatsApp, call and email links
|
|
31
|
+
- presses on WhatsApp, call and email links;
|
|
32
|
+
- how far each visitor watches each video on the page (`video_progress`: at the start, at 25, 50, 75 and 100%,
|
|
33
|
+
with the seconds actually played). Any player built on `<video>` counts when it plays with sound or its own
|
|
34
|
+
controls; a muted background loop does not. Name a video with `data-optiq-video="Spring advert"`, or leave one
|
|
35
|
+
out with `data-optiq-video="off"`.
|
|
32
36
|
|
|
33
37
|
`init({ key, auto: false })` turns these off. Call `page()` yourself when they are off.
|
|
34
38
|
|
package/dist/browser.d.ts
CHANGED
|
@@ -10,6 +10,31 @@ export interface InitOptions {
|
|
|
10
10
|
endpoint?: string;
|
|
11
11
|
/** The visitor's choice, if the site already knows it. */
|
|
12
12
|
consent?: Consent;
|
|
13
|
+
/** Where suggestions come from. Default: the Optiq Studio Enterprise API. */
|
|
14
|
+
api?: string;
|
|
15
|
+
}
|
|
16
|
+
/** A product to show a shopper, and why. */
|
|
17
|
+
export interface Suggestion {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string | null;
|
|
20
|
+
category: string | null;
|
|
21
|
+
brand: string | null;
|
|
22
|
+
price: number | null;
|
|
23
|
+
currency: string | null;
|
|
24
|
+
url: string | null;
|
|
25
|
+
image: string | null;
|
|
26
|
+
/** bought_together, viewed_together, in_category, popular or recently_viewed. */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** The product it goes with (bought_together, viewed_together). */
|
|
29
|
+
because: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface SuggestOptions {
|
|
32
|
+
/** The product on screen: suggestions go with it, and it is left out. */
|
|
33
|
+
product?: string;
|
|
34
|
+
/** Product ids to leave out (what is already in the basket, for example). */
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
/** How many, 1 to 24. Default 8. */
|
|
37
|
+
limit?: number;
|
|
13
38
|
}
|
|
14
39
|
/** One thing in a basket or an order. */
|
|
15
40
|
export interface Item {
|
|
@@ -20,6 +45,10 @@ export interface Item {
|
|
|
20
45
|
variant?: string;
|
|
21
46
|
price?: number;
|
|
22
47
|
quantity?: number;
|
|
48
|
+
/** The product's page. */
|
|
49
|
+
url?: string;
|
|
50
|
+
/** The product's picture. */
|
|
51
|
+
image?: string;
|
|
23
52
|
}
|
|
24
53
|
export interface Properties {
|
|
25
54
|
/** A sale's or deposit's amount. */
|
|
@@ -35,10 +64,12 @@ export interface Properties {
|
|
|
35
64
|
[property: string]: string | number | boolean | Item[] | undefined;
|
|
36
65
|
}
|
|
37
66
|
export declare const COLLECTOR = "https://optiq-collect-951694748196.europe-west2.run.app/e";
|
|
67
|
+
export declare const API = "https://enterpriseapi-951694748196.europe-west2.run.app";
|
|
38
68
|
/** One tracker on one page. Most sites use the module functions below, which share one. */
|
|
39
69
|
export declare class Tracker {
|
|
40
70
|
private key;
|
|
41
71
|
private endpoint;
|
|
72
|
+
private api;
|
|
42
73
|
private consentState;
|
|
43
74
|
private touch;
|
|
44
75
|
private utm;
|
|
@@ -48,6 +79,9 @@ export declare class Tracker {
|
|
|
48
79
|
private personKind;
|
|
49
80
|
private queue;
|
|
50
81
|
private timer;
|
|
82
|
+
/** Events still being hashed (a moment), and whether a flush asked for them to go the moment they are ready. */
|
|
83
|
+
private hashing;
|
|
84
|
+
private urgent;
|
|
51
85
|
private lastPath;
|
|
52
86
|
private wired;
|
|
53
87
|
constructor(o: InitOptions);
|
|
@@ -57,6 +91,11 @@ export declare class Tracker {
|
|
|
57
91
|
flush(): void;
|
|
58
92
|
/** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
|
|
59
93
|
track(name: string, props?: Properties): void;
|
|
94
|
+
/** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
|
|
95
|
+
suggest(o?: SuggestOptions): Promise<{
|
|
96
|
+
suggestions: Suggestion[];
|
|
97
|
+
recentlyViewed: Suggestion[];
|
|
98
|
+
}>;
|
|
60
99
|
/** Who the visitor is, from now on: their email or phone, hashed here. */
|
|
61
100
|
identify(p: {
|
|
62
101
|
email?: string;
|
|
@@ -90,3 +129,8 @@ export declare const consent: (state: Consent) => void;
|
|
|
90
129
|
export declare const page: () => void;
|
|
91
130
|
/** Sends what waits now. */
|
|
92
131
|
export declare const flush: () => void;
|
|
132
|
+
/** Products to show this shopper, and why (an empty list if anything goes wrong, so a page never breaks). */
|
|
133
|
+
export declare function suggest(o?: SuggestOptions): Promise<{
|
|
134
|
+
suggestions: Suggestion[];
|
|
135
|
+
recentlyViewed: Suggestion[];
|
|
136
|
+
}>;
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
API: () => API,
|
|
23
24
|
COLLECTOR: () => COLLECTOR,
|
|
24
25
|
Tracker: () => Tracker,
|
|
25
26
|
consent: () => consent,
|
|
@@ -29,6 +30,7 @@ __export(index_exports, {
|
|
|
29
30
|
identify: () => identify,
|
|
30
31
|
init: () => init,
|
|
31
32
|
page: () => page,
|
|
33
|
+
suggest: () => suggest,
|
|
32
34
|
track: () => track
|
|
33
35
|
});
|
|
34
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -65,6 +67,7 @@ function hashPerson(p) {
|
|
|
65
67
|
|
|
66
68
|
// src/browser.ts
|
|
67
69
|
var COLLECTOR = "https://optiq-collect-951694748196.europe-west2.run.app/e";
|
|
70
|
+
var API = "https://enterpriseapi-951694748196.europe-west2.run.app";
|
|
68
71
|
var DAY = 864e5;
|
|
69
72
|
var IDLE = 30 * 6e4;
|
|
70
73
|
var hasWindow = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
@@ -101,10 +104,14 @@ var Tracker = class {
|
|
|
101
104
|
constructor(o) {
|
|
102
105
|
this.queue = [];
|
|
103
106
|
this.timer = null;
|
|
107
|
+
/** Events still being hashed (a moment), and whether a flush asked for them to go the moment they are ready. */
|
|
108
|
+
this.hashing = 0;
|
|
109
|
+
this.urgent = false;
|
|
104
110
|
this.lastPath = null;
|
|
105
111
|
this.wired = false;
|
|
106
112
|
this.key = o.key;
|
|
107
113
|
this.endpoint = o.endpoint || COLLECTOR;
|
|
114
|
+
this.api = (o.api || API).replace(/\/+$/, "");
|
|
108
115
|
this.consentState = o.consent || get("consent") || "unknown";
|
|
109
116
|
const q = new URLSearchParams(window.location.search);
|
|
110
117
|
const now = Date.now();
|
|
@@ -180,6 +187,7 @@ var Tracker = class {
|
|
|
180
187
|
flush() {
|
|
181
188
|
if (this.timer) clearTimeout(this.timer);
|
|
182
189
|
this.timer = null;
|
|
190
|
+
if (this.hashing) this.urgent = true;
|
|
183
191
|
while (this.queue.length) {
|
|
184
192
|
const body = JSON.stringify({ key: this.key, visit: this.context(), events: this.queue.splice(0, 50) });
|
|
185
193
|
let sent = false;
|
|
@@ -224,7 +232,9 @@ var Tracker = class {
|
|
|
224
232
|
consent: this.consentState,
|
|
225
233
|
...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
|
|
226
234
|
};
|
|
235
|
+
this.hashing++;
|
|
227
236
|
void personOf(denied ? null : props).then((h) => {
|
|
237
|
+
this.hashing--;
|
|
228
238
|
if (h) {
|
|
229
239
|
this.person = h.hash;
|
|
230
240
|
this.personKind = h.kind;
|
|
@@ -234,9 +244,28 @@ var Tracker = class {
|
|
|
234
244
|
ev.person_hash = denied ? null : this.person;
|
|
235
245
|
ev.person_kind = denied || !this.person ? null : this.personKind;
|
|
236
246
|
this.queue.push(ev);
|
|
237
|
-
if (
|
|
247
|
+
if (this.urgent && !this.hashing) {
|
|
248
|
+
this.urgent = false;
|
|
249
|
+
this.flush();
|
|
250
|
+
} else if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
|
|
238
251
|
});
|
|
239
252
|
}
|
|
253
|
+
/** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
|
|
254
|
+
async suggest(o = {}) {
|
|
255
|
+
const q = new URLSearchParams({ key: this.key });
|
|
256
|
+
const visitor = this.consentState === "denied" ? null : get("vid");
|
|
257
|
+
if (visitor) q.set("visitor", visitor);
|
|
258
|
+
if (o.product) q.set("product", o.product);
|
|
259
|
+
if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
|
|
260
|
+
if (o.limit) q.set("limit", String(o.limit));
|
|
261
|
+
try {
|
|
262
|
+
const r = await fetch(`${this.api}/api/v1/suggest?${q}`, { mode: "cors", credentials: "omit" });
|
|
263
|
+
if (!r.ok) return { suggestions: [], recentlyViewed: [] };
|
|
264
|
+
return await r.json();
|
|
265
|
+
} catch {
|
|
266
|
+
return { suggestions: [], recentlyViewed: [] };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
240
269
|
/** Who the visitor is, from now on: their email or phone, hashed here. */
|
|
241
270
|
identify(p) {
|
|
242
271
|
void personOf(p).then((h) => {
|
|
@@ -324,12 +353,108 @@ var Tracker = class {
|
|
|
324
353
|
},
|
|
325
354
|
true
|
|
326
355
|
);
|
|
356
|
+
watchVideos((p) => this.track("video_progress", p));
|
|
327
357
|
document.addEventListener("visibilitychange", () => {
|
|
328
358
|
if (document.visibilityState === "hidden") this.flush();
|
|
329
359
|
});
|
|
330
360
|
window.addEventListener("pagehide", () => this.flush());
|
|
331
361
|
}
|
|
332
362
|
};
|
|
363
|
+
function watchVideos(send) {
|
|
364
|
+
if (typeof WeakMap !== "function") return;
|
|
365
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
366
|
+
const playing = [];
|
|
367
|
+
const nameOf = (v) => {
|
|
368
|
+
const marked = v.getAttribute("data-optiq-video");
|
|
369
|
+
if (marked && marked !== "on") return marked.slice(0, 120);
|
|
370
|
+
const label = v.getAttribute("title") || v.getAttribute("aria-label");
|
|
371
|
+
if (label) return label.slice(0, 120);
|
|
372
|
+
for (const u of [v.currentSrc, v.getAttribute("src"), v.poster]) {
|
|
373
|
+
if (!u || u.indexOf("blob:") === 0 || u.indexOf("data:") === 0) continue;
|
|
374
|
+
const file = u.split("?")[0].split("#")[0].split("/").filter(Boolean).pop();
|
|
375
|
+
if (file) {
|
|
376
|
+
try {
|
|
377
|
+
return decodeURIComponent(file).slice(0, 120);
|
|
378
|
+
} catch {
|
|
379
|
+
return file.slice(0, 120);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return (document.title || "video").slice(0, 120);
|
|
384
|
+
};
|
|
385
|
+
const counts = (v) => {
|
|
386
|
+
const marked = v.getAttribute("data-optiq-video");
|
|
387
|
+
if (marked === "off") return false;
|
|
388
|
+
if (marked) return true;
|
|
389
|
+
return !v.loop && (v.controls || !v.muted);
|
|
390
|
+
};
|
|
391
|
+
const report = (v, s, percent) => {
|
|
392
|
+
const duration = Number.isFinite(v.duration) && v.duration > 0 ? Math.round(v.duration) : void 0;
|
|
393
|
+
send({ video: nameOf(v), percent, seconds: Math.round(s.played), duration });
|
|
394
|
+
s.sent = s.played;
|
|
395
|
+
};
|
|
396
|
+
const videoOf = (e) => typeof HTMLVideoElement !== "undefined" && e.target instanceof HTMLVideoElement ? e.target : null;
|
|
397
|
+
document.addEventListener(
|
|
398
|
+
"play",
|
|
399
|
+
(e) => {
|
|
400
|
+
const v = videoOf(e);
|
|
401
|
+
if (!v) return;
|
|
402
|
+
let s = seen.get(v);
|
|
403
|
+
if (!s) {
|
|
404
|
+
s = { at: v.currentTime, played: 0, sent: 0, reached: -1, counted: null };
|
|
405
|
+
seen.set(v, s);
|
|
406
|
+
}
|
|
407
|
+
s.at = v.currentTime;
|
|
408
|
+
if (s.counted === null) s.counted = counts(v);
|
|
409
|
+
if (!s.counted) return;
|
|
410
|
+
if (playing.indexOf(v) < 0) playing.push(v);
|
|
411
|
+
if (s.reached < 0) {
|
|
412
|
+
s.reached = 0;
|
|
413
|
+
report(v, s, 0);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
true
|
|
417
|
+
);
|
|
418
|
+
document.addEventListener(
|
|
419
|
+
"timeupdate",
|
|
420
|
+
(e) => {
|
|
421
|
+
const v = videoOf(e);
|
|
422
|
+
const s = v ? seen.get(v) : void 0;
|
|
423
|
+
if (!v || !s || !s.counted) return;
|
|
424
|
+
const step = v.currentTime - s.at;
|
|
425
|
+
if (step > 0 && step < 2.5) s.played += step;
|
|
426
|
+
s.at = v.currentTime;
|
|
427
|
+
if (!Number.isFinite(v.duration) || v.duration <= 0) return;
|
|
428
|
+
const quarter = Math.min(100, Math.floor(v.currentTime / v.duration * 4) * 25);
|
|
429
|
+
if (quarter >= 25 && quarter > s.reached) {
|
|
430
|
+
s.reached = quarter;
|
|
431
|
+
report(v, s, quarter);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
true
|
|
435
|
+
);
|
|
436
|
+
document.addEventListener(
|
|
437
|
+
"ended",
|
|
438
|
+
(e) => {
|
|
439
|
+
const v = videoOf(e);
|
|
440
|
+
const s = v ? seen.get(v) : void 0;
|
|
441
|
+
if (!v || !s || !s.counted || s.reached >= 100) return;
|
|
442
|
+
s.reached = 100;
|
|
443
|
+
report(v, s, 100);
|
|
444
|
+
},
|
|
445
|
+
true
|
|
446
|
+
);
|
|
447
|
+
const leaving = () => {
|
|
448
|
+
for (const v of playing) {
|
|
449
|
+
const s = seen.get(v);
|
|
450
|
+
if (s && s.played - s.sent >= 3) report(v, s, Math.max(0, s.reached));
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
document.addEventListener("visibilitychange", () => {
|
|
454
|
+
if (document.visibilityState === "hidden") leaving();
|
|
455
|
+
});
|
|
456
|
+
window.addEventListener("pagehide", leaving);
|
|
457
|
+
}
|
|
333
458
|
function platformCookies() {
|
|
334
459
|
const out = {};
|
|
335
460
|
try {
|
|
@@ -376,6 +501,10 @@ function pageProduct() {
|
|
|
376
501
|
if (typeof o.category === "string") item.category = o.category.slice(0, 120);
|
|
377
502
|
if (brand) item.brand = String(brand).slice(0, 120);
|
|
378
503
|
if (Number.isFinite(price)) item.price = price;
|
|
504
|
+
const image = typeof o.image === "string" ? o.image : Array.isArray(o.image) ? o.image.find((x) => typeof x === "string") : o.image?.url;
|
|
505
|
+
if (typeof image === "string" && /^https?:\/\//.test(image)) item.image = image.slice(0, 500);
|
|
506
|
+
const page2 = typeof o.url === "string" && /^https?:\/\//.test(o.url) ? o.url : window.location.origin + window.location.pathname;
|
|
507
|
+
item.url = page2.slice(0, 500);
|
|
379
508
|
const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
|
|
380
509
|
return { item, currency };
|
|
381
510
|
}
|
|
@@ -411,3 +540,7 @@ var identify = (p) => later((t) => t.identify(p));
|
|
|
411
540
|
var consent = (state) => later((t) => t.consent(state));
|
|
412
541
|
var page = () => later((t) => t.page());
|
|
413
542
|
var flush = () => later((t) => t.flush());
|
|
543
|
+
function suggest(o) {
|
|
544
|
+
if (!hasWindow()) return Promise.resolve({ suggestions: [], recentlyViewed: [] });
|
|
545
|
+
return new Promise((resolve) => later((t) => void t.suggest(o).then(resolve)));
|
|
546
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { COLLECTOR, Tracker, consent, contactChannel, flush, hashPerson, identify, init, page, track } from "./browser";
|
|
2
|
-
export type { Consent, InitOptions, Item, Properties } from "./browser";
|
|
1
|
+
export { API, COLLECTOR, Tracker, consent, contactChannel, flush, hashPerson, identify, init, page, suggest, track } from "./browser";
|
|
2
|
+
export type { Consent, InitOptions, Item, Properties, SuggestOptions, Suggestion } from "./browser";
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ function hashPerson(p) {
|
|
|
30
30
|
|
|
31
31
|
// src/browser.ts
|
|
32
32
|
var COLLECTOR = "https://optiq-collect-951694748196.europe-west2.run.app/e";
|
|
33
|
+
var API = "https://enterpriseapi-951694748196.europe-west2.run.app";
|
|
33
34
|
var DAY = 864e5;
|
|
34
35
|
var IDLE = 30 * 6e4;
|
|
35
36
|
var hasWindow = () => typeof window !== "undefined" && typeof document !== "undefined";
|
|
@@ -66,10 +67,14 @@ var Tracker = class {
|
|
|
66
67
|
constructor(o) {
|
|
67
68
|
this.queue = [];
|
|
68
69
|
this.timer = null;
|
|
70
|
+
/** Events still being hashed (a moment), and whether a flush asked for them to go the moment they are ready. */
|
|
71
|
+
this.hashing = 0;
|
|
72
|
+
this.urgent = false;
|
|
69
73
|
this.lastPath = null;
|
|
70
74
|
this.wired = false;
|
|
71
75
|
this.key = o.key;
|
|
72
76
|
this.endpoint = o.endpoint || COLLECTOR;
|
|
77
|
+
this.api = (o.api || API).replace(/\/+$/, "");
|
|
73
78
|
this.consentState = o.consent || get("consent") || "unknown";
|
|
74
79
|
const q = new URLSearchParams(window.location.search);
|
|
75
80
|
const now = Date.now();
|
|
@@ -145,6 +150,7 @@ var Tracker = class {
|
|
|
145
150
|
flush() {
|
|
146
151
|
if (this.timer) clearTimeout(this.timer);
|
|
147
152
|
this.timer = null;
|
|
153
|
+
if (this.hashing) this.urgent = true;
|
|
148
154
|
while (this.queue.length) {
|
|
149
155
|
const body = JSON.stringify({ key: this.key, visit: this.context(), events: this.queue.splice(0, 50) });
|
|
150
156
|
let sent = false;
|
|
@@ -189,7 +195,9 @@ var Tracker = class {
|
|
|
189
195
|
consent: this.consentState,
|
|
190
196
|
...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
|
|
191
197
|
};
|
|
198
|
+
this.hashing++;
|
|
192
199
|
void personOf(denied ? null : props).then((h) => {
|
|
200
|
+
this.hashing--;
|
|
193
201
|
if (h) {
|
|
194
202
|
this.person = h.hash;
|
|
195
203
|
this.personKind = h.kind;
|
|
@@ -199,9 +207,28 @@ var Tracker = class {
|
|
|
199
207
|
ev.person_hash = denied ? null : this.person;
|
|
200
208
|
ev.person_kind = denied || !this.person ? null : this.personKind;
|
|
201
209
|
this.queue.push(ev);
|
|
202
|
-
if (
|
|
210
|
+
if (this.urgent && !this.hashing) {
|
|
211
|
+
this.urgent = false;
|
|
212
|
+
this.flush();
|
|
213
|
+
} else if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
|
|
203
214
|
});
|
|
204
215
|
}
|
|
216
|
+
/** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
|
|
217
|
+
async suggest(o = {}) {
|
|
218
|
+
const q = new URLSearchParams({ key: this.key });
|
|
219
|
+
const visitor = this.consentState === "denied" ? null : get("vid");
|
|
220
|
+
if (visitor) q.set("visitor", visitor);
|
|
221
|
+
if (o.product) q.set("product", o.product);
|
|
222
|
+
if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
|
|
223
|
+
if (o.limit) q.set("limit", String(o.limit));
|
|
224
|
+
try {
|
|
225
|
+
const r = await fetch(`${this.api}/api/v1/suggest?${q}`, { mode: "cors", credentials: "omit" });
|
|
226
|
+
if (!r.ok) return { suggestions: [], recentlyViewed: [] };
|
|
227
|
+
return await r.json();
|
|
228
|
+
} catch {
|
|
229
|
+
return { suggestions: [], recentlyViewed: [] };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
205
232
|
/** Who the visitor is, from now on: their email or phone, hashed here. */
|
|
206
233
|
identify(p) {
|
|
207
234
|
void personOf(p).then((h) => {
|
|
@@ -289,12 +316,108 @@ var Tracker = class {
|
|
|
289
316
|
},
|
|
290
317
|
true
|
|
291
318
|
);
|
|
319
|
+
watchVideos((p) => this.track("video_progress", p));
|
|
292
320
|
document.addEventListener("visibilitychange", () => {
|
|
293
321
|
if (document.visibilityState === "hidden") this.flush();
|
|
294
322
|
});
|
|
295
323
|
window.addEventListener("pagehide", () => this.flush());
|
|
296
324
|
}
|
|
297
325
|
};
|
|
326
|
+
function watchVideos(send) {
|
|
327
|
+
if (typeof WeakMap !== "function") return;
|
|
328
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
329
|
+
const playing = [];
|
|
330
|
+
const nameOf = (v) => {
|
|
331
|
+
const marked = v.getAttribute("data-optiq-video");
|
|
332
|
+
if (marked && marked !== "on") return marked.slice(0, 120);
|
|
333
|
+
const label = v.getAttribute("title") || v.getAttribute("aria-label");
|
|
334
|
+
if (label) return label.slice(0, 120);
|
|
335
|
+
for (const u of [v.currentSrc, v.getAttribute("src"), v.poster]) {
|
|
336
|
+
if (!u || u.indexOf("blob:") === 0 || u.indexOf("data:") === 0) continue;
|
|
337
|
+
const file = u.split("?")[0].split("#")[0].split("/").filter(Boolean).pop();
|
|
338
|
+
if (file) {
|
|
339
|
+
try {
|
|
340
|
+
return decodeURIComponent(file).slice(0, 120);
|
|
341
|
+
} catch {
|
|
342
|
+
return file.slice(0, 120);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return (document.title || "video").slice(0, 120);
|
|
347
|
+
};
|
|
348
|
+
const counts = (v) => {
|
|
349
|
+
const marked = v.getAttribute("data-optiq-video");
|
|
350
|
+
if (marked === "off") return false;
|
|
351
|
+
if (marked) return true;
|
|
352
|
+
return !v.loop && (v.controls || !v.muted);
|
|
353
|
+
};
|
|
354
|
+
const report = (v, s, percent) => {
|
|
355
|
+
const duration = Number.isFinite(v.duration) && v.duration > 0 ? Math.round(v.duration) : void 0;
|
|
356
|
+
send({ video: nameOf(v), percent, seconds: Math.round(s.played), duration });
|
|
357
|
+
s.sent = s.played;
|
|
358
|
+
};
|
|
359
|
+
const videoOf = (e) => typeof HTMLVideoElement !== "undefined" && e.target instanceof HTMLVideoElement ? e.target : null;
|
|
360
|
+
document.addEventListener(
|
|
361
|
+
"play",
|
|
362
|
+
(e) => {
|
|
363
|
+
const v = videoOf(e);
|
|
364
|
+
if (!v) return;
|
|
365
|
+
let s = seen.get(v);
|
|
366
|
+
if (!s) {
|
|
367
|
+
s = { at: v.currentTime, played: 0, sent: 0, reached: -1, counted: null };
|
|
368
|
+
seen.set(v, s);
|
|
369
|
+
}
|
|
370
|
+
s.at = v.currentTime;
|
|
371
|
+
if (s.counted === null) s.counted = counts(v);
|
|
372
|
+
if (!s.counted) return;
|
|
373
|
+
if (playing.indexOf(v) < 0) playing.push(v);
|
|
374
|
+
if (s.reached < 0) {
|
|
375
|
+
s.reached = 0;
|
|
376
|
+
report(v, s, 0);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
true
|
|
380
|
+
);
|
|
381
|
+
document.addEventListener(
|
|
382
|
+
"timeupdate",
|
|
383
|
+
(e) => {
|
|
384
|
+
const v = videoOf(e);
|
|
385
|
+
const s = v ? seen.get(v) : void 0;
|
|
386
|
+
if (!v || !s || !s.counted) return;
|
|
387
|
+
const step = v.currentTime - s.at;
|
|
388
|
+
if (step > 0 && step < 2.5) s.played += step;
|
|
389
|
+
s.at = v.currentTime;
|
|
390
|
+
if (!Number.isFinite(v.duration) || v.duration <= 0) return;
|
|
391
|
+
const quarter = Math.min(100, Math.floor(v.currentTime / v.duration * 4) * 25);
|
|
392
|
+
if (quarter >= 25 && quarter > s.reached) {
|
|
393
|
+
s.reached = quarter;
|
|
394
|
+
report(v, s, quarter);
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
true
|
|
398
|
+
);
|
|
399
|
+
document.addEventListener(
|
|
400
|
+
"ended",
|
|
401
|
+
(e) => {
|
|
402
|
+
const v = videoOf(e);
|
|
403
|
+
const s = v ? seen.get(v) : void 0;
|
|
404
|
+
if (!v || !s || !s.counted || s.reached >= 100) return;
|
|
405
|
+
s.reached = 100;
|
|
406
|
+
report(v, s, 100);
|
|
407
|
+
},
|
|
408
|
+
true
|
|
409
|
+
);
|
|
410
|
+
const leaving = () => {
|
|
411
|
+
for (const v of playing) {
|
|
412
|
+
const s = seen.get(v);
|
|
413
|
+
if (s && s.played - s.sent >= 3) report(v, s, Math.max(0, s.reached));
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
document.addEventListener("visibilitychange", () => {
|
|
417
|
+
if (document.visibilityState === "hidden") leaving();
|
|
418
|
+
});
|
|
419
|
+
window.addEventListener("pagehide", leaving);
|
|
420
|
+
}
|
|
298
421
|
function platformCookies() {
|
|
299
422
|
const out = {};
|
|
300
423
|
try {
|
|
@@ -341,6 +464,10 @@ function pageProduct() {
|
|
|
341
464
|
if (typeof o.category === "string") item.category = o.category.slice(0, 120);
|
|
342
465
|
if (brand) item.brand = String(brand).slice(0, 120);
|
|
343
466
|
if (Number.isFinite(price)) item.price = price;
|
|
467
|
+
const image = typeof o.image === "string" ? o.image : Array.isArray(o.image) ? o.image.find((x) => typeof x === "string") : o.image?.url;
|
|
468
|
+
if (typeof image === "string" && /^https?:\/\//.test(image)) item.image = image.slice(0, 500);
|
|
469
|
+
const page2 = typeof o.url === "string" && /^https?:\/\//.test(o.url) ? o.url : window.location.origin + window.location.pathname;
|
|
470
|
+
item.url = page2.slice(0, 500);
|
|
344
471
|
const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
|
|
345
472
|
return { item, currency };
|
|
346
473
|
}
|
|
@@ -376,7 +503,12 @@ var identify = (p) => later((t) => t.identify(p));
|
|
|
376
503
|
var consent = (state) => later((t) => t.consent(state));
|
|
377
504
|
var page = () => later((t) => t.page());
|
|
378
505
|
var flush = () => later((t) => t.flush());
|
|
506
|
+
function suggest(o) {
|
|
507
|
+
if (!hasWindow()) return Promise.resolve({ suggestions: [], recentlyViewed: [] });
|
|
508
|
+
return new Promise((resolve) => later((t) => void t.suggest(o).then(resolve)));
|
|
509
|
+
}
|
|
379
510
|
export {
|
|
511
|
+
API,
|
|
380
512
|
COLLECTOR,
|
|
381
513
|
Tracker,
|
|
382
514
|
consent,
|
|
@@ -386,5 +518,6 @@ export {
|
|
|
386
518
|
identify,
|
|
387
519
|
init,
|
|
388
520
|
page,
|
|
521
|
+
suggest,
|
|
389
522
|
track
|
|
390
523
|
};
|
package/dist/server.cjs
CHANGED
|
@@ -70,7 +70,7 @@ var OptiqError = class extends Error {
|
|
|
70
70
|
this.name = "OptiqError";
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
-
var Optiq = class {
|
|
73
|
+
var Optiq = class _Optiq {
|
|
74
74
|
constructor(o) {
|
|
75
75
|
if (!o || typeof o.secretKey !== "string" || !/^sk_[A-Za-z0-9]{30,60}$/.test(o.secretKey)) {
|
|
76
76
|
throw new Error("Optiq needs the source's secret key (sk_...), from the Developer page.");
|
|
@@ -127,6 +127,33 @@ var Optiq = class {
|
|
|
127
127
|
}
|
|
128
128
|
return out;
|
|
129
129
|
}
|
|
130
|
+
async get(path) {
|
|
131
|
+
const r = await fetch(`${this.baseUrl}${path}`, { headers: { Authorization: `Bearer ${this.secretKey}` } });
|
|
132
|
+
const data = await r.json().catch(() => ({}));
|
|
133
|
+
if (!r.ok) throw new OptiqError(typeof data.error === "string" ? data.error : `Optiq answered ${r.status}.`, r.status);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
static who(w) {
|
|
137
|
+
const q = new URLSearchParams();
|
|
138
|
+
if (w.person_hash) q.set("person_hash", w.person_hash);
|
|
139
|
+
else if (w.email) q.set("email", w.email);
|
|
140
|
+
else if (w.phone !== void 0) q.set("phone", String(w.phone));
|
|
141
|
+
if (w.visitor_id) q.set("visitor_id", w.visitor_id);
|
|
142
|
+
return q;
|
|
143
|
+
}
|
|
144
|
+
/** Products to offer one customer (across every device they used), and why: for your pages, emails and messages. */
|
|
145
|
+
async suggest(o) {
|
|
146
|
+
const q = _Optiq.who(o);
|
|
147
|
+
if (o.visitor_id) q.set("visitor", o.visitor_id);
|
|
148
|
+
if (o.product) q.set("product", o.product);
|
|
149
|
+
if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
|
|
150
|
+
if (o.limit) q.set("limit", String(o.limit));
|
|
151
|
+
return this.get(`/api/v1/suggest?${q}`);
|
|
152
|
+
}
|
|
153
|
+
/** Everything your company knows about one customer: visits, sources, products, orders, spend, segment, a timeline. */
|
|
154
|
+
async profile(who) {
|
|
155
|
+
return this.get(`/api/v1/people?${_Optiq.who(who)}`);
|
|
156
|
+
}
|
|
130
157
|
/** Forgets a customer in your company: every event and visit of theirs. Answers how many events went. */
|
|
131
158
|
async forget(who) {
|
|
132
159
|
const hash = who.person_hash || await hashPerson(who);
|
package/dist/server.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface ServerEvent {
|
|
|
23
23
|
variant?: string;
|
|
24
24
|
price?: number;
|
|
25
25
|
quantity?: number;
|
|
26
|
+
url?: string;
|
|
27
|
+
image?: string;
|
|
26
28
|
}[];
|
|
27
29
|
/** The browser's visitor id, if your site passes it to your server (localStorage optiq_vid). */
|
|
28
30
|
visitor_id?: string;
|
|
@@ -70,6 +72,28 @@ export declare class Optiq {
|
|
|
70
72
|
private prepare;
|
|
71
73
|
/** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
|
|
72
74
|
track(events: ServerEvent | ServerEvent[]): Promise<TrackResult>;
|
|
75
|
+
private get;
|
|
76
|
+
private static who;
|
|
77
|
+
/** Products to offer one customer (across every device they used), and why: for your pages, emails and messages. */
|
|
78
|
+
suggest(o: {
|
|
79
|
+
email?: string;
|
|
80
|
+
phone?: string | number;
|
|
81
|
+
person_hash?: string;
|
|
82
|
+
visitor_id?: string;
|
|
83
|
+
product?: string;
|
|
84
|
+
exclude?: string[];
|
|
85
|
+
limit?: number;
|
|
86
|
+
}): Promise<{
|
|
87
|
+
suggestions: Record<string, unknown>[];
|
|
88
|
+
recentlyViewed: Record<string, unknown>[];
|
|
89
|
+
}>;
|
|
90
|
+
/** Everything your company knows about one customer: visits, sources, products, orders, spend, segment, a timeline. */
|
|
91
|
+
profile(who: {
|
|
92
|
+
email?: string;
|
|
93
|
+
phone?: string | number;
|
|
94
|
+
person_hash?: string;
|
|
95
|
+
visitor_id?: string;
|
|
96
|
+
}): Promise<Record<string, unknown>>;
|
|
73
97
|
/** Forgets a customer in your company: every event and visit of theirs. Answers how many events went. */
|
|
74
98
|
forget(who: {
|
|
75
99
|
email?: string;
|
package/dist/server.js
CHANGED
|
@@ -43,7 +43,7 @@ var OptiqError = class extends Error {
|
|
|
43
43
|
this.name = "OptiqError";
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
var Optiq = class {
|
|
46
|
+
var Optiq = class _Optiq {
|
|
47
47
|
constructor(o) {
|
|
48
48
|
if (!o || typeof o.secretKey !== "string" || !/^sk_[A-Za-z0-9]{30,60}$/.test(o.secretKey)) {
|
|
49
49
|
throw new Error("Optiq needs the source's secret key (sk_...), from the Developer page.");
|
|
@@ -100,6 +100,33 @@ var Optiq = class {
|
|
|
100
100
|
}
|
|
101
101
|
return out;
|
|
102
102
|
}
|
|
103
|
+
async get(path) {
|
|
104
|
+
const r = await fetch(`${this.baseUrl}${path}`, { headers: { Authorization: `Bearer ${this.secretKey}` } });
|
|
105
|
+
const data = await r.json().catch(() => ({}));
|
|
106
|
+
if (!r.ok) throw new OptiqError(typeof data.error === "string" ? data.error : `Optiq answered ${r.status}.`, r.status);
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
static who(w) {
|
|
110
|
+
const q = new URLSearchParams();
|
|
111
|
+
if (w.person_hash) q.set("person_hash", w.person_hash);
|
|
112
|
+
else if (w.email) q.set("email", w.email);
|
|
113
|
+
else if (w.phone !== void 0) q.set("phone", String(w.phone));
|
|
114
|
+
if (w.visitor_id) q.set("visitor_id", w.visitor_id);
|
|
115
|
+
return q;
|
|
116
|
+
}
|
|
117
|
+
/** Products to offer one customer (across every device they used), and why: for your pages, emails and messages. */
|
|
118
|
+
async suggest(o) {
|
|
119
|
+
const q = _Optiq.who(o);
|
|
120
|
+
if (o.visitor_id) q.set("visitor", o.visitor_id);
|
|
121
|
+
if (o.product) q.set("product", o.product);
|
|
122
|
+
if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
|
|
123
|
+
if (o.limit) q.set("limit", String(o.limit));
|
|
124
|
+
return this.get(`/api/v1/suggest?${q}`);
|
|
125
|
+
}
|
|
126
|
+
/** Everything your company knows about one customer: visits, sources, products, orders, spend, segment, a timeline. */
|
|
127
|
+
async profile(who) {
|
|
128
|
+
return this.get(`/api/v1/people?${_Optiq.who(who)}`);
|
|
129
|
+
}
|
|
103
130
|
/** Forgets a customer in your company: every event and visit of theirs. Answers how many events went. */
|
|
104
131
|
async forget(who) {
|
|
105
132
|
const hash = who.person_hash || await hashPerson(who);
|
package/dist/t.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/* Optiq Studio Enterprise tracking 0.
|
|
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))})();})();
|
|
1
|
+
/* Optiq Studio Enterprise tracking 0.4.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 D=Object.defineProperty;var x=Object.getOwnPropertySymbols;var K=Object.prototype.hasOwnProperty,N=Object.prototype.propertyIsEnumerable;var q=(i,t,e)=>t in i?D(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e,w=(i,t)=>{for(var e in t||(t={}))K.call(t,e)&&q(i,e,t[e]);if(x)for(var e of x(t))N.call(t,e)&&q(i,e,t[e]);return i};function O(i){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(i)).then(e=>Array.from(new Uint8Array(e),o=>o.toString(16).padStart(2,"0")).join(""),()=>null)}async function S(i){if(i&&typeof i.email=="string"&&i.email.indexOf("@")>0){let t=await O(i.email.trim().toLowerCase());return t?{hash:t,kind:"email"}:null}if(i&&(typeof i.phone=="string"||typeof i.phone=="number")){let t=String(i.phone).replace(/[^0-9]/g,""),e=t.length>=7?await O(t):null;return e?{hash:e,kind:"phone"}:null}return null}var U="https://optiq-collect-951694748196.europe-west2.run.app/e",J="https://enterpriseapi-951694748196.europe-west2.run.app",L=864e5,W=30*6e4,P=()=>typeof window!="undefined"&&typeof document!="undefined";function m(i){try{return window.localStorage.getItem(`optiq_${i}`)}catch(t){return null}}function p(i,t){try{t===null?window.localStorage.removeItem(`optiq_${i}`):window.localStorage.setItem(`optiq_${i}`,t)}catch(e){}}function v(i){try{return JSON.parse(m(i)||"{}")||{}}catch(t){return{}}}function _(i){let t="abcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(i);typeof crypto!="undefined"&&crypto.getRandomValues&&crypto.getRandomValues(e);let o="";for(let a=0;a<i;a++)o+=t.charAt((e[a]||Math.floor(Math.random()*256))%36);return o}var A=class{constructor(t){this.queue=[];this.timer=null;this.hashing=0;this.urgent=!1;this.lastPath=null;this.wired=!1;this.key=t.key,this.endpoint=t.endpoint||U,this.api=(t.api||J).replace(/\/+$/,""),this.consentState=t.consent||m("consent")||"unknown";let e=new URLSearchParams(window.location.search),o=Date.now();this.touch=v("touch");let a=e.get("osr");if(a&&/^[A-Za-z0-9]{4,16}$/.test(a)){let r=new Date(o).toISOString();(!this.touch.first||o-Date.parse(this.touch.first.at)>90*L)&&(this.touch.first={ref:a,at:r}),this.touch.last={ref:a,at:r},p("touch",JSON.stringify(this.touch))}let s={};for(let r of["source","medium","campaign","content","term"]){let c=e.get(`utm_${r}`);c&&(s[r]=c.slice(0,200))}Object.keys(s).length&&p("utm",JSON.stringify(s)),this.utm=Object.keys(s).length?s:v("utm");let u={};for(let r of["gclid","fbclid","ttclid","wbraid","gbraid"]){let c=e.get(r);c&&(u[r]=c.slice(0,500))}Object.keys(u).length&&p("clicks",JSON.stringify(u)),this.clicks=Object.keys(u).length?u:v("clicks");let l=this.consentState==="denied"?{}:v("visit"),n=`${e.get("utm_campaign")||""}|${a||""}`;this.visit=l.id&&o-(l.last||0)<=W&&(n==="|"||n===l.from)?l:{id:_(20),started:new Date(o).toISOString(),from:n,landing:window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:s,click:e.get("gclid")?"gclid":e.get("fbclid")?"fbclid":e.get("ttclid")?"ttclid":null,ref:a&&/^[A-Za-z0-9]{4,16}$/.test(a)?a:null},this.person=m("ph"),this.personKind=m("pk")||null,t.auto!==!1&&this.wire()}visitor(){if(this.consentState==="denied")return null;let t=m("vid");return t||(t=_(20),p("vid",t)),t}context(){let t=null;try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch(o){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.hashing&&(this.urgent=!0);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(o){e=!1}if(!e)try{fetch(this.endpoint,{method:"POST",body:t,keepalive:!0,mode:"cors",headers:{"Content-Type":"text/plain"}}).catch(()=>{})}catch(o){}}}track(t,e={}){let o={};for(let[n,r]of Object.entries(e))!["value","currency","email","phone","items"].includes(n)&&r!==void 0&&!Array.isArray(r)&&(o[n]=r);let a=Date.now();this.visit.last=a,this.consentState!=="denied"&&p("visit",JSON.stringify(this.visit));let s=this.touch.last&&a-Date.parse(this.touch.last.at)<=7*L?this.touch.last:null,u=this.consentState==="denied",l=w({event_id:_(18),name:String(t||"").toLowerCase(),occurred_at:new Date(a).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:u?{}:w(w({},this.clicks),F()),first_ref:this.touch.first?this.touch.first.ref:null,first_ref_at:this.touch.first?this.touch.first.at:null,last_ref:s?s.ref:null,last_ref_at:s?s.at:null,value:e.value===void 0?null:Number(e.value),currency:e.currency||null,properties:o,consent:this.consentState},Array.isArray(e.items)&&e.items.length?{items:e.items.slice(0,50)}:{});this.hashing++,S(u?null:e).then(n=>{this.hashing--,n&&(this.person=n.hash,this.personKind=n.kind,p("ph",n.hash),p("pk",n.kind)),l.person_hash=u?null:this.person,l.person_kind=u||!this.person?null:this.personKind,this.queue.push(l),this.urgent&&!this.hashing?(this.urgent=!1,this.flush()):this.timer||(this.timer=setTimeout(()=>this.flush(),400))})}async suggest(t={}){var a;let e=new URLSearchParams({key:this.key}),o=this.consentState==="denied"?null:m("vid");o&&e.set("visitor",o),t.product&&e.set("product",t.product),(a=t.exclude)!=null&&a.length&&e.set("exclude",t.exclude.join(",")),t.limit&&e.set("limit",String(t.limit));try{let s=await fetch(`${this.api}/api/v1/suggest?${e}`,{mode:"cors",credentials:"omit"});return s.ok?await s.json():{suggestions:[],recentlyViewed:[]}}catch(s){return{suggestions:[],recentlyViewed:[]}}}identify(t){S(t).then(e=>{e&&this.consentState!=="denied"&&(this.person=e.hash,this.personKind=e.kind,p("ph",e.hash),p("pk",e.kind))})}consent(t){if(this.consentState=t==="granted"||t==="denied"?t:"unknown",p("consent",this.consentState),this.consentState==="denied"){for(let e of["vid","ph","pk","clicks","visit"])p(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),o=["q","s","search","query","keyword"].map(s=>e.get(s)).find(s=>s&&s.trim());o&&this.track("search",{query:o.trim().slice(0,200)});let a=B();a&&this.track("view_content",{items:[a.item],value:a.item.price,currency:a.currency})}wire(){if(this.wired)return;this.wired=!0,this.page();for(let e of["pushState","replaceState"]){let o=history[e];typeof o=="function"&&(history[e]=function(...a){let s=o.apply(this,a);return setTimeout(()=>f==null?void 0:f.page(),0),s})}window.addEventListener("popstate",()=>this.page());let t=typeof WeakSet=="function"?new WeakSet:null;document.addEventListener("focusin",e=>{var a,s;let o=(s=(a=e.target)==null?void 0:a.closest)==null?void 0:s.call(a,"form");!o||!t||t.has(o)||(t.add(o),this.track("form_start",{form:(o.getAttribute("name")||o.id||"").slice(0,60)||void 0}))}),document.addEventListener("submit",e=>{let o=e.target;if(!o||!o.getAttribute||o.getAttribute("data-optiq")!=="lead")return;let a=o.querySelector('input[type="email"]'),s=o.querySelector('input[type="tel"]');this.track("lead",{form:(o.getAttribute("name")||o.id||"").slice(0,60)||void 0,email:a?a.value:void 0,phone:s?s.value:void 0}),this.flush()}),document.addEventListener("click",e=>{var s,u;let o=(u=(s=e.target)==null?void 0:s.closest)==null?void 0:u.call(s,"a[href]");if(!o)return;let a=Z(o.getAttribute("href")||"");a&&(this.track("contact",{channel:a}),this.flush())},!0),z(e=>this.track("video_progress",e)),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}};function z(i){if(typeof WeakMap!="function")return;let t=new WeakMap,e=[],o=n=>{let r=n.getAttribute("data-optiq-video");if(r&&r!=="on")return r.slice(0,120);let c=n.getAttribute("title")||n.getAttribute("aria-label");if(c)return c.slice(0,120);for(let d of[n.currentSrc,n.getAttribute("src"),n.poster]){if(!d||d.indexOf("blob:")===0||d.indexOf("data:")===0)continue;let g=d.split("?")[0].split("#")[0].split("/").filter(Boolean).pop();if(g)try{return decodeURIComponent(g).slice(0,120)}catch(k){return g.slice(0,120)}}return(document.title||"video").slice(0,120)},a=n=>{let r=n.getAttribute("data-optiq-video");return r==="off"?!1:r?!0:!n.loop&&(n.controls||!n.muted)},s=(n,r,c)=>{let d=Number.isFinite(n.duration)&&n.duration>0?Math.round(n.duration):void 0;i({video:o(n),percent:c,seconds:Math.round(r.played),duration:d}),r.sent=r.played},u=n=>typeof HTMLVideoElement!="undefined"&&n.target instanceof HTMLVideoElement?n.target:null;document.addEventListener("play",n=>{let r=u(n);if(!r)return;let c=t.get(r);c||(c={at:r.currentTime,played:0,sent:0,reached:-1,counted:null},t.set(r,c)),c.at=r.currentTime,c.counted===null&&(c.counted=a(r)),c.counted&&(e.indexOf(r)<0&&e.push(r),c.reached<0&&(c.reached=0,s(r,c,0)))},!0),document.addEventListener("timeupdate",n=>{let r=u(n),c=r?t.get(r):void 0;if(!r||!c||!c.counted)return;let d=r.currentTime-c.at;if(d>0&&d<2.5&&(c.played+=d),c.at=r.currentTime,!Number.isFinite(r.duration)||r.duration<=0)return;let g=Math.min(100,Math.floor(r.currentTime/r.duration*4)*25);g>=25&&g>c.reached&&(c.reached=g,s(r,c,g))},!0),document.addEventListener("ended",n=>{let r=u(n),c=r?t.get(r):void 0;!r||!c||!c.counted||c.reached>=100||(c.reached=100,s(r,c,100))},!0);let l=()=>{for(let n of e){let r=t.get(n);r&&r.played-r.sent>=3&&s(n,r,Math.max(0,r.reached))}};document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&l()}),window.addEventListener("pagehide",l)}function F(){let i={};try{for(let t of document.cookie.split(";")){let[e,...o]=t.trim().split("="),a=decodeURIComponent(o.join("=")||"").slice(0,300);a&&(e==="_fbp"?i.fbp=a:e==="_fbc"?i.fbc=a:e==="_ttp"&&(i.ttp=a))}}catch(t){}return i}function B(){var t,e,o;let i=document.querySelectorAll('script[type="application/ld+json"]');for(let a of Array.from(i)){let s;try{s=JSON.parse(a.textContent||"null")}catch(l){continue}let u=[s];for(;u.length;){let l=u.shift();if(Array.isArray(l)){u.push(...l);continue}if(!l||typeof l!="object")continue;let n=l;n["@graph"]&&u.push(n["@graph"]);let r=n["@type"];if(r!=="Product"&&!(Array.isArray(r)&&r.includes("Product")))continue;let c=Array.isArray(n.offers)?n.offers[0]:n.offers,d=String(n.sku||n.productID||n.gtin13||n.mpn||n["@id"]||n.url||n.name||"").slice(0,100);if(!d)continue;let g=typeof n.brand=="string"?n.brand:(t=n.brand)==null?void 0:t.name,k=Number((e=c==null?void 0:c.price)!=null?e:c==null?void 0:c.lowPrice),h={id:d};typeof n.name=="string"&&(h.name=n.name.slice(0,200)),typeof n.category=="string"&&(h.category=n.category.slice(0,120)),g&&(h.brand=String(g).slice(0,120)),Number.isFinite(k)&&(h.price=k);let b=typeof n.image=="string"?n.image:Array.isArray(n.image)?n.image.find($=>typeof $=="string"):(o=n.image)==null?void 0:o.url;typeof b=="string"&&/^https?:\/\//.test(b)&&(h.image=b.slice(0,500));let H=typeof n.url=="string"&&/^https?:\/\//.test(n.url)?n.url:window.location.origin+window.location.pathname;h.url=H.slice(0,500);let j=typeof(c==null?void 0:c.priceCurrency)=="string"?c.priceCurrency.toUpperCase().slice(0,3):void 0;return{item:h,currency:j}}}return null}function Z(i){return/^tel:/i.test(i)?"call":/^mailto:[^?]/i.test(i)?"email":/^https?:\/\/(wa\.me\/\+?[0-9]|api\.whatsapp\.com\/send\/?\?(.*&)?phone=)/i.test(i)?"whatsapp":null}var f=null,E=[];function R(i){return P()?f||(!i||typeof i.key!="string"||!/^pk_[A-Za-z0-9]{20,40}$/.test(i.key)?(console.warn("[optiq] init needs the source's publishable key (pk_...), from the Developer page."),null):(f=new A(i),E.splice(0).forEach(t=>t()),f)):null}var y=i=>{P()&&(f?i(f):E.push(()=>f&&i(f)))},T=(i,t)=>y(e=>e.track(i,t)),C=i=>y(t=>t.identify(i)),I=i=>y(t=>t.consent(i)),M=()=>y(i=>i.page());function V(i){return P()?new Promise(t=>y(e=>void e.suggest(i).then(t))):Promise.resolve({suggestions:[],recentlyViewed:[]})}(function(){if(window.__optiqLoaded)return;window.__optiqLoaded=!0;let i=document.currentScript||document.querySelector('script[src*="/t.js"][data-key]');if(!i)return;let t=(window.optiq&&window.optiq.q||[]).map(s=>Array.prototype.slice.call(s));function e(s,u,l){s==="track"?T(String(u),l||{}):s==="identify"?C(u||{}):s==="consent"?I(u):s==="page"&&M()}let o=(...s)=>e(...s);o.track=(s,u)=>T(s,u),o.suggest=s=>V(s),window.optiq=o;let a=t.filter(s=>s[0]==="consent").pop();R({key:i.getAttribute("data-key")||"",endpoint:i.getAttribute("data-endpoint")||void 0,api:i.getAttribute("data-api")||void 0,consent:a&&(a[1]==="granted"||a[1]==="denied")?a[1]:void 0}),t.forEach(s=>e(...s))})();})();
|
package/package.json
CHANGED