@optiqstudio/track 0.1.0 → 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/dist/browser.d.ts CHANGED
@@ -11,6 +11,16 @@ export interface InitOptions {
11
11
  /** The visitor's choice, if the site already knows it. */
12
12
  consent?: Consent;
13
13
  }
14
+ /** One thing in a basket or an order. */
15
+ export interface Item {
16
+ id: string;
17
+ name?: string;
18
+ category?: string;
19
+ brand?: string;
20
+ variant?: string;
21
+ price?: number;
22
+ quantity?: number;
23
+ }
14
24
  export interface Properties {
15
25
  /** A sale's or deposit's amount. */
16
26
  value?: number;
@@ -20,7 +30,9 @@ export interface Properties {
20
30
  email?: string;
21
31
  /** Hashed in the browser before anything is sent (digits only). */
22
32
  phone?: string | number;
23
- [property: string]: string | number | boolean | undefined;
33
+ /** What was viewed, put in the basket or bought (view_content, add_to_cart, begin_checkout, purchase). */
34
+ items?: Item[];
35
+ [property: string]: string | number | boolean | Item[] | undefined;
24
36
  }
25
37
  export declare const COLLECTOR = "https://optiq-collect-951694748196.europe-west2.run.app/e";
26
38
  /** One tracker on one page. Most sites use the module functions below, which share one. */
@@ -33,6 +45,7 @@ export declare class Tracker {
33
45
  private clicks;
34
46
  private visit;
35
47
  private person;
48
+ private personKind;
36
49
  private queue;
37
50
  private timer;
38
51
  private lastPath;
@@ -53,6 +66,11 @@ export declare class Tracker {
53
66
  consent(state: Consent): void;
54
67
  /** A page view (called by itself in auto mode, on load and on every move of a single-page site). */
55
68
  page(): void;
69
+ /**
70
+ * What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
71
+ * its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
72
+ */
73
+ private shopSignals;
56
74
  private wire;
57
75
  }
58
76
  /** Which kind of contact a link begins, or null: tel: is a call, mailto: with an address an email, wa.me with a number WhatsApp. */
package/dist/hash.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  /** SHA-256 in hex, by Web Crypto (browsers, Node 20+, Deno, Bun, edge runtimes); null where it is missing. */
2
2
  export declare function sha256(s: string): Promise<string | null>;
3
+ /** Which a person hash is of: the ad platforms take emails and phones apart. */
4
+ export type PersonKind = "email" | "phone";
5
+ /** The hash and what it is of, or null. */
6
+ export declare function personOf(p: {
7
+ email?: unknown;
8
+ phone?: unknown;
9
+ } | null | undefined): Promise<{
10
+ hash: string;
11
+ kind: PersonKind;
12
+ } | null>;
3
13
  /** An email (trimmed, lower-case) or a phone (digits only), as a SHA-256: the only way a person is ever sent. */
4
14
  export declare function hashPerson(p: {
5
15
  email?: unknown;
package/dist/index.cjs CHANGED
@@ -42,6 +42,18 @@ function sha256(s) {
42
42
  () => null
43
43
  );
44
44
  }
45
+ async function personOf(p) {
46
+ if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
47
+ const hash = await sha256(p.email.trim().toLowerCase());
48
+ return hash ? { hash, kind: "email" } : null;
49
+ }
50
+ if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
51
+ const digits = String(p.phone).replace(/[^0-9]/g, "");
52
+ const hash = digits.length >= 7 ? await sha256(digits) : null;
53
+ return hash ? { hash, kind: "phone" } : null;
54
+ }
55
+ return null;
56
+ }
45
57
  function hashPerson(p) {
46
58
  if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
47
59
  if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
@@ -131,6 +143,7 @@ var Tracker = class {
131
143
  ref: osr && /^[A-Za-z0-9]{4,16}$/.test(osr) ? osr : null
132
144
  };
133
145
  this.person = get("ph");
146
+ this.personKind = get("pk") || null;
134
147
  if (o.auto !== false) this.wire();
135
148
  }
136
149
  visitor() {
@@ -186,7 +199,7 @@ var Tracker = class {
186
199
  /** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
187
200
  track(name, props = {}) {
188
201
  const rest = {};
189
- for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone"].includes(k) && v !== void 0) rest[k] = v;
202
+ for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone", "items"].includes(k) && v !== void 0 && !Array.isArray(v)) rest[k] = v;
190
203
  const fresh = Date.now();
191
204
  this.visit.last = fresh;
192
205
  if (this.consentState !== "denied") put("visit", JSON.stringify(this.visit));
@@ -200,7 +213,7 @@ var Tracker = class {
200
213
  page_url: window.location.origin + window.location.pathname,
201
214
  referrer: document.referrer ? document.referrer.split("?")[0].slice(0, 300) : null,
202
215
  utm: this.utm,
203
- click_ids: denied ? {} : this.clicks,
216
+ click_ids: denied ? {} : { ...this.clicks, ...platformCookies() },
204
217
  first_ref: this.touch.first ? this.touch.first.ref : null,
205
218
  first_ref_at: this.touch.first ? this.touch.first.at : null,
206
219
  last_ref: last ? last.ref : null,
@@ -208,24 +221,30 @@ var Tracker = class {
208
221
  value: props.value === void 0 ? null : Number(props.value),
209
222
  currency: props.currency || null,
210
223
  properties: rest,
211
- consent: this.consentState
224
+ consent: this.consentState,
225
+ ...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
212
226
  };
213
- void hashPerson(denied ? null : props).then((h) => {
227
+ void personOf(denied ? null : props).then((h) => {
214
228
  if (h) {
215
- this.person = h;
216
- put("ph", h);
229
+ this.person = h.hash;
230
+ this.personKind = h.kind;
231
+ put("ph", h.hash);
232
+ put("pk", h.kind);
217
233
  }
218
234
  ev.person_hash = denied ? null : this.person;
235
+ ev.person_kind = denied || !this.person ? null : this.personKind;
219
236
  this.queue.push(ev);
220
237
  if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
221
238
  });
222
239
  }
223
240
  /** Who the visitor is, from now on: their email or phone, hashed here. */
224
241
  identify(p) {
225
- void hashPerson(p).then((h) => {
242
+ void personOf(p).then((h) => {
226
243
  if (h && this.consentState !== "denied") {
227
- this.person = h;
228
- put("ph", h);
244
+ this.person = h.hash;
245
+ this.personKind = h.kind;
246
+ put("ph", h.hash);
247
+ put("pk", h.kind);
229
248
  }
230
249
  });
231
250
  }
@@ -234,8 +253,9 @@ var Tracker = class {
234
253
  this.consentState = state === "granted" || state === "denied" ? state : "unknown";
235
254
  put("consent", this.consentState);
236
255
  if (this.consentState === "denied") {
237
- for (const k of ["vid", "ph", "clicks", "visit"]) put(k, null);
256
+ for (const k of ["vid", "ph", "pk", "clicks", "visit"]) put(k, null);
238
257
  this.person = null;
258
+ this.personKind = null;
239
259
  this.clicks = {};
240
260
  }
241
261
  }
@@ -245,6 +265,19 @@ var Tracker = class {
245
265
  if (path === this.lastPath) return;
246
266
  this.lastPath = path;
247
267
  this.track("page_view");
268
+ if (this.wired) setTimeout(() => this.shopSignals(path), 1200);
269
+ }
270
+ /**
271
+ * What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
272
+ * its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
273
+ */
274
+ shopSignals(path) {
275
+ if (path !== window.location.pathname + window.location.search) return;
276
+ const q = new URLSearchParams(window.location.search);
277
+ const query = ["q", "s", "search", "query", "keyword"].map((k) => q.get(k)).find((v) => v && v.trim());
278
+ if (query) this.track("search", { query: query.trim().slice(0, 200) });
279
+ const product = pageProduct();
280
+ if (product) this.track("view_content", { items: [product.item], value: product.item.price, currency: product.currency });
248
281
  }
249
282
  wire() {
250
283
  if (this.wired) return;
@@ -297,6 +330,58 @@ var Tracker = class {
297
330
  window.addEventListener("pagehide", () => this.flush());
298
331
  }
299
332
  };
333
+ function platformCookies() {
334
+ const out = {};
335
+ try {
336
+ for (const part of document.cookie.split(";")) {
337
+ const [k, ...v] = part.trim().split("=");
338
+ const value = decodeURIComponent(v.join("=") || "").slice(0, 300);
339
+ if (!value) continue;
340
+ if (k === "_fbp") out.fbp = value;
341
+ else if (k === "_fbc") out.fbc = value;
342
+ else if (k === "_ttp") out.ttp = value;
343
+ }
344
+ } catch {
345
+ }
346
+ return out;
347
+ }
348
+ function pageProduct() {
349
+ const scripts = document.querySelectorAll('script[type="application/ld+json"]');
350
+ for (const el of Array.from(scripts)) {
351
+ let data;
352
+ try {
353
+ data = JSON.parse(el.textContent || "null");
354
+ } catch {
355
+ continue;
356
+ }
357
+ const queue = [data];
358
+ while (queue.length) {
359
+ const n = queue.shift();
360
+ if (Array.isArray(n)) {
361
+ queue.push(...n);
362
+ continue;
363
+ }
364
+ if (!n || typeof n !== "object") continue;
365
+ const o = n;
366
+ if (o["@graph"]) queue.push(o["@graph"]);
367
+ const type = o["@type"];
368
+ if (type !== "Product" && !(Array.isArray(type) && type.includes("Product"))) continue;
369
+ const offers = Array.isArray(o.offers) ? o.offers[0] : o.offers;
370
+ const id = String(o.sku || o.productID || o.gtin13 || o.mpn || o["@id"] || o.url || o.name || "").slice(0, 100);
371
+ if (!id) continue;
372
+ const brand = typeof o.brand === "string" ? o.brand : o.brand?.name;
373
+ const price = Number(offers?.price ?? offers?.lowPrice);
374
+ const item = { id };
375
+ if (typeof o.name === "string") item.name = o.name.slice(0, 200);
376
+ if (typeof o.category === "string") item.category = o.category.slice(0, 120);
377
+ if (brand) item.brand = String(brand).slice(0, 120);
378
+ if (Number.isFinite(price)) item.price = price;
379
+ const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
380
+ return { item, currency };
381
+ }
382
+ }
383
+ return null;
384
+ }
300
385
  function contactChannel(href) {
301
386
  if (/^tel:/i.test(href)) return "call";
302
387
  if (/^mailto:[^?]/i.test(href)) return "email";
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { COLLECTOR, Tracker, consent, contactChannel, flush, hashPerson, identify, init, page, track } from "./browser";
2
- export type { Consent, InitOptions, Properties } from "./browser";
2
+ export type { Consent, InitOptions, Item, Properties } from "./browser";
package/dist/index.js CHANGED
@@ -7,6 +7,18 @@ function sha256(s) {
7
7
  () => null
8
8
  );
9
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
+ }
10
22
  function hashPerson(p) {
11
23
  if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
12
24
  if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
@@ -96,6 +108,7 @@ var Tracker = class {
96
108
  ref: osr && /^[A-Za-z0-9]{4,16}$/.test(osr) ? osr : null
97
109
  };
98
110
  this.person = get("ph");
111
+ this.personKind = get("pk") || null;
99
112
  if (o.auto !== false) this.wire();
100
113
  }
101
114
  visitor() {
@@ -151,7 +164,7 @@ var Tracker = class {
151
164
  /** Something a visitor did: a standard name (lead, sign_up, purchase, first_deposit, ...) or one of your own. */
152
165
  track(name, props = {}) {
153
166
  const rest = {};
154
- for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone"].includes(k) && v !== void 0) rest[k] = v;
167
+ for (const [k, v] of Object.entries(props)) if (!["value", "currency", "email", "phone", "items"].includes(k) && v !== void 0 && !Array.isArray(v)) rest[k] = v;
155
168
  const fresh = Date.now();
156
169
  this.visit.last = fresh;
157
170
  if (this.consentState !== "denied") put("visit", JSON.stringify(this.visit));
@@ -165,7 +178,7 @@ var Tracker = class {
165
178
  page_url: window.location.origin + window.location.pathname,
166
179
  referrer: document.referrer ? document.referrer.split("?")[0].slice(0, 300) : null,
167
180
  utm: this.utm,
168
- click_ids: denied ? {} : this.clicks,
181
+ click_ids: denied ? {} : { ...this.clicks, ...platformCookies() },
169
182
  first_ref: this.touch.first ? this.touch.first.ref : null,
170
183
  first_ref_at: this.touch.first ? this.touch.first.at : null,
171
184
  last_ref: last ? last.ref : null,
@@ -173,24 +186,30 @@ var Tracker = class {
173
186
  value: props.value === void 0 ? null : Number(props.value),
174
187
  currency: props.currency || null,
175
188
  properties: rest,
176
- consent: this.consentState
189
+ consent: this.consentState,
190
+ ...Array.isArray(props.items) && props.items.length ? { items: props.items.slice(0, 50) } : {}
177
191
  };
178
- void hashPerson(denied ? null : props).then((h) => {
192
+ void personOf(denied ? null : props).then((h) => {
179
193
  if (h) {
180
- this.person = h;
181
- put("ph", h);
194
+ this.person = h.hash;
195
+ this.personKind = h.kind;
196
+ put("ph", h.hash);
197
+ put("pk", h.kind);
182
198
  }
183
199
  ev.person_hash = denied ? null : this.person;
200
+ ev.person_kind = denied || !this.person ? null : this.personKind;
184
201
  this.queue.push(ev);
185
202
  if (!this.timer) this.timer = setTimeout(() => this.flush(), 400);
186
203
  });
187
204
  }
188
205
  /** Who the visitor is, from now on: their email or phone, hashed here. */
189
206
  identify(p) {
190
- void hashPerson(p).then((h) => {
207
+ void personOf(p).then((h) => {
191
208
  if (h && this.consentState !== "denied") {
192
- this.person = h;
193
- put("ph", h);
209
+ this.person = h.hash;
210
+ this.personKind = h.kind;
211
+ put("ph", h.hash);
212
+ put("pk", h.kind);
194
213
  }
195
214
  });
196
215
  }
@@ -199,8 +218,9 @@ var Tracker = class {
199
218
  this.consentState = state === "granted" || state === "denied" ? state : "unknown";
200
219
  put("consent", this.consentState);
201
220
  if (this.consentState === "denied") {
202
- for (const k of ["vid", "ph", "clicks", "visit"]) put(k, null);
221
+ for (const k of ["vid", "ph", "pk", "clicks", "visit"]) put(k, null);
203
222
  this.person = null;
223
+ this.personKind = null;
204
224
  this.clicks = {};
205
225
  }
206
226
  }
@@ -210,6 +230,19 @@ var Tracker = class {
210
230
  if (path === this.lastPath) return;
211
231
  this.lastPath = path;
212
232
  this.track("page_view");
233
+ if (this.wired) setTimeout(() => this.shopSignals(path), 1200);
234
+ }
235
+ /**
236
+ * What a shop's page says by itself: a product looked at (the schema.org Product the page publishes for Google, with
237
+ * its id, name, category, brand and price) and a search (?q=, ?s=, ?search=, ?query=, ?keyword=). Once per page.
238
+ */
239
+ shopSignals(path) {
240
+ if (path !== window.location.pathname + window.location.search) return;
241
+ const q = new URLSearchParams(window.location.search);
242
+ const query = ["q", "s", "search", "query", "keyword"].map((k) => q.get(k)).find((v) => v && v.trim());
243
+ if (query) this.track("search", { query: query.trim().slice(0, 200) });
244
+ const product = pageProduct();
245
+ if (product) this.track("view_content", { items: [product.item], value: product.item.price, currency: product.currency });
213
246
  }
214
247
  wire() {
215
248
  if (this.wired) return;
@@ -262,6 +295,58 @@ var Tracker = class {
262
295
  window.addEventListener("pagehide", () => this.flush());
263
296
  }
264
297
  };
298
+ function platformCookies() {
299
+ const out = {};
300
+ try {
301
+ for (const part of document.cookie.split(";")) {
302
+ const [k, ...v] = part.trim().split("=");
303
+ const value = decodeURIComponent(v.join("=") || "").slice(0, 300);
304
+ if (!value) continue;
305
+ if (k === "_fbp") out.fbp = value;
306
+ else if (k === "_fbc") out.fbc = value;
307
+ else if (k === "_ttp") out.ttp = value;
308
+ }
309
+ } catch {
310
+ }
311
+ return out;
312
+ }
313
+ function pageProduct() {
314
+ const scripts = document.querySelectorAll('script[type="application/ld+json"]');
315
+ for (const el of Array.from(scripts)) {
316
+ let data;
317
+ try {
318
+ data = JSON.parse(el.textContent || "null");
319
+ } catch {
320
+ continue;
321
+ }
322
+ const queue = [data];
323
+ while (queue.length) {
324
+ const n = queue.shift();
325
+ if (Array.isArray(n)) {
326
+ queue.push(...n);
327
+ continue;
328
+ }
329
+ if (!n || typeof n !== "object") continue;
330
+ const o = n;
331
+ if (o["@graph"]) queue.push(o["@graph"]);
332
+ const type = o["@type"];
333
+ if (type !== "Product" && !(Array.isArray(type) && type.includes("Product"))) continue;
334
+ const offers = Array.isArray(o.offers) ? o.offers[0] : o.offers;
335
+ const id = String(o.sku || o.productID || o.gtin13 || o.mpn || o["@id"] || o.url || o.name || "").slice(0, 100);
336
+ if (!id) continue;
337
+ const brand = typeof o.brand === "string" ? o.brand : o.brand?.name;
338
+ const price = Number(offers?.price ?? offers?.lowPrice);
339
+ const item = { id };
340
+ if (typeof o.name === "string") item.name = o.name.slice(0, 200);
341
+ if (typeof o.category === "string") item.category = o.category.slice(0, 120);
342
+ if (brand) item.brand = String(brand).slice(0, 120);
343
+ if (Number.isFinite(price)) item.price = price;
344
+ const currency = typeof offers?.priceCurrency === "string" ? offers.priceCurrency.toUpperCase().slice(0, 3) : void 0;
345
+ return { item, currency };
346
+ }
347
+ }
348
+ return null;
349
+ }
265
350
  function contactChannel(href) {
266
351
  if (/^tel:/i.test(href)) return "call";
267
352
  if (/^mailto:[^?]/i.test(href)) return "email";
package/dist/server.cjs CHANGED
@@ -34,6 +34,18 @@ function sha256(s) {
34
34
  () => null
35
35
  );
36
36
  }
37
+ async function personOf(p) {
38
+ if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) {
39
+ const hash = await sha256(p.email.trim().toLowerCase());
40
+ return hash ? { hash, kind: "email" } : null;
41
+ }
42
+ if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
43
+ const digits = String(p.phone).replace(/[^0-9]/g, "");
44
+ const hash = digits.length >= 7 ? await sha256(digits) : null;
45
+ return hash ? { hash, kind: "phone" } : null;
46
+ }
47
+ return null;
48
+ }
37
49
  function hashPerson(p) {
38
50
  if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
39
51
  if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
@@ -91,14 +103,15 @@ var Optiq = class {
91
103
  }
92
104
  async prepare(e) {
93
105
  const { email, phone, ...rest } = e;
94
- const hash = e.person_hash || await hashPerson({ email, phone });
106
+ const who = e.person_hash ? { hash: e.person_hash, kind: e.person_kind || "email" } : await personOf({ email, phone });
95
107
  const iso = (v) => v instanceof Date ? v.toISOString() : v;
96
108
  return {
97
109
  ...rest,
98
110
  event_id: e.event_id || newId(),
99
111
  occurred_at: iso(e.occurred_at),
100
112
  last_ref_at: iso(e.last_ref_at),
101
- person_hash: hash || void 0
113
+ person_hash: who?.hash || void 0,
114
+ person_kind: who?.kind || void 0
102
115
  };
103
116
  }
104
117
  /** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
package/dist/server.d.ts CHANGED
@@ -12,6 +12,18 @@ export interface ServerEvent {
12
12
  phone?: string | number;
13
13
  /** Who, if you already hash: SHA-256 of the trimmed, lower-case email, or of the phone's digits. */
14
14
  person_hash?: string;
15
+ /** What person_hash is of, so the ad platforms match it as an email or a phone. Default "email". */
16
+ person_kind?: "email" | "phone";
17
+ /** What was bought: each with its id, and its name, category, price and quantity where known. */
18
+ items?: {
19
+ id: string;
20
+ name?: string;
21
+ category?: string;
22
+ brand?: string;
23
+ variant?: string;
24
+ price?: number;
25
+ quantity?: number;
26
+ }[];
15
27
  /** The browser's visitor id, if your site passes it to your server (localStorage optiq_vid). */
16
28
  visitor_id?: string;
17
29
  value?: number;
package/dist/server.js CHANGED
@@ -7,6 +7,18 @@ function sha256(s) {
7
7
  () => null
8
8
  );
9
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
+ }
10
22
  function hashPerson(p) {
11
23
  if (p && typeof p.email === "string" && p.email.indexOf("@") > 0) return sha256(p.email.trim().toLowerCase());
12
24
  if (p && (typeof p.phone === "string" || typeof p.phone === "number")) {
@@ -64,14 +76,15 @@ var Optiq = class {
64
76
  }
65
77
  async prepare(e) {
66
78
  const { email, phone, ...rest } = e;
67
- const hash = e.person_hash || await hashPerson({ email, phone });
79
+ const who = e.person_hash ? { hash: e.person_hash, kind: e.person_kind || "email" } : await personOf({ email, phone });
68
80
  const iso = (v) => v instanceof Date ? v.toISOString() : v;
69
81
  return {
70
82
  ...rest,
71
83
  event_id: e.event_id || newId(),
72
84
  occurred_at: iso(e.occurred_at),
73
85
  last_ref_at: iso(e.last_ref_at),
74
- person_hash: hash || void 0
86
+ person_hash: who?.hash || void 0,
87
+ person_kind: who?.kind || void 0
75
88
  };
76
89
  }
77
90
  /** Sends one event or many (any number: they go 500 at a time). Answers what was kept, repeated or refused. */
package/dist/t.js CHANGED
@@ -1,2 +1,2 @@
1
- /* Optiq Studio Enterprise tracking 0.1.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";(()=>{function k(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)}function m(n){if(n&&typeof n.email=="string"&&n.email.indexOf("@")>0)return k(n.email.trim().toLowerCase());if(n&&(typeof n.phone=="string"||typeof n.phone=="number")){let t=String(n.phone).replace(/[^0-9]/g,"");if(t.length>=7)return k(t)}return Promise.resolve(null)}var A="https://optiq-collect-951694748196.europe-west2.run.app/e",b=864e5,P=30*6e4,S=()=>typeof window!="undefined"&&typeof document!="undefined";function p(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 h(n){try{return JSON.parse(p(n)||"{}")||{}}catch(t){return{}}}function w(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 y=class{constructor(t){this.queue=[];this.timer=null;this.lastPath=null;this.wired=!1;this.key=t.key,this.endpoint=t.endpoint||A,this.consentState=t.consent||p("consent")||"unknown";let e=new URLSearchParams(window.location.search),r=Date.now();this.touch=h("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*b)&&(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 f=e.get(`utm_${l}`);f&&(i[l]=f.slice(0,200))}Object.keys(i).length&&d("utm",JSON.stringify(i)),this.utm=Object.keys(i).length?i:h("utm");let o={};for(let l of["gclid","fbclid","ttclid","wbraid","gbraid"]){let f=e.get(l);f&&(o[l]=f.slice(0,500))}Object.keys(o).length&&d("clicks",JSON.stringify(o)),this.clicks=Object.keys(o).length?o:h("clicks");let u=this.consentState==="denied"?{}:h("visit"),c=`${e.get("utm_campaign")||""}|${s||""}`;this.visit=u.id&&r-(u.last||0)<=P&&(c==="|"||c===u.from)?u:{id:w(20),started:new Date(r).toISOString(),from:c,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=p("ph"),t.auto!==!1&&this.wire()}visitor(){if(this.consentState==="denied")return null;let t=p("vid");return t||(t=w(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[c,l]of Object.entries(e))!["value","currency","email","phone"].includes(c)&&l!==void 0&&(r[c]=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*b?this.touch.last:null,o=this.consentState==="denied",u={event_id:w(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:o?{}:this.clicks,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};m(o?null:e).then(c=>{c&&(this.person=c,d("ph",c)),u.person_hash=o?null:this.person,this.queue.push(u),this.timer||(this.timer=setTimeout(()=>this.flush(),400))})}identify(t){m(t).then(e=>{e&&this.consentState!=="denied"&&(this.person=e,d("ph",e))})}consent(t){if(this.consentState=t==="granted"||t==="denied"?t:"unknown",d("consent",this.consentState),this.consentState==="denied"){for(let e of["vid","ph","clicks","visit"])d(e,null);this.person=null,this.clicks={}}}page(){let t=window.location.pathname+window.location.search;t!==this.lastPath&&(this.lastPath=t,this.track("page_view"))}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(()=>a==null?void 0:a.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,o;let r=(o=(i=e.target)==null?void 0:i.closest)==null?void 0:o.call(i,"a[href]");if(!r)return;let s=L(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 L(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 a=null,_=[];function q(n){return S()?a||(!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):(a=new y(n),_.splice(0).forEach(t=>t()),a)):null}var g=n=>{S()&&(a?n(a):_.push(()=>a&&n(a)))},v=(n,t)=>g(e=>e.track(n,t)),T=n=>g(t=>t.identify(n)),O=n=>g(t=>t.consent(n)),x=()=>g(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,o,u){i==="track"?v(String(o),u||{}):i==="identify"?T(o||{}):i==="consent"?O(o):i==="page"&&x()}let r=(...i)=>e(...i);r.track=(i,o)=>v(i,o),window.optiq=r;let s=t.filter(i=>i[0]==="consent").pop();q({key:n.getAttribute("data-key")||"",endpoint:n.getAttribute("data-endpoint")||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.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,8 +1,17 @@
1
1
  {
2
2
  "name": "@optiqstudio/track",
3
- "version": "0.1.0",
3
+ "version": "0.2.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
- "keywords": ["optiq", "optiq studio", "attribution", "tracking", "conversions", "analytics", "marketing", "whatsapp"],
5
+ "keywords": [
6
+ "optiq",
7
+ "optiq studio",
8
+ "attribution",
9
+ "tracking",
10
+ "conversions",
11
+ "analytics",
12
+ "marketing",
13
+ "whatsapp"
14
+ ],
6
15
  "homepage": "https://enterprise.optiq.studio/developers",
7
16
  "license": "MIT",
8
17
  "author": "DaveLabs <info@davelabs.co> (https://davelabs.co)",
@@ -24,8 +33,14 @@
24
33
  },
25
34
  "./package.json": "./package.json"
26
35
  },
27
- "files": ["dist", "README.md", "LICENSE"],
28
- "engines": { "node": ">=20" },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
29
44
  "scripts": {
30
45
  "build": "node build.mjs",
31
46
  "test": "node --test test/*.test.mjs",