mk-sdk-git 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/mk-sdk.js +1 -0
- package/package.json +30 -0
- package/src/core/autoTrack.js +159 -0
- package/src/core/config.js +84 -0
- package/src/core/identity.js +57 -0
- package/src/core/pageContext.js +83 -0
- package/src/core/sanitize.js +73 -0
- package/src/core/transport.js +124 -0
- package/src/ecommerce/extractors.js +144 -0
- package/src/ecommerce/funnel.js +131 -0
- package/src/ecommerce/jsonld.js +23 -0
- package/src/index.js +67 -0
- package/src/platforms/generic.js +41 -0
- package/src/platforms/hybris.js +44 -0
- package/src/platforms/index.js +86 -0
- package/src/platforms/nuvemshop.js +48 -0
- package/src/platforms/shopify.js +35 -0
- package/src/platforms/vtex.js +34 -0
- package/src/platforms/wake.js +52 -0
- package/src/tryon/anchor.js +122 -0
- package/src/tryon/automount.js +255 -0
- package/src/tryon/bridge.js +210 -0
- package/src/tryon/buttons/config.js +56 -0
- package/src/tryon/buttons/gregory-black.js +38 -0
- package/src/tryon/buttons/gregory-card.js +76 -0
- package/src/tryon/buttons/helpers.js +57 -0
- package/src/tryon/buttons/registry.js +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// sanitize.js — remove PII e campos pesados antes de enviar pro collector.
|
|
2
|
+
|
|
3
|
+
// Campos que NUNCA devem sair da página (LGPD). Match case-insensitive parcial.
|
|
4
|
+
const PII_PATTERNS = [
|
|
5
|
+
/email/i, /e-?mail/i, /cpf/i, /cnpj/i, /phone/i, /telefone/i, /celular/i,
|
|
6
|
+
/address/i, /endereco/i, /cep/i, /zip/i, /card/i, /cartao/i, /cvv/i,
|
|
7
|
+
/password/i, /senha/i, /token/i, /\bnome\b/i, /fullname/i, /firstname/i,
|
|
8
|
+
/lastname/i, /sobrenome/i, /birth/i, /nascimento/i, /rg\b/i
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
// Campos de imagem/binário pesados (não têm valor analítico).
|
|
12
|
+
const HEAVY_KEYS = { imageUrl: 1, image: 1, thumbnail: 1, photo: 1, imageBase64: 1, src: 1 };
|
|
13
|
+
const MAX_STRING_LEN = 2000;
|
|
14
|
+
|
|
15
|
+
function isPiiKey(key) {
|
|
16
|
+
for (let i = 0; i < PII_PATTERNS.length; i++) {
|
|
17
|
+
if (PII_PATTERNS[i].test(key)) return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sanitiza um objeto de params:
|
|
24
|
+
* - Remove chaves PII completamente
|
|
25
|
+
* - Troca campos de imagem por <key>Present: boolean
|
|
26
|
+
* - Trunca strings > 2KB
|
|
27
|
+
* - Percorre 1 nível de profundidade (objetos aninhados rasos)
|
|
28
|
+
*/
|
|
29
|
+
export function sanitize(data) {
|
|
30
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
31
|
+
if (typeof data === 'string' && data.length > MAX_STRING_LEN) {
|
|
32
|
+
return data.slice(0, 200) + '...[truncated ' + data.length + 'B]';
|
|
33
|
+
}
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const k in data) {
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(data, k)) continue;
|
|
39
|
+
if (isPiiKey(k)) continue; // descarta PII
|
|
40
|
+
const v = data[k];
|
|
41
|
+
if (HEAVY_KEYS[k]) {
|
|
42
|
+
out[k + 'Present'] = !!v;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (typeof v === 'string' && v.length > MAX_STRING_LEN) {
|
|
46
|
+
out[k] = v.slice(0, 200) + '...[truncated ' + v.length + 'B]';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
50
|
+
// 1 nível de recursão pra objetos aninhados (ex: product, ecommerce)
|
|
51
|
+
out[k] = sanitizeShallow(v);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
out[k] = v;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sanitizeShallow(obj) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const k in obj) {
|
|
62
|
+
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
|
63
|
+
if (isPiiKey(k)) continue;
|
|
64
|
+
const v = obj[k];
|
|
65
|
+
if (HEAVY_KEYS[k]) { out[k + 'Present'] = !!v; continue; }
|
|
66
|
+
if (typeof v === 'string' && v.length > MAX_STRING_LEN) {
|
|
67
|
+
out[k] = v.slice(0, 200) + '...[truncated]';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out[k] = v; // não recursa mais fundo (evita payload gigante)
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// transport.js — fila de eventos com batch, flush por timer/tamanho, sendBeacon.
|
|
2
|
+
|
|
3
|
+
import { getConfig, log } from './config.js';
|
|
4
|
+
import { getVisitorId, getSessionId } from './identity.js';
|
|
5
|
+
import { sanitize } from './sanitize.js';
|
|
6
|
+
|
|
7
|
+
let queue = [];
|
|
8
|
+
let flushTimer = null;
|
|
9
|
+
let siteContext = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Define o contexto do site (uma vez por pageview). Injetado em todo batch.
|
|
13
|
+
*/
|
|
14
|
+
export function setSiteContext(ctx) {
|
|
15
|
+
siteContext = ctx;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Enfileira um evento. name é o nome cru (vira "page_<name>").
|
|
20
|
+
* Aplica sanitize automaticamente.
|
|
21
|
+
*/
|
|
22
|
+
export function track(name, params, tsOverride) {
|
|
23
|
+
const cfg = getConfig();
|
|
24
|
+
if (!cfg.projectId) {
|
|
25
|
+
// Sem projectId não dá pra enviar — enfileira em pré-buffer pequeno.
|
|
26
|
+
(track._pre = track._pre || []).push({ name, params, ts: tsOverride || Date.now() });
|
|
27
|
+
if (track._pre.length > 50) track._pre.shift();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Drena pré-buffer se houver
|
|
32
|
+
if (track._pre && track._pre.length) {
|
|
33
|
+
const pending = track._pre;
|
|
34
|
+
track._pre = [];
|
|
35
|
+
for (const p of pending) _enqueue(p.name, p.params, p.ts);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_enqueue(name, params, tsOverride);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _enqueue(name, params, tsOverride) {
|
|
42
|
+
const cfg = getConfig();
|
|
43
|
+
const cleanParams = sanitize(params || {});
|
|
44
|
+
const finalName = 'page_' + normalizeName(name);
|
|
45
|
+
queue.push({
|
|
46
|
+
name: finalName,
|
|
47
|
+
ts: tsOverride || Date.now(),
|
|
48
|
+
params: cleanParams
|
|
49
|
+
});
|
|
50
|
+
log('queued', name, cleanParams);
|
|
51
|
+
|
|
52
|
+
// Canal de observação (extensão de QA). Inócuo em produção (observe=false).
|
|
53
|
+
if (cfg.observe) {
|
|
54
|
+
try {
|
|
55
|
+
window.postMessage({
|
|
56
|
+
__mkpixel: true,
|
|
57
|
+
event: { name: finalName, params: cleanParams },
|
|
58
|
+
site: siteContext ? { pageType: siteContext.pageType, platform: siteContext.platform, url: siteContext.url } : null
|
|
59
|
+
}, '*');
|
|
60
|
+
} catch (_) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (queue.length >= cfg.batchMaxSize) {
|
|
64
|
+
flush();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!flushTimer) {
|
|
68
|
+
flushTimer = setTimeout(() => { flushTimer = null; flush(); }, cfg.batchIntervalMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normaliza nome: tira 'on' prefix, camelCase -> snake_case, evita prefixo page_ duplo.
|
|
74
|
+
*/
|
|
75
|
+
function normalizeName(name) {
|
|
76
|
+
if (typeof name !== 'string' || !name) return 'unknown';
|
|
77
|
+
let n = name.replace(/^on/, '');
|
|
78
|
+
n = n.replace(/[A-Z]/g, (m, i) => (i > 0 ? '_' : '') + m.toLowerCase());
|
|
79
|
+
if (n.indexOf('page_') === 0) n = n.slice(5); // evita page_page_view
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function flush(useBeacon) {
|
|
84
|
+
if (queue.length === 0) return;
|
|
85
|
+
const cfg = getConfig();
|
|
86
|
+
if (!cfg.projectId || !cfg.collectorUrl) return;
|
|
87
|
+
|
|
88
|
+
const events = queue.splice(0, 50);
|
|
89
|
+
const payload = JSON.stringify({
|
|
90
|
+
product: cfg.product || undefined,
|
|
91
|
+
visitorId: getVisitorId(),
|
|
92
|
+
sessionId: getSessionId(),
|
|
93
|
+
events,
|
|
94
|
+
site: siteContext || undefined
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const url = cfg.collectorUrl + '/v1/track';
|
|
98
|
+
|
|
99
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
100
|
+
try {
|
|
101
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
102
|
+
navigator.sendBeacon(url + '?projectId=' + encodeURIComponent(cfg.projectId), blob);
|
|
103
|
+
return;
|
|
104
|
+
} catch (_) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
fetch(url, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
keepalive: true,
|
|
110
|
+
headers: { 'Content-Type': 'application/json', 'X-MK-Project-Id': cfg.projectId },
|
|
111
|
+
body: payload
|
|
112
|
+
}).catch(() => {});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Flush final no unload pra não perder eventos pendentes.
|
|
116
|
+
export function initUnloadFlush() {
|
|
117
|
+
try {
|
|
118
|
+
window.addEventListener('pagehide', () => flush(true));
|
|
119
|
+
window.addEventListener('beforeunload', () => flush(true));
|
|
120
|
+
document.addEventListener('visibilitychange', () => {
|
|
121
|
+
if (document.hidden) flush(true);
|
|
122
|
+
});
|
|
123
|
+
} catch (_) {}
|
|
124
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// extractors.js — extração de produto/pedido/lista por sinais nativos genéricos.
|
|
2
|
+
// O SKU por plataforma vive em ../platforms/ (resolveSku é re-exportado daqui só
|
|
3
|
+
// por compatibilidade com quem já importava deste módulo). JSON-LD em ./jsonld.js.
|
|
4
|
+
|
|
5
|
+
import { readJsonLd } from './jsonld.js';
|
|
6
|
+
import { resolveSku } from '../platforms/index.js';
|
|
7
|
+
|
|
8
|
+
export { readJsonLd }; // re-export (compat)
|
|
9
|
+
export { resolveSku }; // re-export (compat) — a lógica por plataforma está em ../platforms/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extrai produto da PDP. Combina o resolveSku (por plataforma) com nome/preço/marca
|
|
13
|
+
* de JSON-LD, microdata ou Open Graph.
|
|
14
|
+
*/
|
|
15
|
+
export function extractProduct() {
|
|
16
|
+
const skuRes = resolveSku();
|
|
17
|
+
const products = readJsonLd(['Product']);
|
|
18
|
+
|
|
19
|
+
if (products.length > 0) {
|
|
20
|
+
const p = products[0];
|
|
21
|
+
const offer = Array.isArray(p.offers) ? p.offers[0] : p.offers;
|
|
22
|
+
return {
|
|
23
|
+
sku: skuRes ? skuRes.value : (p.sku || p.mpn || p.gtin13 || p.productID || null),
|
|
24
|
+
skuSource: skuRes ? skuRes.source : null,
|
|
25
|
+
name: typeof p.name === 'string' ? p.name : null,
|
|
26
|
+
price: offer ? parseFloat(offer.price || offer.lowPrice) || null : null,
|
|
27
|
+
currency: offer ? offer.priceCurrency : null,
|
|
28
|
+
brand: p.brand ? (typeof p.brand === 'string' ? p.brand : p.brand.name) : null,
|
|
29
|
+
category: p.category || null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const og = (prop) => {
|
|
34
|
+
const m = document.querySelector('meta[property="og:' + prop + '"], meta[property="product:' + prop + '"]');
|
|
35
|
+
return m ? m.getAttribute('content') : null;
|
|
36
|
+
};
|
|
37
|
+
const micro = (prop) => {
|
|
38
|
+
const e = document.querySelector('[itemprop="' + prop + '"]');
|
|
39
|
+
return e ? (e.getAttribute('content') || (e.textContent || '').trim()) : null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (skuRes || og('type') === 'product' || og('price:amount') || micro('name')) {
|
|
43
|
+
const priceRaw = micro('price') || og('price:amount');
|
|
44
|
+
return {
|
|
45
|
+
sku: skuRes ? skuRes.value : null,
|
|
46
|
+
skuSource: skuRes ? skuRes.source : null,
|
|
47
|
+
name: micro('name') || og('title') || (document.title || null),
|
|
48
|
+
price: priceRaw ? parseFloat(String(priceRaw).replace(',', '.')) || null : null,
|
|
49
|
+
currency: micro('priceCurrency') || og('price:currency') || null,
|
|
50
|
+
brand: micro('brand') || null,
|
|
51
|
+
category: null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extrai dados de pedido/compra (página de confirmação).
|
|
59
|
+
* Cascata: JSON-LD Order > dataLayer (bônus) > meta > DOM scraping.
|
|
60
|
+
*/
|
|
61
|
+
export function extractOrder() {
|
|
62
|
+
const orders = readJsonLd(['Order', 'Invoice']);
|
|
63
|
+
if (orders.length > 0) {
|
|
64
|
+
const o = orders[0];
|
|
65
|
+
const total = o.totalPaymentDue ? parseFloat(o.totalPaymentDue.price) : parseFloat(o.price || o.total);
|
|
66
|
+
return {
|
|
67
|
+
orderId: o.orderNumber || o.confirmationNumber || o.identifier || null,
|
|
68
|
+
revenue: Number.isFinite(total) ? total : null,
|
|
69
|
+
currency: (o.totalPaymentDue && o.totalPaymentDue.priceCurrency) || o.priceCurrency || null,
|
|
70
|
+
source: 'jsonld'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (Array.isArray(window.dataLayer)) {
|
|
75
|
+
for (let i = window.dataLayer.length - 1; i >= 0; i--) {
|
|
76
|
+
const item = window.dataLayer[i];
|
|
77
|
+
if (item && item.event === 'purchase' && item.ecommerce) {
|
|
78
|
+
const ec = item.ecommerce;
|
|
79
|
+
return {
|
|
80
|
+
orderId: ec.transaction_id || ec.transactionId || null,
|
|
81
|
+
revenue: parseFloat(ec.value || ec.revenue) || null,
|
|
82
|
+
currency: ec.currency || ec.currencyCode || null,
|
|
83
|
+
itemCount: (ec.items || ec.products || []).length || null,
|
|
84
|
+
source: 'datalayer'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (_) {}
|
|
90
|
+
const metaTotal = document.querySelector('meta[property="order:total"], meta[name="order-total"]');
|
|
91
|
+
if (metaTotal && metaTotal.content) {
|
|
92
|
+
return { orderId: null, revenue: parseFloat(metaTotal.content) || null, source: 'meta' };
|
|
93
|
+
}
|
|
94
|
+
const dom = scrapeOrderFromDom();
|
|
95
|
+
if (dom) return dom;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scrapeOrderFromDom() {
|
|
100
|
+
try {
|
|
101
|
+
const totalSelectors = ['.order-total', '#order-total', '.total-price', '.order__total', '[data-order-total]', '.summary-total', '.grand-total'];
|
|
102
|
+
for (const sel of totalSelectors) {
|
|
103
|
+
const el = document.querySelector(sel);
|
|
104
|
+
if (el) {
|
|
105
|
+
const val = parseMoney(el.textContent);
|
|
106
|
+
if (val) return { orderId: scrapeOrderId(), revenue: val, source: 'dom' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
} catch (_) { return null; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function scrapeOrderId() {
|
|
114
|
+
try {
|
|
115
|
+
const text = document.body.innerText || '';
|
|
116
|
+
const m = text.match(/(?:pedido|order|n[º°o]\.?)\s*#?\s*([A-Z0-9-]{4,})/i);
|
|
117
|
+
return m ? m[1] : null;
|
|
118
|
+
} catch (_) { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseMoney(str) {
|
|
122
|
+
if (!str) return null;
|
|
123
|
+
const cleaned = String(str).replace(/[^\d.,]/g, '');
|
|
124
|
+
if (!cleaned) return null;
|
|
125
|
+
let normalized;
|
|
126
|
+
if (cleaned.indexOf(',') > cleaned.indexOf('.')) normalized = cleaned.replace(/\./g, '').replace(',', '.');
|
|
127
|
+
else normalized = cleaned.replace(/,/g, '');
|
|
128
|
+
const n = parseFloat(normalized);
|
|
129
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Lê item list de categoria/busca (JSON-LD ItemList). */
|
|
133
|
+
export function extractItemList() {
|
|
134
|
+
const lists = readJsonLd(['ItemList', 'CollectionPage']);
|
|
135
|
+
if (lists.length === 0) return null;
|
|
136
|
+
const list = lists[0];
|
|
137
|
+
const elements = list.itemListElement || (list.mainEntity && list.mainEntity.itemListElement) || [];
|
|
138
|
+
const skus = [];
|
|
139
|
+
for (const el of elements.slice(0, 50)) {
|
|
140
|
+
const item = el.item || el;
|
|
141
|
+
if (item && (item.sku || item.productID || item.name)) skus.push(item.sku || item.productID || item.name);
|
|
142
|
+
}
|
|
143
|
+
return skus.length > 0 ? { count: skus.length, skus: skus.slice(0, 20) } : null;
|
|
144
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// funnel.js — emite eventos do funil de e-commerce reagindo ao pageType
|
|
2
|
+
// detectado, + heurística de add_to_cart por clique. Sinais nativos, sem GA.
|
|
3
|
+
|
|
4
|
+
import { track } from '../core/transport.js';
|
|
5
|
+
import { log } from '../core/config.js';
|
|
6
|
+
import { extractProduct, extractOrder, extractItemList } from './extractors.js';
|
|
7
|
+
|
|
8
|
+
let purchaseEmittedThisLoad = false;
|
|
9
|
+
|
|
10
|
+
// ─── Heurística de add_to_cart por texto de botão (mesma do collector) ───
|
|
11
|
+
const ADD_TO_CART_PATTERNS = [
|
|
12
|
+
/\b(adicionar|adic\.?|colocar|incluir|jogar)\b.{0,30}\b(carrinho|sacola|sacolinha|bag)\b/i,
|
|
13
|
+
/\bcomprar\b(?!\s+(em|por|sem))/i, // só BUTTON (checado no listener)
|
|
14
|
+
/\bcomprar\s*$/i,
|
|
15
|
+
/^add\s+to\s+(cart|bag)\b/i,
|
|
16
|
+
/^buy\s+(now|it|this)\b/i,
|
|
17
|
+
/^(sacolinha|carrinho|sacola)$/i
|
|
18
|
+
];
|
|
19
|
+
const PURCHASE_BTN_PATTERNS = [
|
|
20
|
+
/^finalizar(\s+(compra|pedido|pagamento))?\b/i,
|
|
21
|
+
/^(fechar|confirmar)\s+(compra|pedido|pagamento)\b/i,
|
|
22
|
+
/^pagar(\s+(agora|pedido))?\b/i,
|
|
23
|
+
/^(complete|place)\s+(purchase|order)\b/i,
|
|
24
|
+
/^checkout$/i
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function matchAny(patterns, text) {
|
|
28
|
+
for (let i = 0; i < patterns.length; i++) if (patterns[i].test(text)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Chamado pelo autoTrack a cada pageview (incl. SPA). Emite o evento de funil
|
|
34
|
+
* correspondente ao tipo de página.
|
|
35
|
+
*/
|
|
36
|
+
export function onPageView(pageType) {
|
|
37
|
+
switch (pageType) {
|
|
38
|
+
case 'pdp': {
|
|
39
|
+
const product = extractProduct();
|
|
40
|
+
track('view_item', product || { sku: null });
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 'category':
|
|
44
|
+
case 'search': {
|
|
45
|
+
const list = extractItemList();
|
|
46
|
+
track('view_item_list', list || { count: 0 });
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case 'cart':
|
|
50
|
+
track('view_cart', {});
|
|
51
|
+
break;
|
|
52
|
+
case 'checkout':
|
|
53
|
+
track('begin_checkout', {});
|
|
54
|
+
break;
|
|
55
|
+
case 'purchase':
|
|
56
|
+
emitPurchase();
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Emite purchase com dedup. Conversão é contada mesmo sem revenue.
|
|
63
|
+
*/
|
|
64
|
+
function emitPurchase() {
|
|
65
|
+
const order = extractOrder();
|
|
66
|
+
|
|
67
|
+
// Dedup: por orderId (cross-reload) ou por load (sem orderId)
|
|
68
|
+
if (order && order.orderId) {
|
|
69
|
+
const key = '_mk_purch_' + order.orderId;
|
|
70
|
+
try {
|
|
71
|
+
if (sessionStorage.getItem(key)) { log('purchase já contado', order.orderId); return; }
|
|
72
|
+
sessionStorage.setItem(key, '1');
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
} else {
|
|
75
|
+
if (purchaseEmittedThisLoad) return;
|
|
76
|
+
}
|
|
77
|
+
purchaseEmittedThisLoad = true;
|
|
78
|
+
|
|
79
|
+
track('purchase', order || { revenue: null, _note: 'no_order_data_extracted' });
|
|
80
|
+
log('purchase emitido', order);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Listener dedicado pra detectar add_to_cart / begin_checkout por clique.
|
|
85
|
+
* Complementa os eventos de pageType (que não pegam o momento do clique).
|
|
86
|
+
*/
|
|
87
|
+
export function initFunnelClickTracking() {
|
|
88
|
+
if (!document.body) return;
|
|
89
|
+
let lastFire = 0;
|
|
90
|
+
document.body.addEventListener('click', (e) => {
|
|
91
|
+
try {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (now - lastFire < 150) return;
|
|
94
|
+
let el = e.target;
|
|
95
|
+
for (let i = 0; i < 5 && el && el !== document.body; i++) {
|
|
96
|
+
if (el.tagName === 'BUTTON' || el.tagName === 'A' ||
|
|
97
|
+
(el.getAttribute && el.getAttribute('role') === 'button')) break;
|
|
98
|
+
el = el.parentElement;
|
|
99
|
+
}
|
|
100
|
+
if (!el) return;
|
|
101
|
+
const isButton = el.tagName !== 'A';
|
|
102
|
+
const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
|
|
103
|
+
if (!text || text.length > 80) return;
|
|
104
|
+
|
|
105
|
+
// add_to_cart
|
|
106
|
+
let isAdd = false;
|
|
107
|
+
if (matchAny([ADD_TO_CART_PATTERNS[0]], text)) isAdd = true; // adicionar+carrinho (qualquer tag)
|
|
108
|
+
else if (isButton && matchAny([ADD_TO_CART_PATTERNS[1]], text)) isAdd = true; // comprar (só button)
|
|
109
|
+
else if (isButton && matchAny([ADD_TO_CART_PATTERNS[2]], text)) isAdd = true; // termina em comprar
|
|
110
|
+
else if (matchAny(ADD_TO_CART_PATTERNS.slice(3), text)) isAdd = true; // en + palavra única
|
|
111
|
+
|
|
112
|
+
if (isAdd) {
|
|
113
|
+
lastFire = now;
|
|
114
|
+
const product = extractProduct();
|
|
115
|
+
track('add_to_cart', {
|
|
116
|
+
via: 'click',
|
|
117
|
+
buttonText: text,
|
|
118
|
+
sku: product ? product.sku : null,
|
|
119
|
+
price: product ? product.price : null
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// begin_checkout (intenção)
|
|
125
|
+
if (isButton && matchAny(PURCHASE_BTN_PATTERNS, text)) {
|
|
126
|
+
lastFire = now;
|
|
127
|
+
track('begin_checkout', { via: 'click', buttonText: text });
|
|
128
|
+
}
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
}, { capture: true, passive: true });
|
|
131
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// jsonld.js — leitura de dados estruturados JSON-LD. Helper genérico, sem deps
|
|
2
|
+
// (usado por extractors, pageContext e platforms/generic — evita dependência circular).
|
|
3
|
+
|
|
4
|
+
export function readJsonLd(types) {
|
|
5
|
+
const results = [];
|
|
6
|
+
try {
|
|
7
|
+
const nodes = document.querySelectorAll('script[type="application/ld+json"]');
|
|
8
|
+
for (const node of nodes) {
|
|
9
|
+
let parsed;
|
|
10
|
+
try { parsed = JSON.parse(node.textContent); } catch (_) { continue; }
|
|
11
|
+
const items = [];
|
|
12
|
+
if (Array.isArray(parsed)) items.push(...parsed);
|
|
13
|
+
else if (parsed['@graph']) items.push(...parsed['@graph']);
|
|
14
|
+
else items.push(parsed);
|
|
15
|
+
for (const item of items) {
|
|
16
|
+
if (!item || !item['@type']) continue;
|
|
17
|
+
const t = Array.isArray(item['@type']) ? item['@type'] : [item['@type']];
|
|
18
|
+
if (!types || types.some((wanted) => t.includes(wanted))) results.push(item);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// index.js — entrypoint do mk-sdk. Inicia core tracking em todas as páginas
|
|
2
|
+
// e expõe a API de try-on em window.mk.
|
|
3
|
+
|
|
4
|
+
import { resolveConfig, getConfig, log } from './core/config.js';
|
|
5
|
+
import { startAutoTrack } from './core/autoTrack.js';
|
|
6
|
+
import { initUnloadFlush } from './core/transport.js';
|
|
7
|
+
import { onPageView, initFunnelClickTracking } from './ecommerce/funnel.js';
|
|
8
|
+
import { maybeMountButton, initPickerBridge } from './tryon/automount.js';
|
|
9
|
+
import * as tryon from './tryon/bridge.js';
|
|
10
|
+
|
|
11
|
+
(function boot() {
|
|
12
|
+
'use strict';
|
|
13
|
+
try {
|
|
14
|
+
const cfg = resolveConfig();
|
|
15
|
+
|
|
16
|
+
// ─── Core tracking: roda em TODA página ───────────────────────────────
|
|
17
|
+
function init() {
|
|
18
|
+
try {
|
|
19
|
+
initUnloadFlush();
|
|
20
|
+
initPickerBridge(); // canal de preview pro picker de posição do addon
|
|
21
|
+
tryon.initListener(); // escuta o provador (add_to_cart → cart-adapter)
|
|
22
|
+
if (cfg.autoTrack !== false) {
|
|
23
|
+
// funnel + auto-mount do botão reagem ao pageType a cada pageview
|
|
24
|
+
startAutoTrack((pageType, ctx) => {
|
|
25
|
+
onPageView(pageType, ctx);
|
|
26
|
+
maybeMountButton(pageType);
|
|
27
|
+
});
|
|
28
|
+
initFunnelClickTracking();
|
|
29
|
+
}
|
|
30
|
+
log('mk-sdk iniciado', { projectId: cfg.projectId, collectorUrl: cfg.collectorUrl });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (cfg.debug) console.error('[mk-sdk] init erro', e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (document.readyState === 'loading') {
|
|
37
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
38
|
+
} else {
|
|
39
|
+
init();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── API pública ──────────────────────────────────────────────────────
|
|
43
|
+
const api = {
|
|
44
|
+
// try-on
|
|
45
|
+
open: tryon.open,
|
|
46
|
+
close: tryon.close,
|
|
47
|
+
isAvailable: tryon.isAvailable,
|
|
48
|
+
getAvailability: tryon.getAvailability,
|
|
49
|
+
onReady: tryon.onReady,
|
|
50
|
+
onClose: tryon.onClose,
|
|
51
|
+
onAddToCart: tryon.onAddToCart,
|
|
52
|
+
onProductLoaded: tryon.onProductLoaded,
|
|
53
|
+
onError: tryon.onError,
|
|
54
|
+
onInteraction: tryon.onInteraction,
|
|
55
|
+
onGenerationComplete: tryon.onGenerationComplete,
|
|
56
|
+
// meta
|
|
57
|
+
_version: '0.1.0',
|
|
58
|
+
_isMkSdk: true
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (typeof window !== 'undefined') {
|
|
62
|
+
window.mk = api; // global único e canônico do mk-sdk
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
if (typeof console !== 'undefined') console.error('[mk-sdk] boot fatal', e);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// generic.js — fallback pra lojas sem plataforma com adapter dedicado.
|
|
2
|
+
// SKU por sinais nativos (JSON-LD, microdata, Konfidency); cart = clique nativo.
|
|
3
|
+
|
|
4
|
+
import { readJsonLd } from '../ecommerce/jsonld.js';
|
|
5
|
+
|
|
6
|
+
export const id = 'generic';
|
|
7
|
+
export function detect() { return true; } // sempre casa (é o último recurso)
|
|
8
|
+
|
|
9
|
+
export function resolveSku() {
|
|
10
|
+
const products = readJsonLd(['Product']);
|
|
11
|
+
if (products[0]) {
|
|
12
|
+
const p = products[0];
|
|
13
|
+
const s = p.sku || p.mpn || p.gtin13 || p.productID;
|
|
14
|
+
if (s) return { value: String(s), source: 'jsonld' };
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const el = document.querySelector('[itemprop="sku"]');
|
|
18
|
+
if (el) { const v = el.getAttribute('content') || (el.textContent || '').trim(); if (v) return { value: String(v), source: 'microdata' }; }
|
|
19
|
+
} catch (_) {}
|
|
20
|
+
try {
|
|
21
|
+
const kd = window.konfidencyData;
|
|
22
|
+
if (kd && kd.product && kd.product.variants && kd.product.variants[0])
|
|
23
|
+
return { value: String(kd.product.variants[0]), source: 'konfidency' };
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ANCHOR = /(adicionar\s*(à|a|ao)?\s*(sacola|carrinho|cesta|bolsa))|adicionar ao carrinho|add to (cart|bag)|comprar agora/i;
|
|
29
|
+
const ANCHOR_NEG = /continuar|como comprar|lista|desejos|favorito/i;
|
|
30
|
+
|
|
31
|
+
// fallback universal: clica o botão de carrinho nativo (a loja resolve o resto)
|
|
32
|
+
export async function addToCart() {
|
|
33
|
+
const els = document.querySelectorAll('button,[role=button],input[type=submit]');
|
|
34
|
+
for (const el of els) {
|
|
35
|
+
const t = (el.textContent || el.value || '').replace(/\s+/g, ' ').trim();
|
|
36
|
+
if (t && t.length < 40 && ANCHOR.test(t) && !ANCHOR_NEG.test(t)) {
|
|
37
|
+
try { el.click(); return { platform: 'native-click', clicked: t }; } catch (_) {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { platform: 'none', error: 'botão de carrinho nativo não encontrado' };
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// hybris.js — TUDO da plataforma SAP Commerce / Hybris (ex: Marisa): detecção,
|
|
2
|
+
// SKU (productCode da URL) e carrinho. Validado ao vivo na Marisa.
|
|
3
|
+
|
|
4
|
+
export const id = 'hybris';
|
|
5
|
+
export function detect() { return !!(window.ACC && window.ACC.config); }
|
|
6
|
+
|
|
7
|
+
// o productCode da URL (/p/{code}) é o identificador do catálogo. O [itemprop=sku]
|
|
8
|
+
// do Hybris é um código interno diferente — por isso o SKU vem da URL aqui.
|
|
9
|
+
export function resolveSku() {
|
|
10
|
+
const m = (location.pathname || '').match(/\/p\/([^/?#]+)/);
|
|
11
|
+
if (m && m[1]) return { value: decodeURIComponent(m[1]), source: 'hybris/url' };
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const norm = (s) => String(s == null ? '' : s).trim().toLowerCase();
|
|
16
|
+
|
|
17
|
+
export async function addToCart({ size, quantity }) {
|
|
18
|
+
// cada tamanho carrega o productCode da variante em data-code
|
|
19
|
+
const els = [...document.querySelectorAll('.js-product-select-size, a[data-code], [class*="select-size"]')]
|
|
20
|
+
.filter((e) => e.getAttribute && e.getAttribute('data-code'));
|
|
21
|
+
let el = size ? els.find((e) => norm(e.textContent) === norm(size)) : null;
|
|
22
|
+
if (!el) el = els.find((e) => e.offsetParent !== null) || els[0];
|
|
23
|
+
const code = el && el.getAttribute('data-code');
|
|
24
|
+
if (!code) throw new Error('hybris: variante (data-code) não encontrada');
|
|
25
|
+
|
|
26
|
+
const form = document.getElementById('product--add-to-cart-form') || document.querySelector('form[action*="/cart/add"]');
|
|
27
|
+
const csrfEl = form && form.querySelector('[name="CSRFToken"]');
|
|
28
|
+
const csrf = csrfEl && csrfEl.value;
|
|
29
|
+
const action = (form && form.getAttribute('action')) || '/cart/add';
|
|
30
|
+
if (!csrf) throw new Error('hybris: CSRFToken não encontrado');
|
|
31
|
+
|
|
32
|
+
const body = new URLSearchParams();
|
|
33
|
+
body.set('productCodePost', code);
|
|
34
|
+
body.set('qty', String(quantity || 1));
|
|
35
|
+
body.set('CSRFToken', csrf);
|
|
36
|
+
|
|
37
|
+
const resp = await fetch(action, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
|
|
40
|
+
body: body.toString()
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok) throw new Error('hybris: ' + action + ' HTTP ' + resp.status);
|
|
43
|
+
return { platform: 'hybris', productCode: code, size: el.textContent.trim() };
|
|
44
|
+
}
|