mkfashion-sdk 2.5.0 → 2.7.1
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/mkfashion-sdk-2.7.0.tgz +0 -0
- package/mkfashion-sdk-2.7.1.tgz +0 -0
- package/package.json +2 -2
- package/src/mkfashion.js +308 -40
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkfashion-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce e tracking analytics",
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce e tracking analytics completo",
|
|
5
5
|
"main": "src/mkfashion.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Abra test.html no navegador\""
|
package/src/mkfashion.js
CHANGED
|
@@ -304,31 +304,24 @@ const mkfashion = {
|
|
|
304
304
|
const data = await response.json()
|
|
305
305
|
this._log('Disponibilidade recebida', data)
|
|
306
306
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
307
|
+
// NOTA: availability check é decisão interna (mostra botão ou não) e
|
|
308
|
+
// NÃO emite evento pro collector — não traz valor analítico.
|
|
309
|
+
//
|
|
310
|
+
// MAS é o gatilho mais comum em PDPs: a partir daqui temos projectId
|
|
311
|
+
// confirmado e ativamos o auto-tracking (page_view, click, scroll,
|
|
312
|
+
// form, engagement). Sem isso, páginas onde o usuário só visualiza
|
|
313
|
+
// (sem clicar no try-on) ficariam invisíveis no collector.
|
|
309
314
|
try {
|
|
310
315
|
if (!this._config) this._config = { projectId: resolved, identifier }
|
|
311
|
-
this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
available: data.available === true,
|
|
315
|
-
reason: data.reason || data.message || null
|
|
316
|
-
})
|
|
316
|
+
if (!this._autoTrackInitialized && this.autoTrack !== false) {
|
|
317
|
+
this._initAutoTrack()
|
|
318
|
+
}
|
|
317
319
|
} catch (_) {}
|
|
318
320
|
|
|
319
321
|
return data
|
|
320
322
|
|
|
321
323
|
} catch (error) {
|
|
322
324
|
console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
|
|
323
|
-
// Tracking: também emite erros de availability — útil pra detectar 404/500 em prod.
|
|
324
|
-
try {
|
|
325
|
-
if (!this._config) this._config = { projectId: resolved, identifier }
|
|
326
|
-
this._emitToCollector('availability_error', {
|
|
327
|
-
projectId: resolved,
|
|
328
|
-
identifier,
|
|
329
|
-
error: String(error.message || error).slice(0, 200)
|
|
330
|
-
})
|
|
331
|
-
} catch (_) {}
|
|
332
325
|
throw error
|
|
333
326
|
}
|
|
334
327
|
},
|
|
@@ -621,42 +614,317 @@ const mkfashion = {
|
|
|
621
614
|
},
|
|
622
615
|
|
|
623
616
|
/**
|
|
624
|
-
*
|
|
625
|
-
*
|
|
617
|
+
* Enfileira evento no batch buffer. O flush real acontece em _flushQueue.
|
|
618
|
+
* Fire-and-forget — nunca atrapalha UX.
|
|
626
619
|
*/
|
|
627
620
|
_emitToCollector(eventName, data) {
|
|
628
621
|
try {
|
|
629
|
-
if (!this._config || !this._config.projectId)
|
|
622
|
+
if (!this._config || !this._config.projectId) {
|
|
623
|
+
// Antes do projectId estar setado, enfileira pra flush quando estiver.
|
|
624
|
+
this._preConfigQueue = this._preConfigQueue || []
|
|
625
|
+
if (this._preConfigQueue.length < 50) {
|
|
626
|
+
this._preConfigQueue.push({ eventName, data, ts: Date.now() })
|
|
627
|
+
}
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
630
|
if (!this.collectorUrl) return
|
|
631
631
|
|
|
632
|
-
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
632
|
+
// Drena qualquer evento que foi enfileirado antes do config existir.
|
|
633
|
+
if (this._preConfigQueue && this._preConfigQueue.length > 0) {
|
|
634
|
+
const pending = this._preConfigQueue
|
|
635
|
+
this._preConfigQueue = []
|
|
636
|
+
for (const p of pending) this._queueEvent(p.eventName, p.data, p.ts)
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
|
|
640
|
-
visitorId: this._getVisitorId(),
|
|
641
|
-
sessionId: this._getSessionId(),
|
|
642
|
-
events: [{ name: 'sdk_' + eventName, ts: Date.now(), params: params }],
|
|
643
|
-
site: this._detectSiteContext()
|
|
644
|
-
})
|
|
639
|
+
this._queueEvent(eventName, data)
|
|
645
640
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
'Content-Type': 'application/json',
|
|
651
|
-
'X-MK-Project-Id': this._config.projectId
|
|
652
|
-
},
|
|
653
|
-
body: payload
|
|
654
|
-
}).catch(() => {})
|
|
641
|
+
// Auto-start tracking de site no primeiro emit com config.
|
|
642
|
+
if (!this._autoTrackInitialized && this.autoTrack !== false) {
|
|
643
|
+
this._initAutoTrack()
|
|
644
|
+
}
|
|
655
645
|
} catch (e) {
|
|
656
646
|
if (this.debug) console.warn('[mKFashion tracking]', e.message)
|
|
657
647
|
}
|
|
658
648
|
},
|
|
659
649
|
|
|
650
|
+
/**
|
|
651
|
+
* Normaliza nome de evento — tira prefixo "on" e converte camelCase pra snake_case.
|
|
652
|
+
* Ex: 'onAddToCart' → 'add_to_cart'; 'modal_opened' → 'modal_opened' (já snake).
|
|
653
|
+
*/
|
|
654
|
+
_normalizeEventName(name) {
|
|
655
|
+
if (typeof name !== 'string' || !name) return 'unknown'
|
|
656
|
+
var n = name.replace(/^on/, '')
|
|
657
|
+
n = n.replace(/[A-Z]/g, function (m, i) { return (i > 0 ? '_' : '') + m.toLowerCase() })
|
|
658
|
+
if (n.charAt(0) >= 'A' && n.charAt(0) <= 'Z') n = n.toLowerCase()
|
|
659
|
+
return n
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Adiciona ao buffer de batch. Flush é disparado por timer ou tamanho.
|
|
664
|
+
* Todos os eventos recebem prefixo 'page_'.
|
|
665
|
+
*/
|
|
666
|
+
_queueEvent(eventName, data, tsOverride) {
|
|
667
|
+
this._eventQueue = this._eventQueue || []
|
|
668
|
+
const params = { identifier: this._config.identifier || null }
|
|
669
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
670
|
+
Object.assign(params, data)
|
|
671
|
+
} else if (data !== null && data !== undefined) {
|
|
672
|
+
params.value = data
|
|
673
|
+
}
|
|
674
|
+
this._eventQueue.push({
|
|
675
|
+
name: 'page_' + this._normalizeEventName(eventName),
|
|
676
|
+
ts: tsOverride || Date.now(),
|
|
677
|
+
params: params
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
// Flush sincrono se atingiu limite de tamanho.
|
|
681
|
+
if (this._eventQueue.length >= 20) {
|
|
682
|
+
this._flushQueue()
|
|
683
|
+
return
|
|
684
|
+
}
|
|
685
|
+
// Senão, agenda flush em 2s.
|
|
686
|
+
if (!this._flushTimer) {
|
|
687
|
+
this._flushTimer = setTimeout(() => { this._flushTimer = null; this._flushQueue() }, 2000)
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
|
|
691
|
+
_flushQueue(useBeacon) {
|
|
692
|
+
if (!this._eventQueue || this._eventQueue.length === 0) return
|
|
693
|
+
if (!this._config || !this._config.projectId) return
|
|
694
|
+
if (!this.collectorUrl) return
|
|
695
|
+
|
|
696
|
+
const events = this._eventQueue.splice(0, 50) // máximo 50 por POST
|
|
697
|
+
const payload = JSON.stringify({
|
|
698
|
+
visitorId: this._getVisitorId(),
|
|
699
|
+
sessionId: this._getSessionId(),
|
|
700
|
+
events: events,
|
|
701
|
+
site: this._detectSiteContext()
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
const url = this.collectorUrl + '/v1/track/sdk'
|
|
705
|
+
const headers = {
|
|
706
|
+
'Content-Type': 'application/json',
|
|
707
|
+
'X-MK-Project-Id': this._config.projectId
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// sendBeacon no pagehide pra garantir envio durante unload.
|
|
711
|
+
if (useBeacon && navigator.sendBeacon) {
|
|
712
|
+
try {
|
|
713
|
+
const blob = new Blob([payload], { type: 'application/json' })
|
|
714
|
+
// sendBeacon nao suporta headers custom — usamos query param.
|
|
715
|
+
navigator.sendBeacon(url + '?projectId=' + encodeURIComponent(this._config.projectId), blob)
|
|
716
|
+
return
|
|
717
|
+
} catch (_) {}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
fetch(url, { method: 'POST', keepalive: true, headers: headers, body: payload })
|
|
721
|
+
.catch(() => {})
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
// ============ AUTO-TRACK (pageview/click/scroll/form/engagement) ============
|
|
725
|
+
|
|
726
|
+
_autoTrackInitialized: false,
|
|
727
|
+
_scrollDepthSeen: null,
|
|
728
|
+
_engagementTimer: null,
|
|
729
|
+
_engagementLastTick: 0,
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Inicia captura automática de comportamento no site host. Chamado quando
|
|
733
|
+
* _config recebe um projectId, OU manualmente via mkfashion.startTracking().
|
|
734
|
+
* Setar mkfashion.autoTrack = false antes do init pra desabilitar.
|
|
735
|
+
*/
|
|
736
|
+
_initAutoTrack() {
|
|
737
|
+
if (this._autoTrackInitialized) return
|
|
738
|
+
this._autoTrackInitialized = true
|
|
739
|
+
try {
|
|
740
|
+
this._trackPageview()
|
|
741
|
+
this._initClickTracking()
|
|
742
|
+
this._initScrollTracking()
|
|
743
|
+
this._initFormTracking()
|
|
744
|
+
this._initEngagementTracking()
|
|
745
|
+
this._initSpaNavigation()
|
|
746
|
+
this._initFlushOnUnload()
|
|
747
|
+
this._log('Auto-tracking iniciado')
|
|
748
|
+
} catch (e) {
|
|
749
|
+
if (this.debug) console.warn('[mKFashion auto-track init]', e.message)
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* API pública pra ativar auto-tracking sem precisar abrir o modal.
|
|
755
|
+
* Útil em sites onde a SDK só renderiza botão (sem getAvailability) ou
|
|
756
|
+
* em landing pages sem produto definido.
|
|
757
|
+
*/
|
|
758
|
+
startTracking(projectId, identifier) {
|
|
759
|
+
if (!projectId) {
|
|
760
|
+
console.error('[mKFashion] startTracking: projectId obrigatorio')
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
const resolved = this._resolveProjectId(projectId)
|
|
764
|
+
if (!this._config) this._config = { projectId: resolved, identifier: identifier || null }
|
|
765
|
+
this._initAutoTrack()
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
_trackPageview() {
|
|
769
|
+
this._scrollDepthSeen = {}
|
|
770
|
+
// 'view' aqui vira 'page_view' (prefix 'page_' é adicionado em _queueEvent).
|
|
771
|
+
this._emitToCollector('view', {
|
|
772
|
+
url: location.href,
|
|
773
|
+
pathname: location.pathname,
|
|
774
|
+
referrer: document.referrer || null,
|
|
775
|
+
title: document.title || null
|
|
776
|
+
})
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
_initClickTracking() {
|
|
780
|
+
if (!document.body) return
|
|
781
|
+
const INTERACTIVE = { A: 1, BUTTON: 1, INPUT: 1, SELECT: 1, TEXTAREA: 1 }
|
|
782
|
+
let lastClick = 0
|
|
783
|
+
document.body.addEventListener('click', (e) => {
|
|
784
|
+
try {
|
|
785
|
+
const now = Date.now()
|
|
786
|
+
if (now - lastClick < 100) return
|
|
787
|
+
lastClick = now
|
|
788
|
+
let el = e.target
|
|
789
|
+
for (let i = 0; i < 5 && el && el !== document.body; i++) {
|
|
790
|
+
if (INTERACTIVE[el.tagName] || (el.getAttribute && el.getAttribute('role') === 'button')) break
|
|
791
|
+
el = el.parentElement
|
|
792
|
+
}
|
|
793
|
+
if (!el) return
|
|
794
|
+
if (!INTERACTIVE[el.tagName] && !(el.getAttribute && el.getAttribute('role') === 'button')) return
|
|
795
|
+
|
|
796
|
+
const data = { tag: el.tagName }
|
|
797
|
+
const t = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' ? el.value : el.textContent
|
|
798
|
+
data.text = (t || '').replace(/\s+/g, ' ').trim().substring(0, 100)
|
|
799
|
+
if (el.href) data.href = String(el.href).substring(0, 500)
|
|
800
|
+
if (el.id) data.id = el.id
|
|
801
|
+
if (el.className && typeof el.className === 'string') {
|
|
802
|
+
data.classes = el.className.trim().substring(0, 200)
|
|
803
|
+
}
|
|
804
|
+
this._emitToCollector('click', data)
|
|
805
|
+
} catch (_) {}
|
|
806
|
+
}, { capture: true, passive: true })
|
|
807
|
+
},
|
|
808
|
+
|
|
809
|
+
_initScrollTracking() {
|
|
810
|
+
const TH = [25, 50, 75, 100]
|
|
811
|
+
let scrollTimer = null
|
|
812
|
+
const check = () => {
|
|
813
|
+
try {
|
|
814
|
+
const sy = window.scrollY || document.documentElement.scrollTop || 0
|
|
815
|
+
const dh = Math.max(
|
|
816
|
+
document.body && document.body.scrollHeight || 0,
|
|
817
|
+
document.documentElement && document.documentElement.scrollHeight || 0
|
|
818
|
+
)
|
|
819
|
+
if (dh <= 0) return
|
|
820
|
+
const p = Math.min(100, Math.floor((sy + window.innerHeight) / dh * 100))
|
|
821
|
+
if (!this._scrollDepthSeen) this._scrollDepthSeen = {}
|
|
822
|
+
for (const t of TH) {
|
|
823
|
+
if (p >= t && !this._scrollDepthSeen[t]) {
|
|
824
|
+
this._scrollDepthSeen[t] = true
|
|
825
|
+
this._emitToCollector('scroll_depth', { depth: t })
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch (_) {}
|
|
829
|
+
}
|
|
830
|
+
check()
|
|
831
|
+
window.addEventListener('scroll', () => {
|
|
832
|
+
if (scrollTimer) return
|
|
833
|
+
scrollTimer = setTimeout(() => { scrollTimer = null; check() }, 250)
|
|
834
|
+
}, { passive: true })
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
_initFormTracking() {
|
|
838
|
+
if (!document.body) return
|
|
839
|
+
const CC = ['wpcf7-form', 'wpforms-form', 'gform_wrapper', 'formidable']
|
|
840
|
+
const NP = ['subscribe', 'newsletter', 'mailchimp', 'convertkit']
|
|
841
|
+
document.body.addEventListener('submit', (e) => {
|
|
842
|
+
try {
|
|
843
|
+
const f = e.target
|
|
844
|
+
if (!f || f.tagName !== 'FORM') return
|
|
845
|
+
// Skip WooCommerce checkout (capturado por outro caminho)
|
|
846
|
+
if (f.classList && (f.classList.contains('woocommerce-checkout') ||
|
|
847
|
+
f.classList.contains('wc-block-checkout__form'))) return
|
|
848
|
+
|
|
849
|
+
let ft = 'other'
|
|
850
|
+
const cls = f.className || ''
|
|
851
|
+
const act = (f.action || '').toLowerCase()
|
|
852
|
+
if (f.querySelector('input[type="search"]') || f.querySelector('input[name="s"]')) ft = 'search'
|
|
853
|
+
else {
|
|
854
|
+
for (const c of CC) if (cls.indexOf(c) !== -1) { ft = 'contact'; break }
|
|
855
|
+
if (ft === 'other') for (const n of NP) if (act.indexOf(n) !== -1) { ft = 'newsletter'; break }
|
|
856
|
+
}
|
|
857
|
+
this._emitToCollector('form_submit', {
|
|
858
|
+
form_id: f.id || '',
|
|
859
|
+
form_action: (f.action || '').substring(0, 300),
|
|
860
|
+
form_type: ft
|
|
861
|
+
})
|
|
862
|
+
} catch (_) {}
|
|
863
|
+
}, { capture: true })
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
_initEngagementTracking() {
|
|
867
|
+
const INTERVAL = 15000
|
|
868
|
+
this._engagementLastTick = Date.now()
|
|
869
|
+
this._engagementTimer = setInterval(() => {
|
|
870
|
+
if (document.hidden) return
|
|
871
|
+
const now = Date.now()
|
|
872
|
+
const elapsedSec = Math.round((now - this._engagementLastTick) / 1000)
|
|
873
|
+
this._engagementLastTick = now
|
|
874
|
+
if (elapsedSec > 0 && elapsedSec < 60) {
|
|
875
|
+
this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
|
|
876
|
+
}
|
|
877
|
+
}, INTERVAL)
|
|
878
|
+
|
|
879
|
+
document.addEventListener('visibilitychange', () => {
|
|
880
|
+
if (document.hidden) {
|
|
881
|
+
const elapsedSec = Math.round((Date.now() - this._engagementLastTick) / 1000)
|
|
882
|
+
if (elapsedSec > 0 && elapsedSec < 600) {
|
|
883
|
+
this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
|
|
884
|
+
}
|
|
885
|
+
this._engagementLastTick = Date.now()
|
|
886
|
+
} else {
|
|
887
|
+
this._engagementLastTick = Date.now()
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
_initSpaNavigation() {
|
|
893
|
+
// Captura SPA navigation (React/Vue/Next/etc) emitindo page_view a cada
|
|
894
|
+
// mudança de URL via history API.
|
|
895
|
+
let lastUrl = location.href
|
|
896
|
+
const self = this
|
|
897
|
+
const onChange = () => {
|
|
898
|
+
if (location.href !== lastUrl) {
|
|
899
|
+
lastUrl = location.href
|
|
900
|
+
self._trackPageview()
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
const origPush = history.pushState
|
|
905
|
+
const origReplace = history.replaceState
|
|
906
|
+
history.pushState = function () { origPush.apply(this, arguments); setTimeout(onChange, 0) }
|
|
907
|
+
history.replaceState = function () { origReplace.apply(this, arguments); setTimeout(onChange, 0) }
|
|
908
|
+
window.addEventListener('popstate', onChange)
|
|
909
|
+
} catch (_) {}
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
_initFlushOnUnload() {
|
|
913
|
+
// Flush sincrono no pagehide pra não perder engagement final + qualquer evento pendente.
|
|
914
|
+
window.addEventListener('pagehide', () => {
|
|
915
|
+
try {
|
|
916
|
+
const elapsedSec = Math.round((Date.now() - this._engagementLastTick) / 1000)
|
|
917
|
+
if (elapsedSec > 0 && elapsedSec < 600) {
|
|
918
|
+
this._emitToCollector('engagement', { url: location.pathname, dwellSec: elapsedSec })
|
|
919
|
+
}
|
|
920
|
+
this._flushQueue(true)
|
|
921
|
+
} catch (_) {}
|
|
922
|
+
})
|
|
923
|
+
window.addEventListener('beforeunload', () => {
|
|
924
|
+
try { this._flushQueue(true) } catch (_) {}
|
|
925
|
+
})
|
|
926
|
+
},
|
|
927
|
+
|
|
660
928
|
_buildUrl() {
|
|
661
929
|
const encoded = encodeURIComponent(this._config.identifier).replace(/\./g, '%2E')
|
|
662
930
|
const url = `${this.appUrl}/${this._config.projectId}/${encoded}`
|