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