@optiqstudio/track 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -2
- package/dist/browser.d.ts +92 -0
- package/dist/hash.d.ts +17 -0
- package/dist/index.cjs +413 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +390 -0
- package/dist/server.cjs +136 -0
- package/dist/server.d.ts +82 -0
- package/dist/server.js +113 -0
- package/dist/t.js +2 -0
- package/package.json +51 -4
package/dist/server.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/hash.ts
|
|
2
|
+
function sha256(s) {
|
|
3
|
+
const c = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
|
|
4
|
+
if (!c || !c.subtle || typeof TextEncoder === "undefined") return Promise.resolve(null);
|
|
5
|
+
return c.subtle.digest("SHA-256", new TextEncoder().encode(s)).then(
|
|
6
|
+
(buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join(""),
|
|
7
|
+
() => null
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
async function personOf(p) {
|
|
11
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
|
|
12
|
+
const hash = await sha256(p.email.trim().toLowerCase());
|
|
13
|
+
return hash ? { hash, kind: "email" } : null;
|
|
14
|
+
}
|
|
15
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
16
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
17
|
+
const hash = digits.length >= 7 ? await sha256(digits) : null;
|
|
18
|
+
return hash ? { hash, kind: "phone" } : null;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function hashPerson(p) {
|
|
23
|
+
if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
|
|
24
|
+
if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
|
|
25
|
+
const digits = String(p.phone).replace(/[^0-9]/g, "");
|
|
26
|
+
if (digits.length >= 7) return sha256(digits);
|
|
27
|
+
}
|
|
28
|
+
return Promise.resolve(null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/server.ts
|
|
32
|
+
var BATCH = 500;
|
|
33
|
+
function newId() {
|
|
34
|
+
const abc = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
35
|
+
const a = new Uint8Array(20);
|
|
36
|
+
crypto.getRandomValues(a);
|
|
37
|
+
return Array.from(a, (b) => abc[b % 36]).join("");
|
|
38
|
+
}
|
|
39
|
+
var OptiqError = class extends Error {
|
|
40
|
+
constructor(message, status) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.name = "OptiqError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var Optiq = class {
|
|
47
|
+
constructor(o) {
|
|
48
|
+
if (!o || typeof o.secretKey !== "string" || !/^sk_[A-Za-z0-9]{30,60}$/.test(o.secretKey)) {
|
|
49
|
+
throw new Error("Optiq needs the source's secret key (sk_...), from the Developer page.");
|
|
50
|
+
}
|
|
51
|
+
this.secretKey = o.secretKey;
|
|
52
|
+
this.baseUrl = (o.baseUrl || "https://enterprise.optiq.studio").replace(/\/+$/, "");
|
|
53
|
+
this.retries = Math.max(1, o.retries ?? 3);
|
|
54
|
+
}
|
|
55
|
+
async call(method, path, body) {
|
|
56
|
+
let last = null;
|
|
57
|
+
for (let attempt = 0; attempt < this.retries; attempt++) {
|
|
58
|
+
if (attempt) await new Promise((r) => setTimeout(r, 500 * 2 ** (attempt - 1)));
|
|
59
|
+
try {
|
|
60
|
+
const r = await fetch(`${this.baseUrl}${path}`, {
|
|
61
|
+
method,
|
|
62
|
+
headers: { Authorization: `Bearer ${this.secretKey}`, "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body)
|
|
64
|
+
});
|
|
65
|
+
const data = await r.json().catch(() => ({}));
|
|
66
|
+
if (r.ok) return data;
|
|
67
|
+
const message = typeof data.error === "string" ? data.error : `Optiq answered ${r.status}.`;
|
|
68
|
+
if (r.status !== 429 && r.status < 500) throw new OptiqError(message, r.status);
|
|
69
|
+
last = new OptiqError(message, r.status);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (e instanceof OptiqError && e.status !== 429 && e.status < 500) throw e;
|
|
72
|
+
last = e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw last instanceof Error ? last : new Error("Optiq did not answer.");
|
|
76
|
+
}
|
|
77
|
+
async prepare(e) {
|
|
78
|
+
const { email, phone, ...rest } = e;
|
|
79
|
+
const who = e.person_hash ? { hash: e.person_hash, kind: e.person_kind || "email" } : await personOf({ email, phone });
|
|
80
|
+
const iso = (v) => v instanceof Date ? v.toISOString() : v;
|
|
81
|
+
return {
|
|
82
|
+
...rest,
|
|
83
|
+
event_id: e.event_id || newId(),
|
|
84
|
+
occurred_at: iso(e.occurred_at),
|
|
85
|
+
last_ref_at: iso(e.last_ref_at),
|
|
86
|
+
person_hash: who?.hash || void 0,
|
|
87
|
+
person_kind: who?.kind || void 0
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
|
|
91
|
+
async track(events) {
|
|
92
|
+
const list = Array.isArray(events) ? events : [events];
|
|
93
|
+
const out = { accepted: 0, duplicates: 0, rejected: [] };
|
|
94
|
+
for (let i = 0; i < list.length; i += BATCH) {
|
|
95
|
+
const part = await Promise.all(list.slice(i, i + BATCH).map((e) => this.prepare(e)));
|
|
96
|
+
const r = await this.call("POST", "/api/v1/events", { events: part });
|
|
97
|
+
out.accepted += r.accepted || 0;
|
|
98
|
+
out.duplicates += r.duplicates || 0;
|
|
99
|
+
for (const x of r.rejected || []) out.rejected.push({ index: x.index + i, reason: x.reason });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Forgets a customer in your company: every event and visit of theirs. Answers how many events went. */
|
|
104
|
+
async forget(who) {
|
|
105
|
+
const hash = who.person_hash || await hashPerson(who);
|
|
106
|
+
if (!hash && !who.visitor_id) throw new Error("Say who: an email, a phone, a person_hash or a visitor_id.");
|
|
107
|
+
return await this.call("DELETE", "/api/v1/people", { person_hash: hash || void 0, visitor_id: who.visitor_id });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
export {
|
|
111
|
+
Optiq,
|
|
112
|
+
OptiqError
|
|
113
|
+
};
|
package/dist/t.js
ADDED
|
@@ -0,0 +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))})();})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optiqstudio/track",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Optiq Studio Enterprise tracking: join every lead, sale and deposit to the video, post or advert that brought it. Browser and server.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"optiq",
|
|
7
|
+
"optiq studio",
|
|
8
|
+
"attribution",
|
|
9
|
+
"tracking",
|
|
10
|
+
"conversions",
|
|
11
|
+
"analytics",
|
|
12
|
+
"marketing",
|
|
13
|
+
"whatsapp"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://enterprise.optiq.studio/developers",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "DaveLabs <info@davelabs.co> (https://davelabs.co)",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./server": {
|
|
30
|
+
"types": "./dist/server.d.ts",
|
|
31
|
+
"import": "./dist/server.js",
|
|
32
|
+
"require": "./dist/server.cjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node build.mjs",
|
|
46
|
+
"test": "node --test test/*.test.mjs",
|
|
47
|
+
"prepublishOnly": "npm run build && npm test"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"esbuild": "0.25.12",
|
|
51
|
+
"typescript": "5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|