mkfashion-sdk 2.7.4 → 2.7.5
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/docs/2026-05-22-sdk-v3-ecommerce-tracking-design.md +311 -311
- package/gtm-snippet.js +116 -116
- package/package.json +1 -1
- package/src/mkfashion.js +113 -1
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
# SDK v3 — Tracking de E-commerce Completo (Core Tracker) — Design Spec
|
|
2
|
-
|
|
3
|
-
**Empresa:** MetaKosmos
|
|
4
|
-
**Data:** 2026-05-22
|
|
5
|
-
**Status:** Rascunho para revisão
|
|
6
|
-
**Repos afetados:** `mkfashion-sdk`, `mk-collector-api`
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 1. Contexto e motivação
|
|
11
|
-
|
|
12
|
-
### Situação atual (v2.7.2)
|
|
13
|
-
|
|
14
|
-
A `mkfashion-sdk` roda **apenas na PDP** (página de produto). O cliente chama
|
|
15
|
-
`mkfashion.init({ projectId, identifier })` ou `isAvailable(...)` na PDP, e a
|
|
16
|
-
partir daí a SDK:
|
|
17
|
-
|
|
18
|
-
- Auto-captura `page_view`, `page_click`, `page_scroll_depth`, `page_form_submit`,
|
|
19
|
-
`page_engagement` da PDP.
|
|
20
|
-
- Captura eventos do iframe de try-on (`page_modal_opened`, `page_generation_complete`, etc).
|
|
21
|
-
- Detecta `add_to_cart` por heurística de texto de botão.
|
|
22
|
-
|
|
23
|
-
**Limitação:** só enxergamos a PDP. Não vemos a jornada completa do visitante —
|
|
24
|
-
entrada, navegação por categorias, carrinho, checkout e **compra final**. Sem o
|
|
25
|
-
purchase, não conseguimos responder a pergunta de negócio mais importante:
|
|
26
|
-
|
|
27
|
-
> **"Visitantes que usaram o try-on convertem mais do que os que não usaram?"**
|
|
28
|
-
|
|
29
|
-
### Objetivo da v3
|
|
30
|
-
|
|
31
|
-
Transformar a SDK de "widget de PDP" em **core tracker de e-commerce** que roda
|
|
32
|
-
em **todas as páginas** do site, capturando o funil completo:
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
entrada → navegação → produto → (try-on) → carrinho → checkout → COMPRA
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
**Restrições de design (decididas com o stakeholder):**
|
|
39
|
-
|
|
40
|
-
1. **Zero trabalho extra pro cliente** além de importar o script. Nada de chamar
|
|
41
|
-
métodos novos, nada de configurar dataLayer.
|
|
42
|
-
2. **Zero dependência de ferramentas de terceiros** (GA4/GTM podem não existir
|
|
43
|
-
na loja). Captura por sinais NATIVOS do e-commerce.
|
|
44
|
-
3. **Try-on continua igual**: a marca chama `open()`/`isAvailable()` só na PDP,
|
|
45
|
-
exatamente como hoje.
|
|
46
|
-
4. **Compatibilidade**: clientes na v2 (SDK na PDP) continuam funcionando durante
|
|
47
|
-
a migração.
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## 2. Princípio central: captura por sinais nativos
|
|
52
|
-
|
|
53
|
-
Toda loja — independente de ter GA4, GTM, ou qualquer analytics — possui 3 coisas
|
|
54
|
-
que existem porque a loja precisa **funcionar e ser indexada**:
|
|
55
|
-
|
|
56
|
-
| Sinal nativo | Sempre existe? | Usado para |
|
|
57
|
-
|---|---|---|
|
|
58
|
-
| **URLs estruturadas** | ✅ sim | detectar etapa do funil (pdp/cart/checkout/purchase) |
|
|
59
|
-
| **Botões/links clicáveis** | ✅ sim | add_to_cart, begin_checkout (heurística de texto) |
|
|
60
|
-
| **HTML semântico** (JSON-LD, meta, DOM) | ✅ quase sempre | produto, preço, order_id, revenue |
|
|
61
|
-
|
|
62
|
-
**Hierarquia de confiança para cada dado:**
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
Conversão (houve compra?) → URL de confirmação → 100% confiável
|
|
66
|
-
Produto visto → URL /produto + JSON-LD → 100%
|
|
67
|
-
Add to cart → clique em botão (heur.) → ~90%
|
|
68
|
-
Revenue da compra → JSON-LD Order > DOM > meta → best-effort
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
> **dataLayer é tratado como BÔNUS oportunístico** — se existir, enriquece; se
|
|
72
|
-
> não, ignoramos. Nunca é dependência.
|
|
73
|
-
|
|
74
|
-
### A distinção crucial: conversão vs revenue
|
|
75
|
-
|
|
76
|
-
- **Conversão (binária)**: "houve uma compra?" → detectável 100% por URL da
|
|
77
|
-
página de confirmação. Já responde a pergunta de negócio principal.
|
|
78
|
-
- **Revenue (valor exato)**: best-effort via JSON-LD/DOM. Se a loja não expõe,
|
|
79
|
-
fica sem valor — mas a conversão continua contada.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## 3. Arquitetura em camadas
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
mkfashion-sdk (v3)
|
|
87
|
-
│
|
|
88
|
-
├── core/ ← roda em TODA página
|
|
89
|
-
│ ├── identity.js visitorId + sessionId (localStorage/sessionStorage)
|
|
90
|
-
│ ├── transport.js batch + flush + sendBeacon
|
|
91
|
-
│ ├── pageContext.js detecta tipo de página por URL/JSON-LD
|
|
92
|
-
│ ├── autoTrack.js page_view, click, scroll, form, engagement
|
|
93
|
-
│ └── sanitize.js remove PII e campos pesados antes de enviar
|
|
94
|
-
│
|
|
95
|
-
├── ecommerce/ ← NOVO — funil de compra (sinais nativos)
|
|
96
|
-
│ ├── catalog.js view_item_list, select_item, view_item (URL + JSON-LD)
|
|
97
|
-
│ ├── cart.js add_to_cart (heurística), view_cart (URL)
|
|
98
|
-
│ ├── checkout.js begin_checkout (URL/clique), purchase (URL + extração)
|
|
99
|
-
│ └── extractors.js JSON-LD, meta tags, DOM scraping, dataLayer (bônus)
|
|
100
|
-
│
|
|
101
|
-
└── tryon/ ← módulo existente (sem mudança funcional)
|
|
102
|
-
└── open(), isAvailable(), getAvailability(), postMessage bridge
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Carregamento
|
|
106
|
-
|
|
107
|
-
```html
|
|
108
|
-
<!-- Cliente coloca no <head> do TEMA GLOBAL (todas as páginas) -->
|
|
109
|
-
<script src="https://unpkg.com/mkfashion-sdk@3/src/mkfashion.js"
|
|
110
|
-
data-mk-project="698c7c681d3129430f15dddb"
|
|
111
|
-
async></script>
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Single source: a SDK detecta a página, inicia o tracking de core + ecommerce, e
|
|
115
|
-
expõe a API de try-on pra ser chamada na PDP.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## 4. Detecção de contexto de página
|
|
120
|
-
|
|
121
|
-
`core/pageContext.js` classifica a página em cascata (primeiro match vence):
|
|
122
|
-
|
|
123
|
-
```js
|
|
124
|
-
function detectPageType() {
|
|
125
|
-
// 1. Override explícito (se o cliente quiser forçar)
|
|
126
|
-
if (window.__mkPageType) return window.__mkPageType
|
|
127
|
-
|
|
128
|
-
// 2. JSON-LD (@type indica tipo de página)
|
|
129
|
-
const ld = readJsonLd()
|
|
130
|
-
if (ld?.['@type'] === 'Product') return 'pdp'
|
|
131
|
-
if (ld?.['@type'] === 'Order' || ld?.['@type'] === 'Invoice') return 'purchase'
|
|
132
|
-
if (ld?.['@type'] === 'CollectionPage' || ld?.['@type'] === 'SearchResultsPage') return 'category'
|
|
133
|
-
|
|
134
|
-
// 3. URL patterns (fallback universal)
|
|
135
|
-
const p = location.pathname.toLowerCase()
|
|
136
|
-
if (p === '/' || p === '') return 'home'
|
|
137
|
-
if (/\/(thank[_-]?you|orders?\/|pedido[_-]?(confirmado|realizado)|sucesso|order[_-]confirmation)/.test(p)) return 'purchase'
|
|
138
|
-
if (/\/checkout/.test(p)) return 'checkout'
|
|
139
|
-
if (/\/(cart|carrinho|sacola|bag)(\/|$|\?)/.test(p)) return 'cart'
|
|
140
|
-
if (/\/(products?|produtos?|p)\//.test(p)) return 'pdp'
|
|
141
|
-
if (/\/(collections?|categoria|c)\//.test(p)) return 'category'
|
|
142
|
-
if (/\/(search|busca|s)(\/|\?)/.test(p)) return 'search'
|
|
143
|
-
return 'other'
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Cada `page_view` carrega `pageType` no `params`, dando semântica ao funil.
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## 5. Mapa de eventos do funil
|
|
152
|
-
|
|
153
|
-
| Etapa | Evento (collector) | Como detecta (sem GA) | Dados |
|
|
154
|
-
|---|---|---|---|
|
|
155
|
-
| Entrada | `page_view` + pageType | sempre | url, referrer, utm, pageType |
|
|
156
|
-
| Listagem | `page_view_item_list` | pageType=category + JSON-LD ItemList | SKUs visíveis |
|
|
157
|
-
| Seleção | `page_select_item` | clique em card de produto (link `/produto/`) | SKU clicado |
|
|
158
|
-
| Produto | `page_view_item` | pageType=pdp + JSON-LD Product | SKU, preço, nome |
|
|
159
|
-
| Try-on | `page_modal_opened`, `page_generation_complete`, ... | já existe (postMessage) | já existe |
|
|
160
|
-
| Add cart | `page_add_to_cart` | heurística de clique (já temos) + JSON-LD price | SKU, texto botão |
|
|
161
|
-
| Carrinho | `page_view_cart` | pageType=cart | — |
|
|
162
|
-
| Checkout | `page_begin_checkout` | pageType=checkout OU clique "Finalizar" | — |
|
|
163
|
-
| **Compra** | `page_purchase` | **pageType=purchase (URL)** | orderId?, revenue?, items? |
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## 6. Detecção de purchase (o ponto crítico)
|
|
168
|
-
|
|
169
|
-
### 6.1 Conversão (binária) — 100% confiável
|
|
170
|
-
|
|
171
|
-
Ao detectar `pageType === 'purchase'` (via URL/JSON-LD), emite `page_purchase`
|
|
172
|
-
imediatamente. Isso conta a conversão mesmo sem revenue.
|
|
173
|
-
|
|
174
|
-
### 6.2 Revenue (best-effort) — cascata de extractors
|
|
175
|
-
|
|
176
|
-
```js
|
|
177
|
-
function extractOrderData() {
|
|
178
|
-
// 1. JSON-LD Order (existe pro SEO, independente de GA)
|
|
179
|
-
const ld = readJsonLd(['Order', 'Invoice'])
|
|
180
|
-
if (ld) return { orderId: ld.orderNumber || ld.confirmationNumber,
|
|
181
|
-
revenue: ld.price || ld.totalPaymentDue?.price,
|
|
182
|
-
currency: ld.priceCurrency }
|
|
183
|
-
|
|
184
|
-
// 2. dataLayer (BÔNUS — só se a loja tiver GA/GTM)
|
|
185
|
-
const dl = window.dataLayer?.find(x => x.event === 'purchase' && x.ecommerce)
|
|
186
|
-
if (dl) return { orderId: dl.ecommerce.transaction_id,
|
|
187
|
-
revenue: dl.ecommerce.value,
|
|
188
|
-
currency: dl.ecommerce.currency,
|
|
189
|
-
items: dl.ecommerce.items }
|
|
190
|
-
|
|
191
|
-
// 3. Meta tags
|
|
192
|
-
const metaTotal = document.querySelector('meta[property="order:total"]')?.content
|
|
193
|
-
if (metaTotal) return { revenue: parseFloat(metaTotal) }
|
|
194
|
-
|
|
195
|
-
// 4. DOM scraping (fallback frágil — best-effort)
|
|
196
|
-
// procura elementos com classe/texto de total: .order-total, #total, "Total: R$"
|
|
197
|
-
const domTotal = scrapeOrderTotal()
|
|
198
|
-
if (domTotal) return { revenue: domTotal, _source: 'dom_scrape' }
|
|
199
|
-
|
|
200
|
-
return null // conversão já contada; sem revenue
|
|
201
|
-
}
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### 6.3 Dedup
|
|
205
|
-
|
|
206
|
-
Página de confirmação pode recarregar (F5) → mesma compra contada 2x. Dedup por:
|
|
207
|
-
- `orderId` quando disponível (collector ignora purchase com orderId já visto)
|
|
208
|
-
- fallback: 1 purchase por `sessionId` por janela de 1h
|
|
209
|
-
|
|
210
|
-
### 6.4 Opt-in pra precisão (opcional, não obrigatório)
|
|
211
|
-
|
|
212
|
-
Cliente que QUER revenue garantido e não tem JSON-LD pode chamar 1 linha:
|
|
213
|
-
```js
|
|
214
|
-
mkfashion.trackPurchase({ orderId: 'X', revenue: 599.80, currency: 'BRL' })
|
|
215
|
-
```
|
|
216
|
-
Mas é **opcional** — a captura automática cobre a maioria.
|
|
217
|
-
|
|
218
|
-
---
|
|
219
|
-
|
|
220
|
-
## 7. A pergunta de negócio que isso destrava
|
|
221
|
-
|
|
222
|
-
Com `visitorId` consistente em todas as páginas + purchase capturado:
|
|
223
|
-
|
|
224
|
-
```
|
|
225
|
-
Coorte A: visitantes que abriram try-on (page_modal_opened)
|
|
226
|
-
Coorte B: visitantes que NÃO abriram
|
|
227
|
-
|
|
228
|
-
Conversão A = purchases(A) / visitantes(A)
|
|
229
|
-
Conversão B = purchases(B) / visitantes(B)
|
|
230
|
-
|
|
231
|
-
Uplift = Conversão A / Conversão B
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Exemplo de resultado esperado:
|
|
235
|
-
```
|
|
236
|
-
Try-on: 8.2% convertem
|
|
237
|
-
Sem try-on: 3.1% convertem
|
|
238
|
-
Uplift: 2.6x ← argumento de venda do produto MK
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
|
-
## 8. Fases de implementação
|
|
244
|
-
|
|
245
|
-
| Fase | Escopo | Risco |
|
|
246
|
-
|---|---|---|
|
|
247
|
-
| **1** | Refatorar SDK em `core/ ecommerce/ tryon/` sem mudar comportamento | baixo |
|
|
248
|
-
| **2** | `pageContext.js` — detecção de tipo de página | baixo |
|
|
249
|
-
| **3** | `extractors.js` — JSON-LD, meta, DOM, dataLayer(bônus) | médio |
|
|
250
|
-
| **4** | `ecommerce/*` — eventos de funil (view_item, cart, checkout, purchase) | médio |
|
|
251
|
-
| **5** | Backend: novos event types + dedup de purchase + agregação de funil | médio |
|
|
252
|
-
| **6** | Cliente migra snippet pra tema global | baixo (config) |
|
|
253
|
-
| **7** | Dashboard de funil + atribuição try-on→conversão | médio |
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## 9. Mudanças no backend (mk-collector-api)
|
|
258
|
-
|
|
259
|
-
- Novos `event_name` aceitos (validação já é livre — `page_*`).
|
|
260
|
-
- `sdk-aggregator.service.js`: agregar funil por dia:
|
|
261
|
-
```js
|
|
262
|
-
funnel: {
|
|
263
|
-
sessions, viewItem, addToCart, beginCheckout, purchase,
|
|
264
|
-
purchaseRevenue, conversionRate
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
- Dedup de purchase por `orderId` (nova lógica no controller ou aggregator).
|
|
268
|
-
- `sdk_daily_metrics`: campos novos `purchases`, `revenue`, `funnel`.
|
|
269
|
-
- Atribuição try-on: cruzar visitors com `page_modal_opened` × `page_purchase`
|
|
270
|
-
(no aggregator ou query sob demanda).
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## 10. Privacidade / LGPD
|
|
275
|
-
|
|
276
|
-
- **NUNCA** capturar campos de formulário de checkout (email, CPF, endereço, cartão).
|
|
277
|
-
- `core/sanitize.js` filtra chaves sensíveis (`email`, `cpf`, `phone`, `address`,
|
|
278
|
-
`card`, `password`) de qualquer payload antes de enviar.
|
|
279
|
-
- `orderId` e `revenue` não são PII.
|
|
280
|
-
- IP continua sendo descartado server-side (só GeoIP).
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## 11. Riscos e mitigações
|
|
285
|
-
|
|
286
|
-
| Risco | Mitigação |
|
|
287
|
-
|---|---|
|
|
288
|
-
| Loja sem JSON-LD nem dataLayer | Conversão por URL (100%); revenue fica nulo (aceitável) |
|
|
289
|
-
| URL de confirmação atípica | Lista de patterns extensível + override `window.__mkPageType` |
|
|
290
|
-
| Purchase contado 2x (refresh) | Dedup por orderId / sessionId |
|
|
291
|
-
| DOM scraping frágil | Marcado com `_source: 'dom_scrape'` pra saber confiabilidade |
|
|
292
|
-
| Volume 5-10x maior (todas páginas) | Batching + sampling de eventos de baixo valor |
|
|
293
|
-
| PII vazando | sanitize.js + nunca ler campos de form |
|
|
294
|
-
| SPA sem reload entre páginas | history.pushState hook (já temos) re-detecta pageType |
|
|
295
|
-
|
|
296
|
-
---
|
|
297
|
-
|
|
298
|
-
## 12. Decisões em aberto (pra próxima revisão)
|
|
299
|
-
|
|
300
|
-
1. Nome do pacote: manter `mkfashion-sdk` v3 ou renomear? (decidir depois)
|
|
301
|
-
2. Sampling: amostrar `page_scroll_depth`/`page_engagement` em sites de alto volume?
|
|
302
|
-
3. Atribuição try-on→conversão: pré-computar no aggregator ou query sob demanda?
|
|
303
|
-
4. Dashboard de funil: nova aba ou endpoint `/api/export/funnel`?
|
|
304
|
-
|
|
305
|
-
---
|
|
306
|
-
|
|
307
|
-
## 13. Referências
|
|
308
|
-
|
|
309
|
-
- SDK atual: `mkfashion-sdk/src/mkfashion.js` (v2.7.2)
|
|
310
|
-
- Collector: `mk-collector-api/` (rotas `/v1/track/sdk`, agregadores)
|
|
311
|
-
- Spec de eventos GA4 ecommerce (usado só como referência de naming dos eventos)
|
|
1
|
+
# SDK v3 — Tracking de E-commerce Completo (Core Tracker) — Design Spec
|
|
2
|
+
|
|
3
|
+
**Empresa:** MetaKosmos
|
|
4
|
+
**Data:** 2026-05-22
|
|
5
|
+
**Status:** Rascunho para revisão
|
|
6
|
+
**Repos afetados:** `mkfashion-sdk`, `mk-collector-api`
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Contexto e motivação
|
|
11
|
+
|
|
12
|
+
### Situação atual (v2.7.2)
|
|
13
|
+
|
|
14
|
+
A `mkfashion-sdk` roda **apenas na PDP** (página de produto). O cliente chama
|
|
15
|
+
`mkfashion.init({ projectId, identifier })` ou `isAvailable(...)` na PDP, e a
|
|
16
|
+
partir daí a SDK:
|
|
17
|
+
|
|
18
|
+
- Auto-captura `page_view`, `page_click`, `page_scroll_depth`, `page_form_submit`,
|
|
19
|
+
`page_engagement` da PDP.
|
|
20
|
+
- Captura eventos do iframe de try-on (`page_modal_opened`, `page_generation_complete`, etc).
|
|
21
|
+
- Detecta `add_to_cart` por heurística de texto de botão.
|
|
22
|
+
|
|
23
|
+
**Limitação:** só enxergamos a PDP. Não vemos a jornada completa do visitante —
|
|
24
|
+
entrada, navegação por categorias, carrinho, checkout e **compra final**. Sem o
|
|
25
|
+
purchase, não conseguimos responder a pergunta de negócio mais importante:
|
|
26
|
+
|
|
27
|
+
> **"Visitantes que usaram o try-on convertem mais do que os que não usaram?"**
|
|
28
|
+
|
|
29
|
+
### Objetivo da v3
|
|
30
|
+
|
|
31
|
+
Transformar a SDK de "widget de PDP" em **core tracker de e-commerce** que roda
|
|
32
|
+
em **todas as páginas** do site, capturando o funil completo:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
entrada → navegação → produto → (try-on) → carrinho → checkout → COMPRA
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Restrições de design (decididas com o stakeholder):**
|
|
39
|
+
|
|
40
|
+
1. **Zero trabalho extra pro cliente** além de importar o script. Nada de chamar
|
|
41
|
+
métodos novos, nada de configurar dataLayer.
|
|
42
|
+
2. **Zero dependência de ferramentas de terceiros** (GA4/GTM podem não existir
|
|
43
|
+
na loja). Captura por sinais NATIVOS do e-commerce.
|
|
44
|
+
3. **Try-on continua igual**: a marca chama `open()`/`isAvailable()` só na PDP,
|
|
45
|
+
exatamente como hoje.
|
|
46
|
+
4. **Compatibilidade**: clientes na v2 (SDK na PDP) continuam funcionando durante
|
|
47
|
+
a migração.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Princípio central: captura por sinais nativos
|
|
52
|
+
|
|
53
|
+
Toda loja — independente de ter GA4, GTM, ou qualquer analytics — possui 3 coisas
|
|
54
|
+
que existem porque a loja precisa **funcionar e ser indexada**:
|
|
55
|
+
|
|
56
|
+
| Sinal nativo | Sempre existe? | Usado para |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| **URLs estruturadas** | ✅ sim | detectar etapa do funil (pdp/cart/checkout/purchase) |
|
|
59
|
+
| **Botões/links clicáveis** | ✅ sim | add_to_cart, begin_checkout (heurística de texto) |
|
|
60
|
+
| **HTML semântico** (JSON-LD, meta, DOM) | ✅ quase sempre | produto, preço, order_id, revenue |
|
|
61
|
+
|
|
62
|
+
**Hierarquia de confiança para cada dado:**
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Conversão (houve compra?) → URL de confirmação → 100% confiável
|
|
66
|
+
Produto visto → URL /produto + JSON-LD → 100%
|
|
67
|
+
Add to cart → clique em botão (heur.) → ~90%
|
|
68
|
+
Revenue da compra → JSON-LD Order > DOM > meta → best-effort
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **dataLayer é tratado como BÔNUS oportunístico** — se existir, enriquece; se
|
|
72
|
+
> não, ignoramos. Nunca é dependência.
|
|
73
|
+
|
|
74
|
+
### A distinção crucial: conversão vs revenue
|
|
75
|
+
|
|
76
|
+
- **Conversão (binária)**: "houve uma compra?" → detectável 100% por URL da
|
|
77
|
+
página de confirmação. Já responde a pergunta de negócio principal.
|
|
78
|
+
- **Revenue (valor exato)**: best-effort via JSON-LD/DOM. Se a loja não expõe,
|
|
79
|
+
fica sem valor — mas a conversão continua contada.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Arquitetura em camadas
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
mkfashion-sdk (v3)
|
|
87
|
+
│
|
|
88
|
+
├── core/ ← roda em TODA página
|
|
89
|
+
│ ├── identity.js visitorId + sessionId (localStorage/sessionStorage)
|
|
90
|
+
│ ├── transport.js batch + flush + sendBeacon
|
|
91
|
+
│ ├── pageContext.js detecta tipo de página por URL/JSON-LD
|
|
92
|
+
│ ├── autoTrack.js page_view, click, scroll, form, engagement
|
|
93
|
+
│ └── sanitize.js remove PII e campos pesados antes de enviar
|
|
94
|
+
│
|
|
95
|
+
├── ecommerce/ ← NOVO — funil de compra (sinais nativos)
|
|
96
|
+
│ ├── catalog.js view_item_list, select_item, view_item (URL + JSON-LD)
|
|
97
|
+
│ ├── cart.js add_to_cart (heurística), view_cart (URL)
|
|
98
|
+
│ ├── checkout.js begin_checkout (URL/clique), purchase (URL + extração)
|
|
99
|
+
│ └── extractors.js JSON-LD, meta tags, DOM scraping, dataLayer (bônus)
|
|
100
|
+
│
|
|
101
|
+
└── tryon/ ← módulo existente (sem mudança funcional)
|
|
102
|
+
└── open(), isAvailable(), getAvailability(), postMessage bridge
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Carregamento
|
|
106
|
+
|
|
107
|
+
```html
|
|
108
|
+
<!-- Cliente coloca no <head> do TEMA GLOBAL (todas as páginas) -->
|
|
109
|
+
<script src="https://unpkg.com/mkfashion-sdk@3/src/mkfashion.js"
|
|
110
|
+
data-mk-project="698c7c681d3129430f15dddb"
|
|
111
|
+
async></script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Single source: a SDK detecta a página, inicia o tracking de core + ecommerce, e
|
|
115
|
+
expõe a API de try-on pra ser chamada na PDP.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 4. Detecção de contexto de página
|
|
120
|
+
|
|
121
|
+
`core/pageContext.js` classifica a página em cascata (primeiro match vence):
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
function detectPageType() {
|
|
125
|
+
// 1. Override explícito (se o cliente quiser forçar)
|
|
126
|
+
if (window.__mkPageType) return window.__mkPageType
|
|
127
|
+
|
|
128
|
+
// 2. JSON-LD (@type indica tipo de página)
|
|
129
|
+
const ld = readJsonLd()
|
|
130
|
+
if (ld?.['@type'] === 'Product') return 'pdp'
|
|
131
|
+
if (ld?.['@type'] === 'Order' || ld?.['@type'] === 'Invoice') return 'purchase'
|
|
132
|
+
if (ld?.['@type'] === 'CollectionPage' || ld?.['@type'] === 'SearchResultsPage') return 'category'
|
|
133
|
+
|
|
134
|
+
// 3. URL patterns (fallback universal)
|
|
135
|
+
const p = location.pathname.toLowerCase()
|
|
136
|
+
if (p === '/' || p === '') return 'home'
|
|
137
|
+
if (/\/(thank[_-]?you|orders?\/|pedido[_-]?(confirmado|realizado)|sucesso|order[_-]confirmation)/.test(p)) return 'purchase'
|
|
138
|
+
if (/\/checkout/.test(p)) return 'checkout'
|
|
139
|
+
if (/\/(cart|carrinho|sacola|bag)(\/|$|\?)/.test(p)) return 'cart'
|
|
140
|
+
if (/\/(products?|produtos?|p)\//.test(p)) return 'pdp'
|
|
141
|
+
if (/\/(collections?|categoria|c)\//.test(p)) return 'category'
|
|
142
|
+
if (/\/(search|busca|s)(\/|\?)/.test(p)) return 'search'
|
|
143
|
+
return 'other'
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Cada `page_view` carrega `pageType` no `params`, dando semântica ao funil.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 5. Mapa de eventos do funil
|
|
152
|
+
|
|
153
|
+
| Etapa | Evento (collector) | Como detecta (sem GA) | Dados |
|
|
154
|
+
|---|---|---|---|
|
|
155
|
+
| Entrada | `page_view` + pageType | sempre | url, referrer, utm, pageType |
|
|
156
|
+
| Listagem | `page_view_item_list` | pageType=category + JSON-LD ItemList | SKUs visíveis |
|
|
157
|
+
| Seleção | `page_select_item` | clique em card de produto (link `/produto/`) | SKU clicado |
|
|
158
|
+
| Produto | `page_view_item` | pageType=pdp + JSON-LD Product | SKU, preço, nome |
|
|
159
|
+
| Try-on | `page_modal_opened`, `page_generation_complete`, ... | já existe (postMessage) | já existe |
|
|
160
|
+
| Add cart | `page_add_to_cart` | heurística de clique (já temos) + JSON-LD price | SKU, texto botão |
|
|
161
|
+
| Carrinho | `page_view_cart` | pageType=cart | — |
|
|
162
|
+
| Checkout | `page_begin_checkout` | pageType=checkout OU clique "Finalizar" | — |
|
|
163
|
+
| **Compra** | `page_purchase` | **pageType=purchase (URL)** | orderId?, revenue?, items? |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 6. Detecção de purchase (o ponto crítico)
|
|
168
|
+
|
|
169
|
+
### 6.1 Conversão (binária) — 100% confiável
|
|
170
|
+
|
|
171
|
+
Ao detectar `pageType === 'purchase'` (via URL/JSON-LD), emite `page_purchase`
|
|
172
|
+
imediatamente. Isso conta a conversão mesmo sem revenue.
|
|
173
|
+
|
|
174
|
+
### 6.2 Revenue (best-effort) — cascata de extractors
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
function extractOrderData() {
|
|
178
|
+
// 1. JSON-LD Order (existe pro SEO, independente de GA)
|
|
179
|
+
const ld = readJsonLd(['Order', 'Invoice'])
|
|
180
|
+
if (ld) return { orderId: ld.orderNumber || ld.confirmationNumber,
|
|
181
|
+
revenue: ld.price || ld.totalPaymentDue?.price,
|
|
182
|
+
currency: ld.priceCurrency }
|
|
183
|
+
|
|
184
|
+
// 2. dataLayer (BÔNUS — só se a loja tiver GA/GTM)
|
|
185
|
+
const dl = window.dataLayer?.find(x => x.event === 'purchase' && x.ecommerce)
|
|
186
|
+
if (dl) return { orderId: dl.ecommerce.transaction_id,
|
|
187
|
+
revenue: dl.ecommerce.value,
|
|
188
|
+
currency: dl.ecommerce.currency,
|
|
189
|
+
items: dl.ecommerce.items }
|
|
190
|
+
|
|
191
|
+
// 3. Meta tags
|
|
192
|
+
const metaTotal = document.querySelector('meta[property="order:total"]')?.content
|
|
193
|
+
if (metaTotal) return { revenue: parseFloat(metaTotal) }
|
|
194
|
+
|
|
195
|
+
// 4. DOM scraping (fallback frágil — best-effort)
|
|
196
|
+
// procura elementos com classe/texto de total: .order-total, #total, "Total: R$"
|
|
197
|
+
const domTotal = scrapeOrderTotal()
|
|
198
|
+
if (domTotal) return { revenue: domTotal, _source: 'dom_scrape' }
|
|
199
|
+
|
|
200
|
+
return null // conversão já contada; sem revenue
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 6.3 Dedup
|
|
205
|
+
|
|
206
|
+
Página de confirmação pode recarregar (F5) → mesma compra contada 2x. Dedup por:
|
|
207
|
+
- `orderId` quando disponível (collector ignora purchase com orderId já visto)
|
|
208
|
+
- fallback: 1 purchase por `sessionId` por janela de 1h
|
|
209
|
+
|
|
210
|
+
### 6.4 Opt-in pra precisão (opcional, não obrigatório)
|
|
211
|
+
|
|
212
|
+
Cliente que QUER revenue garantido e não tem JSON-LD pode chamar 1 linha:
|
|
213
|
+
```js
|
|
214
|
+
mkfashion.trackPurchase({ orderId: 'X', revenue: 599.80, currency: 'BRL' })
|
|
215
|
+
```
|
|
216
|
+
Mas é **opcional** — a captura automática cobre a maioria.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. A pergunta de negócio que isso destrava
|
|
221
|
+
|
|
222
|
+
Com `visitorId` consistente em todas as páginas + purchase capturado:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
Coorte A: visitantes que abriram try-on (page_modal_opened)
|
|
226
|
+
Coorte B: visitantes que NÃO abriram
|
|
227
|
+
|
|
228
|
+
Conversão A = purchases(A) / visitantes(A)
|
|
229
|
+
Conversão B = purchases(B) / visitantes(B)
|
|
230
|
+
|
|
231
|
+
Uplift = Conversão A / Conversão B
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Exemplo de resultado esperado:
|
|
235
|
+
```
|
|
236
|
+
Try-on: 8.2% convertem
|
|
237
|
+
Sem try-on: 3.1% convertem
|
|
238
|
+
Uplift: 2.6x ← argumento de venda do produto MK
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 8. Fases de implementação
|
|
244
|
+
|
|
245
|
+
| Fase | Escopo | Risco |
|
|
246
|
+
|---|---|---|
|
|
247
|
+
| **1** | Refatorar SDK em `core/ ecommerce/ tryon/` sem mudar comportamento | baixo |
|
|
248
|
+
| **2** | `pageContext.js` — detecção de tipo de página | baixo |
|
|
249
|
+
| **3** | `extractors.js` — JSON-LD, meta, DOM, dataLayer(bônus) | médio |
|
|
250
|
+
| **4** | `ecommerce/*` — eventos de funil (view_item, cart, checkout, purchase) | médio |
|
|
251
|
+
| **5** | Backend: novos event types + dedup de purchase + agregação de funil | médio |
|
|
252
|
+
| **6** | Cliente migra snippet pra tema global | baixo (config) |
|
|
253
|
+
| **7** | Dashboard de funil + atribuição try-on→conversão | médio |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 9. Mudanças no backend (mk-collector-api)
|
|
258
|
+
|
|
259
|
+
- Novos `event_name` aceitos (validação já é livre — `page_*`).
|
|
260
|
+
- `sdk-aggregator.service.js`: agregar funil por dia:
|
|
261
|
+
```js
|
|
262
|
+
funnel: {
|
|
263
|
+
sessions, viewItem, addToCart, beginCheckout, purchase,
|
|
264
|
+
purchaseRevenue, conversionRate
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
- Dedup de purchase por `orderId` (nova lógica no controller ou aggregator).
|
|
268
|
+
- `sdk_daily_metrics`: campos novos `purchases`, `revenue`, `funnel`.
|
|
269
|
+
- Atribuição try-on: cruzar visitors com `page_modal_opened` × `page_purchase`
|
|
270
|
+
(no aggregator ou query sob demanda).
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 10. Privacidade / LGPD
|
|
275
|
+
|
|
276
|
+
- **NUNCA** capturar campos de formulário de checkout (email, CPF, endereço, cartão).
|
|
277
|
+
- `core/sanitize.js` filtra chaves sensíveis (`email`, `cpf`, `phone`, `address`,
|
|
278
|
+
`card`, `password`) de qualquer payload antes de enviar.
|
|
279
|
+
- `orderId` e `revenue` não são PII.
|
|
280
|
+
- IP continua sendo descartado server-side (só GeoIP).
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 11. Riscos e mitigações
|
|
285
|
+
|
|
286
|
+
| Risco | Mitigação |
|
|
287
|
+
|---|---|
|
|
288
|
+
| Loja sem JSON-LD nem dataLayer | Conversão por URL (100%); revenue fica nulo (aceitável) |
|
|
289
|
+
| URL de confirmação atípica | Lista de patterns extensível + override `window.__mkPageType` |
|
|
290
|
+
| Purchase contado 2x (refresh) | Dedup por orderId / sessionId |
|
|
291
|
+
| DOM scraping frágil | Marcado com `_source: 'dom_scrape'` pra saber confiabilidade |
|
|
292
|
+
| Volume 5-10x maior (todas páginas) | Batching + sampling de eventos de baixo valor |
|
|
293
|
+
| PII vazando | sanitize.js + nunca ler campos de form |
|
|
294
|
+
| SPA sem reload entre páginas | history.pushState hook (já temos) re-detecta pageType |
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## 12. Decisões em aberto (pra próxima revisão)
|
|
299
|
+
|
|
300
|
+
1. Nome do pacote: manter `mkfashion-sdk` v3 ou renomear? (decidir depois)
|
|
301
|
+
2. Sampling: amostrar `page_scroll_depth`/`page_engagement` em sites de alto volume?
|
|
302
|
+
3. Atribuição try-on→conversão: pré-computar no aggregator ou query sob demanda?
|
|
303
|
+
4. Dashboard de funil: nova aba ou endpoint `/api/export/funnel`?
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 13. Referências
|
|
308
|
+
|
|
309
|
+
- SDK atual: `mkfashion-sdk/src/mkfashion.js` (v2.7.2)
|
|
310
|
+
- Collector: `mk-collector-api/` (rotas `/v1/track/sdk`, agregadores)
|
|
311
|
+
- Spec de eventos GA4 ecommerce (usado só como referência de naming dos eventos)
|
package/gtm-snippet.js
CHANGED
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mKFashion SDK — Injection Snippet (versão compacta)
|
|
3
|
-
*
|
|
4
|
-
* Auto-detecta o SKU do produto na PDP, carrega o SDK e renderiza o botão.
|
|
5
|
-
* Quando o cliente adiciona ao carrinho dentro do provador, faz POST
|
|
6
|
-
* pra /cart/add.js com tamanho/cor se disponíveis.
|
|
7
|
-
*
|
|
8
|
-
* USO: cole o conteúdo no GTM (Custom HTML tag), no console DevTools de
|
|
9
|
-
* uma PDP, ou converta pra bookmarklet.
|
|
10
|
-
*
|
|
11
|
-
* CONFIG: troque SEU_PROJECT_ID pelo projectId real do mKFashion.
|
|
12
|
-
*/
|
|
13
|
-
(function () {
|
|
14
|
-
// Pode ser sobrescrito setando window.MKFASHION_PROJECT_ID antes deste script
|
|
15
|
-
// (útil pra GTM Variables ou ambientes de teste).
|
|
16
|
-
var PROJECT_ID = window.MKFASHION_PROJECT_ID || 'SEU_PROJECT_ID';
|
|
17
|
-
|
|
18
|
-
if (window.mkfashion) return go();
|
|
19
|
-
var s = document.createElement('script');
|
|
20
|
-
s.src = 'https://unpkg.com/mkfashion-sdk/src/mkfashion.js';
|
|
21
|
-
s.async = true;
|
|
22
|
-
s.onload = go;
|
|
23
|
-
document.head.appendChild(s);
|
|
24
|
-
|
|
25
|
-
// Detecta SKU do produto na PDP, em ordem de prioridade.
|
|
26
|
-
function detect() {
|
|
27
|
-
// 1. Override manual via ?sku= ou ?identifier=
|
|
28
|
-
var qs = new URLSearchParams(location.search);
|
|
29
|
-
if (qs.get('sku')) return qs.get('sku');
|
|
30
|
-
if (qs.get('identifier')) return qs.get('identifier');
|
|
31
|
-
|
|
32
|
-
// 2. Konfidency (Marisa, Renner e outras marcas BR usam esse serviço de reviews)
|
|
33
|
-
if (window.konfidencyData && window.konfidencyData.sku) {
|
|
34
|
-
return String(window.konfidencyData.sku);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 3. dataLayer (GA4 / Universal Analytics Enhanced Ecommerce)
|
|
38
|
-
if (window.dataLayer) {
|
|
39
|
-
for (var i = window.dataLayer.length - 1; i >= 0; i--) {
|
|
40
|
-
var x = (window.dataLayer[i] || {}).ecommerce;
|
|
41
|
-
if (!x) continue;
|
|
42
|
-
var p = (x.detail && x.detail.products && x.detail.products[0]) ||
|
|
43
|
-
(x.items && x.items[0]);
|
|
44
|
-
if (p) return String(p.id || p.item_id);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 4. JSON-LD Schema.org Product
|
|
49
|
-
var lds = document.querySelectorAll('script[type="application/ld+json"]');
|
|
50
|
-
for (var j = 0; j < lds.length; j++) {
|
|
51
|
-
try {
|
|
52
|
-
var items = [].concat(JSON.parse(lds[j].textContent));
|
|
53
|
-
for (var k = 0; k < items.length; k++) {
|
|
54
|
-
var pr = items[k]['@type'] === 'Product' ? items[k] :
|
|
55
|
-
(items[k]['@graph'] || []).find(function (g) { return g['@type'] === 'Product'; });
|
|
56
|
-
if (pr && (pr.sku || pr.productID)) return String(pr.sku || pr.productID);
|
|
57
|
-
}
|
|
58
|
-
} catch (e) {}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function go() {
|
|
65
|
-
var sku = detect();
|
|
66
|
-
if (!sku) {
|
|
67
|
-
console.error('[mKFashion] SKU não detectado. Tente passar ?sku=SEU_SKU na URL.');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
console.log('[mKFashion] SKU:', sku);
|
|
71
|
-
|
|
72
|
-
// Cria container se ainda não existe (overlay fixo no canto como fallback)
|
|
73
|
-
if (!document.getElementById('mkfashion-container')) {
|
|
74
|
-
var c = document.createElement('div');
|
|
75
|
-
c.id = 'mkfashion-container';
|
|
76
|
-
c.style.cssText = 'position:fixed;top:90px;right:20px;z-index:99999;background:#fff;padding:8px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.12)';
|
|
77
|
-
document.body.appendChild(c);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Adiciona ao carrinho da plataforma com tamanho/cor se disponíveis
|
|
81
|
-
mkfashion.addToCart(function (p) {
|
|
82
|
-
console.log('[mKFashion] addToCart:', p);
|
|
83
|
-
var item = { id: p.selectedIdentifier, quantity: 1 };
|
|
84
|
-
var props = {};
|
|
85
|
-
if (p.selectedSize) props['Tamanho'] = p.selectedSize;
|
|
86
|
-
if (p.selectedColor) props['Cor'] = p.selectedColor;
|
|
87
|
-
if (Object.keys(props).length) item.properties = props;
|
|
88
|
-
|
|
89
|
-
// Endpoint Shopify-style. Para VTEX/Wake/Magento, ajuste a URL e o body.
|
|
90
|
-
fetch('/cart/add.js', {
|
|
91
|
-
method: 'POST',
|
|
92
|
-
headers: { 'Content-Type': 'application/json' },
|
|
93
|
-
body: JSON.stringify({ items: [item] })
|
|
94
|
-
})
|
|
95
|
-
.then(function (r) { return r.json(); })
|
|
96
|
-
.then(function (d) { console.log('[mKFashion] no carrinho:', d); })
|
|
97
|
-
.catch(function (e) { console.error('[mKFashion] erro cart:', e); });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
mkfashion.init({
|
|
101
|
-
projectId: PROJECT_ID,
|
|
102
|
-
identifier: sku,
|
|
103
|
-
target: '#mkfashion-container'
|
|
104
|
-
}).then(function (r) {
|
|
105
|
-
// Workaround: depois do init() já validar availability, evita re-fetch no
|
|
106
|
-
// click handler. Sem isso, em sites com monitor SPA (New Relic, etc.) o
|
|
107
|
-
// click intercepta o documento e quebra o fetch com "global scope shutting
|
|
108
|
-
// down". Redundante na SDK 2.5.x+ (cache nativo) — mas inofensivo.
|
|
109
|
-
if (r && r.ok) {
|
|
110
|
-
mkfashion.getAvailability = function () {
|
|
111
|
-
return Promise.resolve({ available: true });
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
})();
|
|
1
|
+
/**
|
|
2
|
+
* mKFashion SDK — Injection Snippet (versão compacta)
|
|
3
|
+
*
|
|
4
|
+
* Auto-detecta o SKU do produto na PDP, carrega o SDK e renderiza o botão.
|
|
5
|
+
* Quando o cliente adiciona ao carrinho dentro do provador, faz POST
|
|
6
|
+
* pra /cart/add.js com tamanho/cor se disponíveis.
|
|
7
|
+
*
|
|
8
|
+
* USO: cole o conteúdo no GTM (Custom HTML tag), no console DevTools de
|
|
9
|
+
* uma PDP, ou converta pra bookmarklet.
|
|
10
|
+
*
|
|
11
|
+
* CONFIG: troque SEU_PROJECT_ID pelo projectId real do mKFashion.
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
// Pode ser sobrescrito setando window.MKFASHION_PROJECT_ID antes deste script
|
|
15
|
+
// (útil pra GTM Variables ou ambientes de teste).
|
|
16
|
+
var PROJECT_ID = window.MKFASHION_PROJECT_ID || 'SEU_PROJECT_ID';
|
|
17
|
+
|
|
18
|
+
if (window.mkfashion) return go();
|
|
19
|
+
var s = document.createElement('script');
|
|
20
|
+
s.src = 'https://unpkg.com/mkfashion-sdk/src/mkfashion.js';
|
|
21
|
+
s.async = true;
|
|
22
|
+
s.onload = go;
|
|
23
|
+
document.head.appendChild(s);
|
|
24
|
+
|
|
25
|
+
// Detecta SKU do produto na PDP, em ordem de prioridade.
|
|
26
|
+
function detect() {
|
|
27
|
+
// 1. Override manual via ?sku= ou ?identifier=
|
|
28
|
+
var qs = new URLSearchParams(location.search);
|
|
29
|
+
if (qs.get('sku')) return qs.get('sku');
|
|
30
|
+
if (qs.get('identifier')) return qs.get('identifier');
|
|
31
|
+
|
|
32
|
+
// 2. Konfidency (Marisa, Renner e outras marcas BR usam esse serviço de reviews)
|
|
33
|
+
if (window.konfidencyData && window.konfidencyData.sku) {
|
|
34
|
+
return String(window.konfidencyData.sku);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. dataLayer (GA4 / Universal Analytics Enhanced Ecommerce)
|
|
38
|
+
if (window.dataLayer) {
|
|
39
|
+
for (var i = window.dataLayer.length - 1; i >= 0; i--) {
|
|
40
|
+
var x = (window.dataLayer[i] || {}).ecommerce;
|
|
41
|
+
if (!x) continue;
|
|
42
|
+
var p = (x.detail && x.detail.products && x.detail.products[0]) ||
|
|
43
|
+
(x.items && x.items[0]);
|
|
44
|
+
if (p) return String(p.id || p.item_id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. JSON-LD Schema.org Product
|
|
49
|
+
var lds = document.querySelectorAll('script[type="application/ld+json"]');
|
|
50
|
+
for (var j = 0; j < lds.length; j++) {
|
|
51
|
+
try {
|
|
52
|
+
var items = [].concat(JSON.parse(lds[j].textContent));
|
|
53
|
+
for (var k = 0; k < items.length; k++) {
|
|
54
|
+
var pr = items[k]['@type'] === 'Product' ? items[k] :
|
|
55
|
+
(items[k]['@graph'] || []).find(function (g) { return g['@type'] === 'Product'; });
|
|
56
|
+
if (pr && (pr.sku || pr.productID)) return String(pr.sku || pr.productID);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function go() {
|
|
65
|
+
var sku = detect();
|
|
66
|
+
if (!sku) {
|
|
67
|
+
console.error('[mKFashion] SKU não detectado. Tente passar ?sku=SEU_SKU na URL.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log('[mKFashion] SKU:', sku);
|
|
71
|
+
|
|
72
|
+
// Cria container se ainda não existe (overlay fixo no canto como fallback)
|
|
73
|
+
if (!document.getElementById('mkfashion-container')) {
|
|
74
|
+
var c = document.createElement('div');
|
|
75
|
+
c.id = 'mkfashion-container';
|
|
76
|
+
c.style.cssText = 'position:fixed;top:90px;right:20px;z-index:99999;background:#fff;padding:8px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.12)';
|
|
77
|
+
document.body.appendChild(c);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Adiciona ao carrinho da plataforma com tamanho/cor se disponíveis
|
|
81
|
+
mkfashion.addToCart(function (p) {
|
|
82
|
+
console.log('[mKFashion] addToCart:', p);
|
|
83
|
+
var item = { id: p.selectedIdentifier, quantity: 1 };
|
|
84
|
+
var props = {};
|
|
85
|
+
if (p.selectedSize) props['Tamanho'] = p.selectedSize;
|
|
86
|
+
if (p.selectedColor) props['Cor'] = p.selectedColor;
|
|
87
|
+
if (Object.keys(props).length) item.properties = props;
|
|
88
|
+
|
|
89
|
+
// Endpoint Shopify-style. Para VTEX/Wake/Magento, ajuste a URL e o body.
|
|
90
|
+
fetch('/cart/add.js', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ items: [item] })
|
|
94
|
+
})
|
|
95
|
+
.then(function (r) { return r.json(); })
|
|
96
|
+
.then(function (d) { console.log('[mKFashion] no carrinho:', d); })
|
|
97
|
+
.catch(function (e) { console.error('[mKFashion] erro cart:', e); });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
mkfashion.init({
|
|
101
|
+
projectId: PROJECT_ID,
|
|
102
|
+
identifier: sku,
|
|
103
|
+
target: '#mkfashion-container'
|
|
104
|
+
}).then(function (r) {
|
|
105
|
+
// Workaround: depois do init() já validar availability, evita re-fetch no
|
|
106
|
+
// click handler. Sem isso, em sites com monitor SPA (New Relic, etc.) o
|
|
107
|
+
// click intercepta o documento e quebra o fetch com "global scope shutting
|
|
108
|
+
// down". Redundante na SDK 2.5.x+ (cache nativo) — mas inofensivo.
|
|
109
|
+
if (r && r.ok) {
|
|
110
|
+
mkfashion.getAvailability = function () {
|
|
111
|
+
return Promise.resolve({ available: true });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
})();
|
package/package.json
CHANGED
package/src/mkfashion.js
CHANGED
|
@@ -21,6 +21,12 @@ const mkfashion = {
|
|
|
21
21
|
apiUrl: 'https://mkfashion-new-api.mk3dlabs.com',
|
|
22
22
|
debug: false,
|
|
23
23
|
|
|
24
|
+
// GA4 do mkfashion (mesmo ID do visualizer/index.html). Usado pra disparar
|
|
25
|
+
// experience_clicked, que ocorre ANTES do iframe carregar — sem isto, esse evento
|
|
26
|
+
// cairia apenas no GA da loja (se houver), nao no nosso.
|
|
27
|
+
_MK_GA_ID: 'G-LPV8HY4JPQ',
|
|
28
|
+
_gaInitialized: false,
|
|
29
|
+
|
|
24
30
|
// STAGING - ativado via ?mkStaging=true na URL da página (ver bloco no final)
|
|
25
31
|
stagingAppUrl: 'https://mkfashion.metakosmoslab.com/visualizer',
|
|
26
32
|
stagingApiUrl: 'https://mkfashion-api.metakosmoslab.com',
|
|
@@ -43,6 +49,8 @@ const mkfashion = {
|
|
|
43
49
|
_messageHandler: null,
|
|
44
50
|
_confirmingExit: false,
|
|
45
51
|
_projectIdOverride: null, // sobrescreve projectId via ?mkProjectId= (QA/teste)
|
|
52
|
+
_orientation: 'portrait', // 'portrait' | 'landscape' (apenas desktop usa resize via SDK)
|
|
53
|
+
_portraitDimensions: null, // { width, height } salvos pra restaurar ao voltar de landscape
|
|
46
54
|
|
|
47
55
|
_callbacks: {
|
|
48
56
|
onReady: null,
|
|
@@ -85,6 +93,11 @@ const mkfashion = {
|
|
|
85
93
|
return
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
// Track de intencao ANTES do availability check: captura todo clique no botao
|
|
97
|
+
// (inclusive em produtos indisponiveis). Complementa o experience_started, que so
|
|
98
|
+
// dispara quando o iframe carrega — a diferenca entre os dois mostra o drop-off.
|
|
99
|
+
this._trackExperienceClicked(projectId, identifier)
|
|
100
|
+
|
|
88
101
|
try {
|
|
89
102
|
const availability = await this.getAvailability(projectId, identifier)
|
|
90
103
|
if (!availability.available) {
|
|
@@ -399,6 +412,52 @@ const mkfashion = {
|
|
|
399
412
|
|
|
400
413
|
// ============ METODOS PRIVADOS ============
|
|
401
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Garante o gtag.js do mkfashion no contexto da loja (idempotente).
|
|
417
|
+
* Se a loja ja tem gtag proprio, reusa window.gtag e so adiciona NOSSO id via config
|
|
418
|
+
* com send_page_view:false (nao infla pageviews da loja). Eventos usam send_to pra
|
|
419
|
+
* nao vazar pro GA da loja.
|
|
420
|
+
*/
|
|
421
|
+
_ensureGa() {
|
|
422
|
+
if (this._gaInitialized || typeof window === 'undefined') return
|
|
423
|
+
|
|
424
|
+
if (typeof window.gtag !== 'function') {
|
|
425
|
+
const script = document.createElement('script')
|
|
426
|
+
script.async = true
|
|
427
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${this._MK_GA_ID}`
|
|
428
|
+
document.head.appendChild(script)
|
|
429
|
+
window.dataLayer = window.dataLayer || []
|
|
430
|
+
window.gtag = function () { window.dataLayer.push(arguments) }
|
|
431
|
+
window.gtag('js', new Date())
|
|
432
|
+
}
|
|
433
|
+
window.gtag('config', this._MK_GA_ID, { send_page_view: false })
|
|
434
|
+
this._gaInitialized = true
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/** Dispara experience_clicked no GA4 do mkfashion + callback onInteraction da loja. */
|
|
438
|
+
_trackExperienceClicked(projectId, identifier) {
|
|
439
|
+
try {
|
|
440
|
+
this._ensureGa()
|
|
441
|
+
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
|
442
|
+
window.gtag('event', 'experience_clicked', {
|
|
443
|
+
send_to: this._MK_GA_ID,
|
|
444
|
+
project_id: projectId || 'unknown',
|
|
445
|
+
product_id: identifier || 'unknown'
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error('[mKFashion] Erro ao registrar experience_clicked:', error)
|
|
450
|
+
}
|
|
451
|
+
this._triggerCallback('onInteraction', {
|
|
452
|
+
category: 'experience',
|
|
453
|
+
action: 'click',
|
|
454
|
+
projectId,
|
|
455
|
+
identifier,
|
|
456
|
+
product_id: identifier || 'unknown',
|
|
457
|
+
project_id: projectId || 'unknown'
|
|
458
|
+
})
|
|
459
|
+
},
|
|
460
|
+
|
|
402
461
|
_log(message, data = null) {
|
|
403
462
|
if (this.debug) {
|
|
404
463
|
if (data) {
|
|
@@ -584,6 +643,46 @@ const mkfashion = {
|
|
|
584
643
|
)
|
|
585
644
|
},
|
|
586
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Desktop: alterna o iframe/modal entre portrait e um formato widescreen (landscape).
|
|
648
|
+
* Em landscape usa ~90vw x 9/16, limitado, centralizado. Idempotente.
|
|
649
|
+
* Mobile NAO usa isto — la a rotacao eh fisica (fullscreen + orientation.lock no iframe).
|
|
650
|
+
*/
|
|
651
|
+
_setIframeOrientation(orientation) {
|
|
652
|
+
if (this._orientation === orientation) return
|
|
653
|
+
if (!this._container || !this._iframe) return
|
|
654
|
+
|
|
655
|
+
if (orientation === 'landscape') {
|
|
656
|
+
if (!this._portraitDimensions) {
|
|
657
|
+
this._portraitDimensions = {
|
|
658
|
+
width: this._container.style.width || `${this._config?.width || 430}px`,
|
|
659
|
+
height: this._container.style.height || `${this._config?.height || 800}px`
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Widescreen: ocupa boa parte da viewport mantendo 16:9, sem estourar a tela.
|
|
663
|
+
const w = Math.min(window.innerWidth * 0.92, 960)
|
|
664
|
+
const h = Math.min(w * 9 / 16, window.innerHeight * 0.92)
|
|
665
|
+
this._applyContainerSize(`${Math.round(w)}px`, `${Math.round(h)}px`)
|
|
666
|
+
} else {
|
|
667
|
+
const dims = this._portraitDimensions || { width: '430px', height: '800px' }
|
|
668
|
+
this._applyContainerSize(dims.width, dims.height)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
this._orientation = orientation
|
|
672
|
+
this._log(`Orientacao do iframe: ${orientation}`)
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
_applyContainerSize(width, height) {
|
|
676
|
+
if (this._container) {
|
|
677
|
+
this._container.style.width = width
|
|
678
|
+
this._container.style.height = height
|
|
679
|
+
}
|
|
680
|
+
if (this._iframe) {
|
|
681
|
+
this._iframe.style.width = width
|
|
682
|
+
this._iframe.style.height = height
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
|
|
587
686
|
/** Esconde o modal sem remover do DOM (preserva sessao do iframe) */
|
|
588
687
|
_hideModal() {
|
|
589
688
|
if (!this._modal) return
|
|
@@ -637,6 +736,8 @@ const mkfashion = {
|
|
|
637
736
|
this._container = null
|
|
638
737
|
this._lastConfig = null
|
|
639
738
|
this._confirmingExit = false
|
|
739
|
+
this._orientation = 'portrait'
|
|
740
|
+
this._portraitDimensions = null
|
|
640
741
|
|
|
641
742
|
if (this._escHandler) {
|
|
642
743
|
document.removeEventListener('keydown', this._escHandler)
|
|
@@ -650,7 +751,10 @@ const mkfashion = {
|
|
|
650
751
|
iframe.style.width = typeof width === 'number' ? `${width}px` : width
|
|
651
752
|
iframe.style.height = typeof height === 'number' ? `${height}px` : height
|
|
652
753
|
iframe.style.border = 'none'
|
|
653
|
-
|
|
754
|
+
// fullscreen necessario pro "Ver na horizontal" do fluxo de moveis em mobile:
|
|
755
|
+
// o visualizer chama requestFullscreen + screen.orientation.lock de dentro do iframe
|
|
756
|
+
// (aproveitando o gesto do clique local) pra girar a tela no Android.
|
|
757
|
+
iframe.setAttribute('allow', 'camera *; microphone *; web-share *; fullscreen *')
|
|
654
758
|
iframe.setAttribute('allowfullscreen', 'true')
|
|
655
759
|
return iframe
|
|
656
760
|
},
|
|
@@ -679,6 +783,14 @@ const mkfashion = {
|
|
|
679
783
|
this.restart()
|
|
680
784
|
break
|
|
681
785
|
|
|
786
|
+
case 'request_orientation':
|
|
787
|
+
// Usado pelo fluxo de moveis (furniture) no DESKTOP, onde girar a tela nao faz
|
|
788
|
+
// sentido: o SDK so redimensiona o iframe/modal pra um formato widescreen.
|
|
789
|
+
// Em mobile a rotacao acontece dentro do iframe (fullscreen + orientation.lock),
|
|
790
|
+
// entao mobile nao manda esta mensagem.
|
|
791
|
+
this._setIframeOrientation(data === 'landscape' ? 'landscape' : 'portrait')
|
|
792
|
+
break
|
|
793
|
+
|
|
682
794
|
case 'ready':
|
|
683
795
|
this._log('App pronta', data)
|
|
684
796
|
this._triggerCallback('onReady', data)
|