@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 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 (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
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 (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
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.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))})();})();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optiqstudio/track",
3
- "version": "0.2.0",
3
+ "version": "0.4.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
5
  "keywords": [
6
6
  "optiq",