@optiqstudio/track 0.3.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
@@ -79,6 +79,9 @@ export declare class Tracker {
79
79
  private personKind;
80
80
  private queue;
81
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;
82
85
  private lastPath;
83
86
  private wired;
84
87
  constructor(o: InitOptions);
package/dist/index.cjs CHANGED
@@ -104,6 +104,9 @@ var Tracker = class {
104
104
  constructor(o) {
105
105
  this.queue = [];
106
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;
107
110
  this.lastPath = null;
108
111
  this.wired = false;
109
112
  this.key = o.key;
@@ -184,6 +187,7 @@ var Tracker = class {
184
187
  flush() {
185
188
  if (this.timer) clearTimeout(this.timer);
186
189
  this.timer = null;
190
+ if (this.hashing) this.urgent = true;
187
191
  while (this.queue.length) {
188
192
  const body = JSON.stringify({ key: this.key, visit: this.context(), events: this.queue.splice(0, 50) });
189
193
  let sent = false;
@@ -228,7 +232,9 @@ var Tracker = class {
228
232
  consent: this.consentState,
229
233
  ...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
230
234
  };
235
+ this.hashing++;
231
236
  void personOf(denied ? null : props).then((h) => {
237
+ this.hashing--;
232
238
  if (h) {
233
239
  this.person = h.hash;
234
240
  this.personKind = h.kind;
@@ -238,7 +244,10 @@ var Tracker = class {
238
244
  ev.person_hash = denied ? null : this.person;
239
245
  ev.person_kind = denied || !this.person ? null : this.personKind;
240
246
  this.queue.push(ev);
241
- 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);
242
251
  });
243
252
  }
244
253
  /** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
@@ -344,12 +353,108 @@ var Tracker = class {
344
353
  },
345
354
  true
346
355
  );
356
+ watchVideos((p) => this.track("video_progress", p));
347
357
  document.addEventListener("visibilitychange", () => {
348
358
  if (document.visibilityState === "hidden") this.flush();
349
359
  });
350
360
  window.addEventListener("pagehide", () => this.flush());
351
361
  }
352
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
+ }
353
458
  function platformCookies() {
354
459
  const out = {};
355
460
  try {
package/dist/index.js CHANGED
@@ -67,6 +67,9 @@ var Tracker = class {
67
67
  constructor(o) {
68
68
  this.queue = [];
69
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;
70
73
  this.lastPath = null;
71
74
  this.wired = false;
72
75
  this.key = o.key;
@@ -147,6 +150,7 @@ var Tracker = class {
147
150
  flush() {
148
151
  if (this.timer) clearTimeout(this.timer);
149
152
  this.timer = null;
153
+ if (this.hashing) this.urgent = true;
150
154
  while (this.queue.length) {
151
155
  const body = JSON.stringify({ key: this.key, visit: this.context(), events: this.queue.splice(0, 50) });
152
156
  let sent = false;
@@ -191,7 +195,9 @@ var Tracker = class {
191
195
  consent: this.consentState,
192
196
  ...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
193
197
  };
198
+ this.hashing++;
194
199
  void personOf(denied ? null : props).then((h) => {
200
+ this.hashing--;
195
201
  if (h) {
196
202
  this.person = h.hash;
197
203
  this.personKind = h.kind;
@@ -201,7 +207,10 @@ var Tracker = class {
201
207
  ev.person_hash = denied ? null : this.person;
202
208
  ev.person_kind = denied || !this.person ? null : this.personKind;
203
209
  this.queue.push(ev);
204
- 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);
205
214
  });
206
215
  }
207
216
  /** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
@@ -307,12 +316,108 @@ var Tracker = class {
307
316
  },
308
317
  true
309
318
  );
319
+ watchVideos((p) => this.track("video_progress", p));
310
320
  document.addEventListener("visibilitychange", () => {
311
321
  if (document.visibilityState === "hidden") this.flush();
312
322
  });
313
323
  window.addEventListener("pagehide", () => this.flush());
314
324
  }
315
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
+ }
316
421
  function platformCookies() {
317
422
  const out = {};
318
423
  try {
package/dist/t.js CHANGED
@@ -1,2 +1,2 @@
1
- /* Optiq Studio Enterprise tracking 0.3.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 N=Object.defineProperty;var x=Object.getOwnPropertySymbols;var H=Object.prototype.hasOwnProperty,U=Object.prototype.propertyIsEnumerable;var O=(n,t,e)=>t in n?N(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,m=(n,t)=>{for(var e in t||(t={}))H.call(t,e)&&O(n,e,t[e]);if(x)for(var e of x(t))U.call(t,e)&&O(n,e,t[e]);return n};function T(n){let t=typeof globalThis!="undefined"?globalThis.crypto:void 0;return!t||!t.subtle||typeof TextEncoder=="undefined"?Promise.resolve(null):t.subtle.digest("SHA-256",new TextEncoder().encode(n)).then(e=>Array.from(new Uint8Array(e),r=>r.toString(16).padStart(2,"0")).join(""),()=>null)}async function k(n){if(n&&typeof n.email=="string"&&n.email.indexOf("@")>0){let t=await T(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 T(t):null;return e?{hash:e,kind:"phone"}:null}return null}var J="https://optiq-collect-951694748196.europe-west2.run.app/e",M="https://enterpriseapi-951694748196.europe-west2.run.app",R=864e5,z=30*6e4,S=()=>typeof window!="undefined"&&typeof document!="undefined";function f(n){try{return window.localStorage.getItem(`optiq_${n}`)}catch(t){return null}}function d(n,t){try{t===null?window.localStorage.removeItem(`optiq_${n}`):window.localStorage.setItem(`optiq_${n}`,t)}catch(e){}}function y(n){try{return JSON.parse(f(n)||"{}")||{}}catch(t){return{}}}function v(n){let t="abcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(n);typeof crypto!="undefined"&&crypto.getRandomValues&&crypto.getRandomValues(e);let r="";for(let s=0;s<n;s++)r+=t.charAt((e[s]||Math.floor(Math.random()*256))%36);return r}var b=class{constructor(t){this.queue=[];this.timer=null;this.lastPath=null;this.wired=!1;this.key=t.key,this.endpoint=t.endpoint||J,this.api=(t.api||M).replace(/\/+$/,""),this.consentState=t.consent||f("consent")||"unknown";let e=new URLSearchParams(window.location.search),r=Date.now();this.touch=y("touch");let s=e.get("osr");if(s&&/^[A-Za-z0-9]{4,16}$/.test(s)){let l=new Date(r).toISOString();(!this.touch.first||r-Date.parse(this.touch.first.at)>90*R)&&(this.touch.first={ref:s,at:l}),this.touch.last={ref:s,at:l},d("touch",JSON.stringify(this.touch))}let i={};for(let l of["source","medium","campaign","content","term"]){let u=e.get(`utm_${l}`);u&&(i[l]=u.slice(0,200))}Object.keys(i).length&&d("utm",JSON.stringify(i)),this.utm=Object.keys(i).length?i:y("utm");let a={};for(let l of["gclid","fbclid","ttclid","wbraid","gbraid"]){let u=e.get(l);u&&(a[l]=u.slice(0,500))}Object.keys(a).length&&d("clicks",JSON.stringify(a)),this.clicks=Object.keys(a).length?a:y("clicks");let c=this.consentState==="denied"?{}:y("visit"),o=`${e.get("utm_campaign")||""}|${s||""}`;this.visit=c.id&&r-(c.last||0)<=z&&(o==="|"||o===c.from)?c:{id:v(20),started:new Date(r).toISOString(),from:o,landing:window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:i,click:e.get("gclid")?"gclid":e.get("fbclid")?"fbclid":e.get("ttclid")?"ttclid":null,ref:s&&/^[A-Za-z0-9]{4,16}$/.test(s)?s:null},this.person=f("ph"),this.personKind=f("pk")||null,t.auto!==!1&&this.wire()}visitor(){if(this.consentState==="denied")return null;let t=f("vid");return t||(t=v(20),d("vid",t)),t}context(){let t=null;try{t=Intl.DateTimeFormat().resolvedOptions().timeZone||null}catch(r){t=null}let e=this.visit;return{id:e.id,started_at:e.started,landing:e.landing,referrer:e.referrer,utm:e.utm,click:e.click,ref:e.ref,lang:(navigator.language||"").slice(0,20)||null,tz:t,screen:window.screen?`${window.screen.width}x${window.screen.height}`:null}}flush(){for(this.timer&&clearTimeout(this.timer),this.timer=null;this.queue.length;){let t=JSON.stringify({key:this.key,visit:this.context(),events:this.queue.splice(0,50)}),e=!1;try{e=!!(navigator.sendBeacon&&navigator.sendBeacon(this.endpoint,new Blob([t],{type:"text/plain"})))}catch(r){e=!1}if(!e)try{fetch(this.endpoint,{method:"POST",body:t,keepalive:!0,mode:"cors",headers:{"Content-Type":"text/plain"}}).catch(()=>{})}catch(r){}}}track(t,e={}){let r={};for(let[o,l]of Object.entries(e))!["value","currency","email","phone","items"].includes(o)&&l!==void 0&&!Array.isArray(l)&&(r[o]=l);let s=Date.now();this.visit.last=s,this.consentState!=="denied"&&d("visit",JSON.stringify(this.visit));let i=this.touch.last&&s-Date.parse(this.touch.last.at)<=7*R?this.touch.last:null,a=this.consentState==="denied",c=m({event_id:v(18),name:String(t||"").toLowerCase(),occurred_at:new Date(s).toISOString(),visitor_id:this.visitor(),page_url:window.location.origin+window.location.pathname,referrer:document.referrer?document.referrer.split("?")[0].slice(0,300):null,utm:this.utm,click_ids:a?{}:m(m({},this.clicks),W()),first_ref:this.touch.first?this.touch.first.ref:null,first_ref_at:this.touch.first?this.touch.first.at:null,last_ref:i?i.ref:null,last_ref_at:i?i.at:null,value:e.value===void 0?null:Number(e.value),currency:e.currency||null,properties:r,consent:this.consentState},Array.isArray(e.items)&&e.items.length?{items:e.items.slice(0,50)}:{});k(a?null:e).then(o=>{o&&(this.person=o.hash,this.personKind=o.kind,d("ph",o.hash),d("pk",o.kind)),c.person_hash=a?null:this.person,c.person_kind=a||!this.person?null:this.personKind,this.queue.push(c),this.timer||(this.timer=setTimeout(()=>this.flush(),400))})}async suggest(t={}){var s;let e=new URLSearchParams({key:this.key}),r=this.consentState==="denied"?null:f("vid");r&&e.set("visitor",r),t.product&&e.set("product",t.product),(s=t.exclude)!=null&&s.length&&e.set("exclude",t.exclude.join(",")),t.limit&&e.set("limit",String(t.limit));try{let i=await fetch(`${this.api}/api/v1/suggest?${e}`,{mode:"cors",credentials:"omit"});return i.ok?await i.json():{suggestions:[],recentlyViewed:[]}}catch(i){return{suggestions:[],recentlyViewed:[]}}}identify(t){k(t).then(e=>{e&&this.consentState!=="denied"&&(this.person=e.hash,this.personKind=e.kind,d("ph",e.hash),d("pk",e.kind))})}consent(t){if(this.consentState=t==="granted"||t==="denied"?t:"unknown",d("consent",this.consentState),this.consentState==="denied"){for(let e of["vid","ph","pk","clicks","visit"])d(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),r=["q","s","search","query","keyword"].map(i=>e.get(i)).find(i=>i&&i.trim());r&&this.track("search",{query:r.trim().slice(0,200)});let s=Z();s&&this.track("view_content",{items:[s.item],value:s.item.price,currency:s.currency})}wire(){if(this.wired)return;this.wired=!0,this.page();for(let e of["pushState","replaceState"]){let r=history[e];typeof r=="function"&&(history[e]=function(...s){let i=r.apply(this,s);return setTimeout(()=>g==null?void 0:g.page(),0),i})}window.addEventListener("popstate",()=>this.page());let t=typeof WeakSet=="function"?new WeakSet:null;document.addEventListener("focusin",e=>{var s,i;let r=(i=(s=e.target)==null?void 0:s.closest)==null?void 0:i.call(s,"form");!r||!t||t.has(r)||(t.add(r),this.track("form_start",{form:(r.getAttribute("name")||r.id||"").slice(0,60)||void 0}))}),document.addEventListener("submit",e=>{let r=e.target;if(!r||!r.getAttribute||r.getAttribute("data-optiq")!=="lead")return;let s=r.querySelector('input[type="email"]'),i=r.querySelector('input[type="tel"]');this.track("lead",{form:(r.getAttribute("name")||r.id||"").slice(0,60)||void 0,email:s?s.value:void 0,phone:i?i.value:void 0}),this.flush()}),document.addEventListener("click",e=>{var i,a;let r=(a=(i=e.target)==null?void 0:i.closest)==null?void 0:a.call(i,"a[href]");if(!r)return;let s=B(r.getAttribute("href")||"");s&&(this.track("contact",{channel:s}),this.flush())},!0),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&this.flush()}),window.addEventListener("pagehide",()=>this.flush())}};function W(){let n={};try{for(let t of document.cookie.split(";")){let[e,...r]=t.trim().split("="),s=decodeURIComponent(r.join("=")||"").slice(0,300);s&&(e==="_fbp"?n.fbp=s:e==="_fbc"?n.fbc=s:e==="_ttp"&&(n.ttp=s))}}catch(t){}return n}function Z(){var t,e,r;let n=document.querySelectorAll('script[type="application/ld+json"]');for(let s of Array.from(n)){let i;try{i=JSON.parse(s.textContent||"null")}catch(c){continue}let a=[i];for(;a.length;){let c=a.shift();if(Array.isArray(c)){a.push(...c);continue}if(!c||typeof c!="object")continue;let o=c;o["@graph"]&&a.push(o["@graph"]);let l=o["@type"];if(l!=="Product"&&!(Array.isArray(l)&&l.includes("Product")))continue;let u=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 A=typeof o.brand=="string"?o.brand:(t=o.brand)==null?void 0:t.name,q=Number((e=u==null?void 0:u.price)!=null?e:u==null?void 0:u.lowPrice),p={id:P};typeof o.name=="string"&&(p.name=o.name.slice(0,200)),typeof o.category=="string"&&(p.category=o.category.slice(0,120)),A&&(p.brand=String(A).slice(0,120)),Number.isFinite(q)&&(p.price=q);let w=typeof o.image=="string"?o.image:Array.isArray(o.image)?o.image.find(V=>typeof V=="string"):(r=o.image)==null?void 0:r.url;typeof w=="string"&&/^https?:\/\//.test(w)&&(p.image=w.slice(0,500));let D=typeof o.url=="string"&&/^https?:\/\//.test(o.url)?o.url:window.location.origin+window.location.pathname;p.url=D.slice(0,500);let K=typeof(u==null?void 0:u.priceCurrency)=="string"?u.priceCurrency.toUpperCase().slice(0,3):void 0;return{item:p,currency:K}}}return null}function B(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 g=null,C=[];function L(n){return S()?g||(!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):(g=new b(n),C.splice(0).forEach(t=>t()),g)):null}var h=n=>{S()&&(g?n(g):C.push(()=>g&&n(g)))},_=(n,t)=>h(e=>e.track(n,t)),I=n=>h(t=>t.identify(n)),E=n=>h(t=>t.consent(n)),j=()=>h(n=>n.page());function $(n){return S()?new Promise(t=>h(e=>void e.suggest(n).then(t))):Promise.resolve({suggestions:[],recentlyViewed:[]})}(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,c){i==="track"?_(String(a),c||{}):i==="identify"?I(a||{}):i==="consent"?E(a):i==="page"&&j()}let r=(...i)=>e(...i);r.track=(i,a)=>_(i,a),r.suggest=i=>$(i),window.optiq=r;let s=t.filter(i=>i[0]==="consent").pop();L({key:n.getAttribute("data-key")||"",endpoint:n.getAttribute("data-endpoint")||void 0,api:n.getAttribute("data-api")||void 0,consent:s&&(s[1]==="granted"||s[1]==="denied")?s[1]:void 0}),t.forEach(i=>e(...i))})();})();
1
+ /* Optiq Studio Enterprise tracking 0.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.3.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",