type Component = { id: string name: string type: string } export type TrackingData = { component: Component } /** * CustomCodeComponentTracker * @description Track initial render of a custom code component * @param {HTMLElement} mountElement - The custom code component * @param {TrackingData} trackingData - The specific component data */ class CustomCodeComponentTracker { private mutationObserver?: MutationObserver private intersectionObserver?: IntersectionObserver private viewing?: boolean constructor( private component: HTMLElement, private trackingData: TrackingData ) { this.component = component this.trackingData = trackingData // we're using a mutation observer to watch for changes to the component rather than event listeners // as dynamic imports in the app are run before the tracking client can be instantiated on second // and subsequent page visits in a session // the observers need to instantiated here so that they can be accessed correctly in the destroy method // called by the app this.mutationObserver = new MutationObserver( this.mutationCallback.bind(this) ) this.mutationObserver.observe(this.component, { attributes: true, attributeFilter: ['data-ccc-ready', 'data-ccc-error'], }) this.intersectionObserver = new IntersectionObserver( this.onChange.bind(this), { rootMargin: `0px 0px -300px 0px`, threshold: 0, } ) this.intersectionObserver.observe(this.component) this.viewing = false this.destroy = this.destroy.bind(this) this.init() } private init() { this.dispatchEvent('mount') if (this.component.dataset.cccError) { this.onCccError() } if (this.component.dataset.cccReady) { this.onCccConnected() } this.component.dataset.initialised = 'true' } private mutationCallback(mutationsList: MutationRecord[]): void { for (const mutation of mutationsList) { if (mutation.attributeName === 'data-ccc-ready') { this.onCccConnected() } if (mutation.attributeName === 'data-ccc-error') { this.onCccError() } } } private onCccConnected(): void { this.dispatchEvent('act', { trigger_action: 'success' }) } private onCccError(): void { if (this.component.dataset.cccError) { this.dispatchEvent('act', { trigger_action: `error: ${this.component.dataset.cccError}`, }) } } private onView(): void { this.dispatchEvent('view') this.viewing = true } private onExitView(): void { this.dispatchEvent('act', { trigger_action: 'exit-view' }) this.viewing = false } private dispatchEvent(action: string, extraDetail = {}): void { const event = new CustomEvent('oTracking.event', { detail: { category: 'component', action, ...this.trackingData, ...extraDetail, }, bubbles: true, }) document.body.dispatchEvent(event) } private onChange(changes: IntersectionObserverEntry[]): void { changes.forEach((change) => { if (change.target !== this.component) { return } if ( change.boundingClientRect.height > 0 && (change.isIntersecting || change.intersectionRatio >= 1) ) { this.onView() } if ( this.viewing && (!change.isIntersecting || change.intersectionRatio === 0) ) { this.onExitView() } }) } destroy(): void { if (!this.component) return this.component.removeAttribute('data-initialised') this.mutationObserver?.disconnect() this.intersectionObserver?.unobserve(this.component) this.intersectionObserver?.disconnect() this.viewing && this.onExitView() } } export { CustomCodeComponentTracker }