-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
1 parent
039ac0c
commit f884d4d
Showing
2 changed files
with
180 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,181 @@ | ||
import { pushToDataLayer } from '../../lib/gtm' | ||
import { useEffect, useMemo, useCallback, useState } from 'react' | ||
import useConsentState from './useConsentState' | ||
|
||
type GTMTitleType = { videoTitle: string | undefined } | ||
type VideoRefType = React.RefObject<HTMLVideoElement> | ||
import { pushToDataLayer } from '../../lib/gtm' | ||
import { useEffect, useCallback, useState, RefObject } from 'react' | ||
|
||
const GTM_PLAY_EVENT = 'video_play' | ||
const GTM_PAUSE_EVENT = 'video_pause' | ||
const GTM_COMPLETION_EVENT = 'video_completion' | ||
const GTM_PROGRESS_MILESTONES = [25, 50, 75, 90] // Percentages | ||
|
||
type VideoRefType = RefObject<HTMLVideoElement> | ||
type EventType = typeof GTM_PLAY_EVENT | typeof GTM_PAUSE_EVENT | typeof GTM_COMPLETION_EVENT | string | ||
|
||
type EventData = { | ||
eventType: EventType | ||
videoTitle: string | ||
currentTime: number | ||
src: string | ||
videoType?: string | ||
} | ||
|
||
// Video Analytics Hook | ||
const useVideoAnalytics = (videoRef: VideoRefType, src: string, title?: string): void => { | ||
const [allowAnalytics, setAllowAnalytics] = useState(false) | ||
|
||
useConsentState( | ||
'statistics', | ||
() => setAllowAnalytics(true), | ||
() => setAllowAnalytics(false), | ||
) | ||
|
||
const GTM_TITLE: GTMTitleType = useMemo(() => ({ videoTitle: title || src }), [title, src]) | ||
const pushEventToDataLayer = useCallback( | ||
(eventType: EventType, videoElement: HTMLVideoElement) => { | ||
const eventData: EventData = { | ||
eventType, | ||
videoTitle: title || src, | ||
videoType: videoElement.loop ? 'loop' : undefined, | ||
currentTime: videoElement.currentTime, | ||
src, | ||
} | ||
pushToDataLayer('video_event', eventData) | ||
}, | ||
[title, src], | ||
) | ||
|
||
usePlayEvent(videoRef, pushEventToDataLayer, allowAnalytics) | ||
usePauseEvent(videoRef, pushEventToDataLayer, allowAnalytics) | ||
useCompletionEvent(videoRef, pushEventToDataLayer, allowAnalytics) | ||
useCompletionEventForLoopingVideos(videoRef, pushEventToDataLayer, allowAnalytics) | ||
useVideoProgressEvent(videoRef, pushEventToDataLayer, allowAnalytics) | ||
} | ||
|
||
const usePlayEvent = ( | ||
videoRef: VideoRefType, | ||
pushEvent: (eventType: EventType, videoElement: HTMLVideoElement) => void, | ||
allowAnalytics: boolean, | ||
) => { | ||
useEffect(() => { | ||
const videoElement = videoRef.current | ||
if (!videoElement) return | ||
|
||
const handlePlay = () => { | ||
if (allowAnalytics) { | ||
pushEvent(GTM_PLAY_EVENT, videoElement) | ||
} | ||
} | ||
videoElement.addEventListener('play', handlePlay) | ||
|
||
const handlePlayEvent = useCallback(() => { | ||
pushToDataLayer(GTM_PLAY_EVENT, GTM_TITLE) | ||
}, [GTM_TITLE]) | ||
return () => videoElement.removeEventListener('play', handlePlay) | ||
}, [videoRef, pushEvent, allowAnalytics]) | ||
} | ||
|
||
const handlePauseEvent = useCallback(() => { | ||
pushToDataLayer(GTM_PAUSE_EVENT, GTM_TITLE) | ||
}, [GTM_TITLE]) | ||
const usePauseEvent = ( | ||
videoRef: VideoRefType, | ||
pushEvent: (eventType: EventType, videoElement: HTMLVideoElement) => void, | ||
allowAnalytics: boolean, | ||
) => { | ||
useEffect(() => { | ||
const videoElement = videoRef.current | ||
if (!videoElement) return | ||
|
||
const handlePause = () => { | ||
const isVideoEnded = videoElement.currentTime >= videoElement.duration | ||
if (!isVideoEnded && allowAnalytics) { | ||
pushEvent(GTM_PAUSE_EVENT, videoElement) | ||
} | ||
} | ||
videoElement.addEventListener('pause', handlePause) | ||
|
||
return () => videoElement.removeEventListener('pause', handlePause) | ||
}, [videoRef, pushEvent, allowAnalytics]) | ||
} | ||
|
||
const useCompletionEvent = ( | ||
videoRef: VideoRefType, | ||
pushEvent: (eventType: EventType, videoElement: HTMLVideoElement) => void, | ||
allowAnalytics: boolean, | ||
) => { | ||
useEffect(() => { | ||
const videoElement = videoRef.current | ||
if (!videoElement) return | ||
|
||
if (!videoElement || !allowAnalytics) return | ||
const handleCompletion = () => { | ||
if (allowAnalytics) { | ||
pushEvent(GTM_COMPLETION_EVENT, videoElement) | ||
} | ||
} | ||
videoElement.addEventListener('ended', handleCompletion) | ||
|
||
return () => videoElement.removeEventListener('ended', handleCompletion) | ||
}, [videoRef, pushEvent, allowAnalytics]) | ||
} | ||
|
||
// Looping videos do not trigger 'ended' event listener | ||
// This hook triggers completion when the video is about to loop | ||
const useCompletionEventForLoopingVideos = ( | ||
videoRef: VideoRefType, | ||
pushEvent: (eventType: EventType, videoElement: HTMLVideoElement) => void, | ||
allowAnalytics: boolean, | ||
) => { | ||
const [hasTriggered, setHasTriggered] = useState(false) | ||
|
||
useEffect(() => { | ||
const videoElement = videoRef.current | ||
if (!videoElement || !videoElement.loop || !allowAnalytics || hasTriggered) return | ||
|
||
const threshold = 1 // Threshold in seconds to determine "near end" | ||
const handleTimeUpdate = () => { | ||
const timeLeft = videoElement.duration - videoElement.currentTime | ||
const nearEnd = timeLeft < threshold | ||
|
||
if (nearEnd && !hasTriggered) { | ||
pushEvent(GTM_COMPLETION_EVENT, videoElement) | ||
setHasTriggered(true) // Prevent further triggers | ||
} | ||
} | ||
|
||
videoElement.addEventListener('timeupdate', handleTimeUpdate) | ||
|
||
return () => videoElement.removeEventListener('timeupdate', handleTimeUpdate) | ||
}, [videoRef, pushEvent, allowAnalytics, hasTriggered]) | ||
} | ||
|
||
const useVideoProgressEvent = ( | ||
videoRef: VideoRefType, | ||
pushEvent: (eventType: EventType, videoElement: HTMLVideoElement) => void, | ||
allowAnalytics: boolean, | ||
) => { | ||
const [trackedMilestones, setTrackedMilestones] = useState<number[]>([]) | ||
const intervalDuration = 1000 // Check every second | ||
|
||
useEffect(() => { | ||
const videoElement = videoRef.current | ||
if (!videoElement) return | ||
|
||
const intervalId = setInterval(() => { | ||
if (!allowAnalytics || !videoElement.duration) return | ||
|
||
const progress = (videoElement.currentTime / videoElement.duration) * 100 | ||
GTM_PROGRESS_MILESTONES.forEach((milestone) => { | ||
if (progress >= milestone && !trackedMilestones.includes(milestone)) { | ||
pushEvent(`video_progress_${milestone}`, videoElement) | ||
setTrackedMilestones((prev) => [...prev, milestone]) | ||
} | ||
}) | ||
}, intervalDuration) | ||
|
||
const handlePlay = () => { | ||
if (videoElement.currentTime === 0 && !videoElement.loop) { | ||
setTrackedMilestones([]) // Reset milestones at the start of a new play session | ||
} | ||
} | ||
|
||
videoElement.addEventListener('play', handlePlayEvent) | ||
videoElement.addEventListener('pause', handlePauseEvent) | ||
videoElement.addEventListener('play', handlePlay) | ||
|
||
// Clean up event listeners on unmount | ||
return () => { | ||
videoElement.removeEventListener('play', handlePlayEvent) | ||
videoElement.removeEventListener('pause', handlePauseEvent) | ||
clearInterval(intervalId) | ||
videoElement.removeEventListener('play', handlePlay) | ||
} | ||
}, [allowAnalytics, videoRef, handlePlayEvent, handlePauseEvent]) | ||
}, [videoRef, pushEvent, allowAnalytics, trackedMilestones]) | ||
} | ||
|
||
export default useVideoAnalytics |