@optiqstudio/track 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.d.ts 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;
@@ -57,6 +88,11 @@ export declare class Tracker {
57
88
  flush(): void;
58
89
  /** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
59
90
  track(name: string, props?: Properties): void;
91
+ /** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
92
+ suggest(o?: SuggestOptions): Promise<{
93
+ suggestions: Suggestion[];
94
+ recentlyViewed: Suggestion[];
95
+ }>;
60
96
  /** Who the visitor is, from now on: their email or phone, hashed here. */
61
97
  identify(p: {
62
98
  email?: string;
@@ -90,3 +126,8 @@ export declare const consent: (state: Consent) => void;
90
126
  export declare const page: () => void;
91
127
  /** Sends what waits now. */
92
128
  export declare const flush: () => void;
129
+ /** Products to show this shopper, and why (an empty list if anything goes wrong, so a page never breaks). */
130
+ export declare function suggest(o?: SuggestOptions): Promise<{
131
+ suggestions: Suggestion[];
132
+ recentlyViewed: Suggestion[];
133
+ }>;
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";
@@ -105,6 +108,7 @@ var Tracker = class {
105
108
  this.wired = false;
106
109
  this.key = o.key;
107
110
  this.endpoint = o.endpoint || COLLECTOR;
111
+ this.api = (o.api || API).replace(/\/+$/, "");
108
112
  this.consentState = o.consent || get("consent") || "unknown";
109
113
  const q = new URLSearchParams(window.location.search);
110
114
  const now = Date.now();
@@ -237,6 +241,22 @@ var Tracker = class {
237
241
  if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
238
242
  });
239
243
  }
244
+ /** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
245
+ async suggest(o = {}) {
246
+ const q = new URLSearchParams({ key: this.key });
247
+ const visitor = this.consentState === "denied" ? null : get("vid");
248
+ if (visitor) q.set("visitor", visitor);
249
+ if (o.product) q.set("product", o.product);
250
+ if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
251
+ if (o.limit) q.set("limit", String(o.limit));
252
+ try {
253
+ const r = await fetch(`${this.api}/api/v1/suggest?${q}`, { mode: "cors", credentials: "omit" });
254
+ if (!r.ok) return { suggestions: [], recentlyViewed: [] };
255
+ return await r.json();
256
+ } catch {
257
+ return { suggestions: [], recentlyViewed: [] };
258
+ }
259
+ }
240
260
  /** Who the visitor is, from now on: their email or phone, hashed here. */
241
261
  identify(p) {
242
262
  void personOf(p).then((h) => {
@@ -376,6 +396,10 @@ function pageProduct() {
376
396
  if (typeof o.category === "string") item.category = o.category.slice(0, 120);
377
397
  if (brand) item.brand = String(brand).slice(0, 120);
378
398
  if (Number.isFinite(price)) item.price = price;
399
+ const image = typeof o.image === "string" ? o.image : Array.isArray(o.image) ? o.image.find((x) => typeof x === "string") : o.image?.url;
400
+ if (typeof image === "string" && /^https?:\/\//.test(image)) item.image = image.slice(0, 500);
401
+ const page2 = typeof o.url === "string" && /^https?:\/\//.test(o.url) ? o.url : window.location.origin + window.location.pathname;
402
+ item.url = page2.slice(0, 500);
379
403
  const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
380
404
  return { item, currency };
381
405
  }
@@ -411,3 +435,7 @@ var identify = (p) => later((t) => t.identify(p));
411
435
  var consent = (state) => later((t) => t.consent(state));
412
436
  var page = () => later((t) => t.page());
413
437
  var flush = () => later((t) => t.flush());
438
+ function suggest(o) {
439
+ if (!hasWindow()) return Promise.resolve({ suggestions: [], recentlyViewed: [] });
440
+ return new Promise((resolve) => later((t) => void t.suggest(o).then(resolve)));
441
+ }
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";
@@ -70,6 +71,7 @@ var Tracker = class {
70
71
  this.wired = false;
71
72
  this.key = o.key;
72
73
  this.endpoint = o.endpoint || COLLECTOR;
74
+ this.api = (o.api || API).replace(/\/+$/, "");
73
75
  this.consentState = o.consent || get("consent") || "unknown";
74
76
  const q = new URLSearchParams(window.location.search);
75
77
  const now = Date.now();
@@ -202,6 +204,22 @@ var Tracker = class {
202
204
  if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
203
205
  });
204
206
  }
207
+ /** Products to show this shopper, from what they (and shoppers like them) looked at and bought, and why. */
208
+ async suggest(o = {}) {
209
+ const q = new URLSearchParams({ key: this.key });
210
+ const visitor = this.consentState === "denied" ? null : get("vid");
211
+ if (visitor) q.set("visitor", visitor);
212
+ if (o.product) q.set("product", o.product);
213
+ if (o.exclude?.length) q.set("exclude", o.exclude.join(","));
214
+ if (o.limit) q.set("limit", String(o.limit));
215
+ try {
216
+ const r = await fetch(`${this.api}/api/v1/suggest?${q}`, { mode: "cors", credentials: "omit" });
217
+ if (!r.ok) return { suggestions: [], recentlyViewed: [] };
218
+ return await r.json();
219
+ } catch {
220
+ return { suggestions: [], recentlyViewed: [] };
221
+ }
222
+ }
205
223
  /** Who the visitor is, from now on: their email or phone, hashed here. */
206
224
  identify(p) {
207
225
  void personOf(p).then((h) => {
@@ -341,6 +359,10 @@ function pageProduct() {
341
359
  if (typeof o.category === "string") item.category = o.category.slice(0, 120);
342
360
  if (brand) item.brand = String(brand).slice(0, 120);
343
361
  if (Number.isFinite(price)) item.price = price;
362
+ const image = typeof o.image === "string" ? o.image : Array.isArray(o.image) ? o.image.find((x) => typeof x === "string") : o.image?.url;
363
+ if (typeof image === "string" && /^https?:\/\//.test(image)) item.image = image.slice(0, 500);
364
+ const page2 = typeof o.url === "string" && /^https?:\/\//.test(o.url) ? o.url : window.location.origin + window.location.pathname;
365
+ item.url = page2.slice(0, 500);
344
366
  const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
345
367
  return { item, currency };
346
368
  }
@@ -376,7 +398,12 @@ var identify = (p) => later((t) => t.identify(p));
376
398
  var consent = (state) => later((t) => t.consent(state));
377
399
  var page = () => later((t) => t.page());
378
400
  var flush = () => later((t) => t.flush());
401
+ function suggest(o) {
402
+ if (!hasWindow()) return Promise.resolve({ suggestions: [], recentlyViewed: [] });
403
+ return new Promise((resolve) => later((t) => void t.suggest(o).then(resolve)));
404
+ }
379
405
  export {
406
+ API,
380
407
  COLLECTOR,
381
408
  Tracker,
382
409
  consent,
@@ -386,5 +413,6 @@ export {
386
413
  identify,
387
414
  init,
388
415
  page,
416
+ suggest,
389
417
  track
390
418
  };
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.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))})();})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optiqstudio/track",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",