From 31aff903954b2868d9af70350c745bddb5b33932 Mon Sep 17 00:00:00 2001 From: jakubroch Date: Tue, 21 Jul 2026 14:37:19 +0200 Subject: [PATCH 1/2] feat: new events + maintenance --- .babelrc | 3 - dev/main.js | 108 -------- src/cloudinary-analytics.js | 238 ------------------ src/events-collector.js | 70 ------ src/events.consts.js | 10 - src/events/metadata-event.js | 25 -- src/events/pause-event.js | 15 -- src/events/play-event.js | 11 - src/index.js | 1 - .../nativeHtmlVideoPlayerAdapter.js | 36 --- src/utils/customer-data.js | 63 ----- src/utils/events.js | 52 ---- src/utils/metadata-validator.js | 40 --- src/utils/page-events.js | 45 ---- src/utils/send-beacon-request.js | 17 -- src/utils/unique-ids.js | 19 -- src/utils/validators.js | 73 ------ src/utils/video-player-options.js | 19 -- 18 files changed, 845 deletions(-) delete mode 100644 .babelrc delete mode 100644 dev/main.js delete mode 100644 src/cloudinary-analytics.js delete mode 100644 src/events-collector.js delete mode 100644 src/events.consts.js delete mode 100644 src/events/metadata-event.js delete mode 100644 src/events/pause-event.js delete mode 100644 src/events/play-event.js delete mode 100644 src/index.js delete mode 100644 src/player-adapters/nativeHtmlVideoPlayerAdapter.js delete mode 100644 src/utils/customer-data.js delete mode 100644 src/utils/events.js delete mode 100644 src/utils/metadata-validator.js delete mode 100644 src/utils/page-events.js delete mode 100644 src/utils/send-beacon-request.js delete mode 100644 src/utils/unique-ids.js delete mode 100644 src/utils/validators.js delete mode 100644 src/utils/video-player-options.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 1320b9a..0000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env"] -} diff --git a/dev/main.js b/dev/main.js deleted file mode 100644 index 1873161..0000000 --- a/dev/main.js +++ /dev/null @@ -1,108 +0,0 @@ -import { connectCloudinaryAnalytics } from 'cloudinary-video-analytics'; -window.connectCloudinaryAnalytics = connectCloudinaryAnalytics; - -const videos = [ - { - url: 'https://res.cloudinary.com/demo/video/upload/v1651840278/samples/cld-sample-video.mp4', - publicId: 'samples/cld-sample-video', - }, - { - url: 'https://res.cloudinary.com/demo/video/upload/v1643890261/cld-sample-video.mp4', - publicId: 'cld-sample-video', - }, - { - url: 'https://res.cloudinary.com/demo/video/upload/v1692105601/dog_demo.mp4', - publicId: 'dog_demo', - }, -]; - -window.addEventListener('load', () => { - const manualVideoElement = document.getElementById('main-video-player'); - const manualCloudinaryAnalytics = connectCloudinaryAnalytics(manualVideoElement); - const connectVideoPanel = () => { - let currentSelectedRadioValue = '0'; - const onMainVideoChange = (newVideoPublicId) => { - manualVideoElement.src = newVideoPublicId; - manualCloudinaryAnalytics.startManualTracking({ - cloudName: 'demo', - publicId: newVideoPublicId, - }); - }; - - document.addEventListener('input', (e) => { - if (e.target.getAttribute('name') === 'videoType') { - currentSelectedRadioValue = e.target.value; - onMainVideoChange(videos[currentSelectedRadioValue].url); - } - }); - }; - - // init - connectVideoPanel(); - manualCloudinaryAnalytics.startManualTracking({ - cloudName: 'demo', - publicId: videos[0].publicId, - }); - - // connect extra videos - const extraVideo1Element = document.getElementById('extra-video-1'); - const extraVideo1CldAnalytics = connectCloudinaryAnalytics(extraVideo1Element); - extraVideo1CldAnalytics.startManualTracking({ - cloudName: 'demo', - publicId: videos[0].publicId, - }); - - const extraVideo2Element = document.getElementById('extra-video-2'); - const extraVideo2CldAnalytics = connectCloudinaryAnalytics(extraVideo2Element); - extraVideo2CldAnalytics.startManualTracking({ - cloudName: 'demo', - publicId: videos[1].publicId, - }); - - // auto detection - const autoVideoElement = document.getElementById('auto-video-player'); - const autoCloudinaryAnalytics = connectCloudinaryAnalytics(autoVideoElement); - const connectAutoVideoPanel = () => { - let currentSelectedRadioValue = '0'; - const onMainVideoChange = (newVideoPublicId) => { - autoVideoElement.src = newVideoPublicId; - }; - - document.addEventListener('input', (e) => { - if (e.target.getAttribute('name') === 'autoVideoType') { - currentSelectedRadioValue = e.target.value; - onMainVideoChange(videos[currentSelectedRadioValue].url || ''); - } - }); - }; - - connectAutoVideoPanel(); - autoCloudinaryAnalytics.startAutoTracking({ - customData: { - customData1: 1, // skipped - customData2: 'value', - test: 'test', // skipped - customData5: 'another-value', - thisShouldBeSkipped: { // skipped - value: 1, - }, - }, - customVideoUrlFallback: (videoUrl) => { - if (videoUrl === 'https://res.cloudinary.com/demo/video/upload/v1692105601/dog_demo.mp4') { - return { - cloudName: 'fallback-cloud-name', - publicId: 'fallback-public-id', - }; - } - }, - }); - - // live stream - const liveStreamVideoElement = document.getElementById('live-stream-video-player'); - const liveStreamCldAnalytics = connectCloudinaryAnalytics(liveStreamVideoElement); - liveStreamCldAnalytics.startManualTracking({ - cloudName: 'demo', - publicId: 'live-stream-public-id', - type: 'live', - }); -}); diff --git a/src/cloudinary-analytics.js b/src/cloudinary-analytics.js deleted file mode 100644 index 793a11b..0000000 --- a/src/cloudinary-analytics.js +++ /dev/null @@ -1,238 +0,0 @@ -import { isMobile } from 'is-mobile'; -import { initEventsCollector } from './events-collector'; -import { getVideoViewId, getUserId } from './utils/unique-ids'; -import { sendBeaconRequest } from './utils/send-beacon-request'; -import { - createRegularVideoViewEndEvent, - createLiveStreamViewStartEvent, - createLiveStreamViewEndEvent, - createRegularVideoViewStartEvent, - prepareEvents, -} from './utils/events'; -import { throwErrorIfInvalid, metadataValidator, mainOptionsValidator, trackingOptionsValidator } from './utils/validators'; -import { nativeHtmlVideoPlayerAdapter } from './player-adapters/nativeHtmlVideoPlayerAdapter'; -import { parseCustomerVideoData } from './utils/customer-data'; -import { createPageTracker } from './utils/page-events'; - -const CLD_ANALYTICS_ENDPOINTS_LIST = { - production: { - default: 'https://video-analytics-api.cloudinary.com/v1/video-analytics', - liveStreams: 'https://video-analytics-api.cloudinary.com/v1/live-streams', - }, - development: { - default: 'http://localhost:3001/events', - liveStreams: 'http://localhost:3001/events', - }, -}; -const CLD_ANALYTICS_ENDPOINT = process.env.NODE_ENV === 'development' ? CLD_ANALYTICS_ENDPOINTS_LIST.development : CLD_ANALYTICS_ENDPOINTS_LIST.production; - -export const connectCloudinaryAnalytics = (videoElement, mainOptions = {}) => { - throwErrorIfInvalid( - mainOptionsValidator(mainOptions), - 'Cloudinary video analytics requires proper options object' - ); - - if (!mainOptions.playerAdapter) { - mainOptions.playerAdapter = nativeHtmlVideoPlayerAdapter(videoElement); - } - - let videoTrackingSession = null; - const userId = getUserId(); - const { playerAdapter } = mainOptions; - const isMobileDetected = isMobile({ tablet: true, featureDetect: true }); - const createEventsCollector = initEventsCollector(playerAdapter); - const clearVideoTracking = () => { - if (videoTrackingSession) { - videoTrackingSession.clear(); - videoTrackingSession = null; - } - }; - const viewId = { - _value: getVideoViewId(), - getValue: () => viewId._value, - regenerateValue: () => { - viewId._value = getVideoViewId(); - - if (videoTrackingSession) { - videoTrackingSession.viewId = viewId._value; - } - - return viewId._value; - }, - }; - - const startManualTracking = (metadata, options = {}) => { - if (videoTrackingSession?.type === 'auto') { - throw 'Cloudinary video analytics auto tracking is already connected with this HTML Video Element, to start manual tracking you need to stop auto tracking'; - } - - throwErrorIfInvalid( - metadataValidator(metadata), - 'Cloudinary video analytics manual tracking called without necessary data' - ); - throwErrorIfInvalid( - trackingOptionsValidator(options), - 'Cloudinary video analytics manual tracking called with invalid options' - ); - - clearVideoTracking(); - - if (metadata.type === 'live') { - _startManualTrackingLiveStream(metadata, options); - } else { - options.customVideoUrlFallback = options.customVideoUrlFallback || (() => metadata); - _startManualTrackingRegularVideo(metadata, options); - } - }; - - const _startManualTrackingRegularVideo = (metadata, options) => { - const sendData = (data) => sendBeaconRequest(CLD_ANALYTICS_ENDPOINT.default, data); - const videoViewEventCollector = createEventsCollector(); - const finishVideoTracking = () => { - // multiple events can be triggered one by one for specific browsers but we don't have guarantee which ones - // in this case send data for first event and for rest just skip it to avoid empty payload - if (videoViewEventCollector.getCollectedEventsCount() > 0) { - videoViewEventCollector.addEvent(createRegularVideoViewEndEvent()); - const events = prepareEvents([...videoViewEventCollector.flushEvents()]); - sendData({ - userId, - viewId: viewId.getValue(), - events, - }); - } - }; - - const pageEventsRemoval = createPageTracker({ - onPageViewStart: () => { - viewId.regenerateValue(); - - videoViewEventCollector.start(viewId.getValue()); - videoViewEventCollector.addEvent( - createRegularVideoViewStartEvent({ - videoUrl: playerAdapter.getCurrentSrc(), - trackingType: 'manual', - }, options) - ); - }, - onPageViewEnd: () => { - finishVideoTracking(); - videoViewEventCollector.destroy(); - }, - }, isMobileDetected); - - videoTrackingSession = { - viewId, - type: 'manual', - subtype: 'default', - clear: () => { - finishVideoTracking(); - videoViewEventCollector.destroy(); - pageEventsRemoval(); - }, - }; - }; - const _startManualTrackingLiveStream = (metadata, options) => { - const liveStreamData = parseCustomerVideoData(metadata); - const sendLiveStreamEvent = (event) => { - sendBeaconRequest(CLD_ANALYTICS_ENDPOINT.liveStreams, { - userId, - viewId: viewId.getValue(), - events: prepareEvents([event]), - }); - }; - const pageEventsRemoval = createPageTracker({ - onPageViewStart: () => { - viewId.regenerateValue(); - - sendLiveStreamEvent( - createLiveStreamViewStartEvent({ liveStreamData }, options) - ); - }, - onPageViewEnd: () => { - sendLiveStreamEvent( - createLiveStreamViewEndEvent({ liveStreamData }, options) - ); - }, - }, isMobileDetected); - - videoTrackingSession = { - viewId: viewId.getValue(), - type: 'manual', - subtype: 'live-stream', - clear: () => { - pageEventsRemoval(); - }, - }; - }; - - const startAutoTracking = (options = {}) => { - if (videoTrackingSession) { - throw 'Cloudinary video analytics tracking is already connected with this HTML Video Element'; - } - - throwErrorIfInvalid( - trackingOptionsValidator(options), - 'Cloudinary video analytics auto tracking called with invalid options' - ); - - const sendData = (data) => sendBeaconRequest(CLD_ANALYTICS_ENDPOINT.default, data); - const videoViewEventCollector = createEventsCollector(); - - const finishVideoTracking = () => { - // multiple events can be triggered one by one for specific browsers but we don't have guarantee which ones - // in this case send data for first event and for rest just skip it to avoid empty payload - if (videoViewEventCollector.getCollectedEventsCount() > 0) { - videoViewEventCollector.addEvent(createRegularVideoViewEndEvent()); - const events = prepareEvents([...videoViewEventCollector.flushEvents()]); - sendData({ - userId, - viewId: viewId.getValue(), - events, - }); - } - }; - - const startVideoTracking = () => { - const sourceUrl = playerAdapter.getCurrentSrc(); - if (sourceUrl === window.location.href || !sourceUrl) { - return null; - } - - viewId.regenerateValue(); - videoViewEventCollector.start(viewId.getValue()); - videoViewEventCollector.addEvent( - createRegularVideoViewStartEvent({ - videoUrl: sourceUrl, - trackingType: 'auto', - }, options) - ); - - videoTrackingSession = { - viewId, - type: 'auto', - clear: () => { - finishVideoTracking(); - videoViewEventCollector.destroy(); - }, - }; - }; - - createPageTracker({ - onPageViewStart: () => startVideoTracking(), - onPageViewEnd: () => { - finishVideoTracking(); - videoViewEventCollector.destroy(); - }, - }, isMobileDetected); - - // TODO: check if we need to listen also on loadmetadata to make sure source url is available - playerAdapter.onLoadStart(() => !videoTrackingSession && startVideoTracking()); - playerAdapter.onEmptied(() => clearVideoTracking()); - }; - - return { - startManualTracking, - stopManualTracking: clearVideoTracking, - startAutoTracking, - }; -}; diff --git a/src/events-collector.js b/src/events-collector.js deleted file mode 100644 index d83b41d..0000000 --- a/src/events-collector.js +++ /dev/null @@ -1,70 +0,0 @@ -import { registerPlayEvent } from './events/play-event'; -import { registerPauseEvent } from './events/pause-event'; -import { registerMetadataEvent } from './events/metadata-event'; -import { createEvent } from './utils/events'; - -export const initEventsCollector = (playerAdapter) => { - const collectedEvents = {}; - const rawEvents = {}; - - return () => { - let viewId = null; - const registeredEvents = []; - - const start = (_viewId) => { - if (viewId) { - throw new Error('Events collector session already started'); - } - - // create new collection of events for new video view - viewId = _viewId; - collectedEvents[viewId] = []; - rawEvents[viewId] = []; - registeredEvents.push( - registerPlayEvent(playerAdapter, reportEvent), - registerPauseEvent(playerAdapter, reportEvent), - registerMetadataEvent(playerAdapter, reportEvent), - ); - }; - const destroy = () => { - if (!viewId) { - throw new Error('Events collector session not started'); - } - - registeredEvents.forEach((cb) => cb()); - viewId = null; - }; - const reportEvent = (eventName, eventDetails) => { - if (!viewId) { - throw new Error('Events collector session not started'); - } - - rawEvents[viewId].push(createEvent(eventName, eventDetails)); - }; - const flushEvents = () => { - if (!viewId) { - throw new Error('Events collector session not started'); - } - - const events = rawEvents[viewId].splice(0, rawEvents[viewId].length); - collectedEvents[viewId].splice(collectedEvents[viewId].length, 0, ...events); - return events; - }; - - const getCollectedEventsCount = () => { - if (viewId) { - return rawEvents[viewId]?.length || 0; - } - - return 0; - }; - - return { - flushEvents, - getCollectedEventsCount, - addEvent: ({ eventName, eventDetails }) => reportEvent(eventName, eventDetails), - start, - destroy, - }; - } -}; diff --git a/src/events.consts.js b/src/events.consts.js deleted file mode 100644 index 86a7dfb..0000000 --- a/src/events.consts.js +++ /dev/null @@ -1,10 +0,0 @@ -export const VIDEO_EVENT = { - PLAY: 'play', - PAUSE: 'pause', - LOADED_METADATA: 'loadMetadata', -}; - -export const VIEW_EVENT = { - START: 'viewStart', - END: 'viewEnd', -}; diff --git a/src/events/metadata-event.js b/src/events/metadata-event.js deleted file mode 100644 index 65b32ea..0000000 --- a/src/events/metadata-event.js +++ /dev/null @@ -1,25 +0,0 @@ -import { VIDEO_EVENT } from '../events.consts'; - -export const parseVideoDuration = (durationValue) => { - const videoDuration = Number.isNaN(durationValue) ? null : durationValue; - return Number.POSITIVE_INFINITY === videoDuration ? 'Infinity' : videoDuration; -}; - -export const registerMetadataEvent = (playerAdapter, reportEvent) => { - const reportLoadedMetadata = () => { - reportEvent(VIDEO_EVENT.LOADED_METADATA, { - videoDuration: parseVideoDuration(playerAdapter.getDuration()), - videoUrl: playerAdapter.getCurrentSrc(), - }); - }; - const eventLoadedMetadataClearCallback = playerAdapter.onLoadedMetadata(() => reportLoadedMetadata()); - - if (playerAdapter.getReadyState() > 0) { - // make it async to allow collect any initial events - setTimeout(() => reportLoadedMetadata(), 0); - } - - return () => { - eventLoadedMetadataClearCallback(); - }; -}; diff --git a/src/events/pause-event.js b/src/events/pause-event.js deleted file mode 100644 index d432d89..0000000 --- a/src/events/pause-event.js +++ /dev/null @@ -1,15 +0,0 @@ -import { VIDEO_EVENT } from '../events.consts'; - -export const registerPauseEvent = (playerAdapter, reportEvent) => { - const eventPauseClearCallback = playerAdapter.onPause(() => { - reportEvent(VIDEO_EVENT.PAUSE, {}); - }); - const eventEmptiedClearCallback = playerAdapter.onEmptied(() => { - reportEvent(VIDEO_EVENT.PAUSE, {}); - }); - - return () => { - eventPauseClearCallback(); - eventEmptiedClearCallback(); - }; -}; diff --git a/src/events/play-event.js b/src/events/play-event.js deleted file mode 100644 index 381ff33..0000000 --- a/src/events/play-event.js +++ /dev/null @@ -1,11 +0,0 @@ -import { VIDEO_EVENT } from '../events.consts'; - -export const registerPlayEvent = (playerAdapter, reportEvent) => { - const eventPlayClearCallback = playerAdapter.onPlay(() => { - reportEvent(VIDEO_EVENT.PLAY, {}); - }); - - return () => { - eventPlayClearCallback(); - }; -}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 0ba691e..0000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { connectCloudinaryAnalytics } from './cloudinary-analytics'; diff --git a/src/player-adapters/nativeHtmlVideoPlayerAdapter.js b/src/player-adapters/nativeHtmlVideoPlayerAdapter.js deleted file mode 100644 index b8cd2f2..0000000 --- a/src/player-adapters/nativeHtmlVideoPlayerAdapter.js +++ /dev/null @@ -1,36 +0,0 @@ -const createBaseOnEventListener = (videoElement, eventName, callback) => { - videoElement.addEventListener(eventName, callback); - return () => { - videoElement.removeEventListener(eventName, callback); - }; -}; - -export const nativeHtmlVideoPlayerAdapter = (videoElement) => ({ - onCanPlay: (callback) => createBaseOnEventListener(videoElement, 'canplay', callback), - onCanPlayThrough: (callback) => createBaseOnEventListener(videoElement, 'canplaythrough', callback), - onComplete: (callback) => createBaseOnEventListener(videoElement, 'complete', callback), - onDurationChange: (callback) => createBaseOnEventListener(videoElement, 'durationchange', callback), - onEmptied: (callback) => createBaseOnEventListener(videoElement, 'emptied', callback), - onEnded: (callback) => createBaseOnEventListener(videoElement, 'ended', callback), - onError: (callback) => createBaseOnEventListener(videoElement, 'error', callback), - onLoadedData: (callback) => createBaseOnEventListener(videoElement, 'loadeddata', callback), - onLoadedMetadata: (callback) => createBaseOnEventListener(videoElement, 'loadedmetadata', callback), - onLoadStart: (callback) => createBaseOnEventListener(videoElement, 'loadstart', callback), - onPause: (callback) => createBaseOnEventListener(videoElement, 'pause', callback), - onPlay: (callback) => createBaseOnEventListener(videoElement, 'play', callback), - onPlaying: (callback) => createBaseOnEventListener(videoElement, 'playing', callback), - onProgress: (callback) => createBaseOnEventListener(videoElement, 'progress', callback), - onRateChange: (callback) => createBaseOnEventListener(videoElement, 'ratechange', callback), - onSeeked: (callback) => createBaseOnEventListener(videoElement, 'seeked', callback), - onSeeking: (callback) => createBaseOnEventListener(videoElement, 'seeking', callback), - onStalled: (callback) => createBaseOnEventListener(videoElement, 'stalled', callback), - onSuspend: (callback) => createBaseOnEventListener(videoElement, 'suspend', callback), - onTimeUpdate: (callback) => createBaseOnEventListener(videoElement, 'timeupdate', callback), - onVolumeChange: (callback) => createBaseOnEventListener(videoElement, 'volumechange', callback), - onWaiting: (callback) => createBaseOnEventListener(videoElement, 'waiting', callback), - - getCurrentSrc: () => videoElement.currentSrc, - getCurrentTime: () => videoElement.currentTime, - getReadyState: () => videoElement.readyState, - getDuration: () => videoElement.duration, -}); diff --git a/src/utils/customer-data.js b/src/utils/customer-data.js deleted file mode 100644 index 03197ba..0000000 --- a/src/utils/customer-data.js +++ /dev/null @@ -1,63 +0,0 @@ -const CUSTOMER_DATA_CHARS_LIMIT = 1000; - -const filterOutNonStrings = (data) => { - return Array.from({ length: 5 }).reduce((obj, cv, currentIndex) => { - const key = `customData${currentIndex + 1}`; - - if (typeof data[key] === 'string') { - obj[key] = data[key]; - } - - return obj; - }, {}) -}; - -export const parseCustomData = (customData) => { - const data = typeof customData === 'function' ? customData() : customData; - - if (typeof data === 'object' && data !== null && !Array.isArray(data)) { - const filteredData = filterOutNonStrings(data); - return Object.keys(filteredData).length > 0 ? filteredData : null; - } - - return null; -} - -export const isCustomDataValid = (customData) => { - if (customData === null) { - return false; - } - - const stringifiedValue = JSON.stringify(customData); - return stringifiedValue.length <= CUSTOMER_DATA_CHARS_LIMIT; -}; - -export const useCustomerVideoDataFallback = (videoUrl, fallback) => { - try { - const result = fallback(videoUrl); - return { - cloudName: result.cloudName, - publicId: result.publicId, - }; - } catch (e) { - return null; - } -}; - -export const parseCustomerVideoData = (data) => { - if ( - data !== null && - typeof data === 'object' && - typeof data.cloudName === 'string' && - data.cloudName && - typeof data.publicId === 'string' && - data.publicId - ) { - return { - cloudName: data.cloudName, - publicId: data.publicId, - }; - } - - return null; -}; diff --git a/src/utils/events.js b/src/utils/events.js deleted file mode 100644 index 0418f4c..0000000 --- a/src/utils/events.js +++ /dev/null @@ -1,52 +0,0 @@ -import { isCustomDataValid, parseCustomData, parseCustomerVideoData, useCustomerVideoDataFallback } from './customer-data'; -import { VIEW_EVENT } from '../events.consts'; -import { getVideoPlayerType, getVideoPlayerVersion } from './video-player-options'; - -export const createRegularVideoViewStartEvent = (baseData, customerOptions) => { - const customData = parseCustomData(customerOptions?.customData); - const isValidCustomData = isCustomDataValid(customData); - const customerVideoDataFromFallback = customerOptions?.customVideoUrlFallback ? useCustomerVideoDataFallback(baseData.videoUrl, customerOptions.customVideoUrlFallback) : null; - const customerVideoData = parseCustomerVideoData(customerVideoDataFromFallback); - return createEvent(VIEW_EVENT.START, { - ...baseData, - analyticsModuleVersion: ANALYTICS_VERSION, - videoPlayer: { - type: getVideoPlayerType(customerOptions?.videoPlayerType), - version: getVideoPlayerVersion(customerOptions?.videoPlayerVersion), - }, - customerData: { - ...(isValidCustomData ? { providedData: customData } : {}), - ...(customerVideoData ? { videoData: customerVideoData } : {}), - }, - }); -}; - -export const createRegularVideoViewEndEvent = (baseData = {}) => { - return createEvent(VIEW_EVENT.END, { ...baseData }); -}; - -export const createLiveStreamViewStartEvent = (baseData, customerOptions) => { - return createEvent(VIEW_EVENT.START, { - ...baseData, - analyticsModuleVersion: ANALYTICS_VERSION, - videoPlayer: { - type: getVideoPlayerType(customerOptions?.videoPlayerType), - version: getVideoPlayerVersion(customerOptions?.videoPlayerVersion), - }, - }); -}; - -export const createLiveStreamViewEndEvent = (baseData, customerOptions) => { - return createEvent(VIEW_EVENT.END, { ...baseData }); -}; - -export const createEvent = (eventName, eventDetails) => ({ - eventName, - eventTime: Date.now(), - eventDetails, -}); - -export const prepareEvents = (collectedEvents) => { - const events = [...collectedEvents]; - return JSON.stringify(events); -}; diff --git a/src/utils/metadata-validator.js b/src/utils/metadata-validator.js deleted file mode 100644 index 6de6300..0000000 --- a/src/utils/metadata-validator.js +++ /dev/null @@ -1,40 +0,0 @@ -export const metadataValidator = (metadata) => { - if (typeof metadata !== 'object') { - return { - isValid: false, - errorMessage: 'Metadata param needs to be an object', - }; - } - - if (typeof metadata.cloudName !== 'string') { - return { - isValid: false, - errorMessage: 'You need to provide proper cloud name of your Cloudinary account [cloudName: string]', - }; - } - - if (typeof metadata.type === 'string' && metadata.type !== 'live') { - return { - isValid: false, - errorMessage: 'Property "type" can be either undefined or "live" value [type: "live"]', - }; - } - - if (typeof metadata.publicId !== 'string') { - if (metadata.type === 'live') { - return { - isValid: false, - errorMessage: 'You need to provide proper public ID of your Live Stream output [publicId: string]', - }; - } - - return { - isValid: false, - errorMessage: 'You need to provide proper video public ID of your video on your Cloudinary cloud [publicId: string]', - }; - } - - return { - isValid: true, - }; -}; diff --git a/src/utils/page-events.js b/src/utils/page-events.js deleted file mode 100644 index b60d5cb..0000000 --- a/src/utils/page-events.js +++ /dev/null @@ -1,45 +0,0 @@ -export const createPageTracker = (events, isMobile) => { - if (isMobile) { - return createMobilePageTracker(events); - } - - return createDesktopPageTracker(events); -}; - -const createMobilePageTracker = ({ onPageViewStart, onPageViewEnd }) => { - const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - onPageViewStart?.(); - } else if (document.visibilityState === 'hidden') { - onPageViewEnd?.(); - } - }; - - document.addEventListener('visibilitychange', handleVisibilityChange); - - if (document.visibilityState === 'visible') { - onPageViewStart?.(); - } - - return () => { - document.removeEventListener('visibilitychange', handleVisibilityChange); - }; -}; - -const createDesktopPageTracker = ({ onPageViewStart, onPageViewEnd }) => { - const handlePageLoad = () => onPageViewStart?.(); - const handleBeforeUnload = () => onPageViewEnd?.(); - - if (document.readyState === 'complete') { - handlePageLoad(); - } else { - window.addEventListener('load', handlePageLoad); - } - - window.addEventListener('beforeunload', handleBeforeUnload); - - return () => { - window.removeEventListener('load', handlePageLoad); - window.removeEventListener('beforeunload', handleBeforeUnload); - }; -}; diff --git a/src/utils/send-beacon-request.js b/src/utils/send-beacon-request.js deleted file mode 100644 index 2b664a1..0000000 --- a/src/utils/send-beacon-request.js +++ /dev/null @@ -1,17 +0,0 @@ -export const sendBeaconRequest = (url, data) => { - const params = Object.keys(data).reduce((formData, key) => { - formData.append(key, data[key]); - return formData; - }, new FormData()); - - if (typeof window.navigator.sendBeacon !== 'function') { - return window.fetch(url, { - method: 'POST', - mode: 'no-cors', - body: params, - keepalive: true - }); - } - - return window.navigator.sendBeacon(url, params); -}; diff --git a/src/utils/unique-ids.js b/src/utils/unique-ids.js deleted file mode 100644 index 5e56902..0000000 --- a/src/utils/unique-ids.js +++ /dev/null @@ -1,19 +0,0 @@ -import { v4 as uuidv4 } from 'uuid'; - -const CLD_ANALYTICS_USER_ID_KEY = 'cld-analytics-user-id'; - -export const getUserId = () => { - const storageUserId = window.localStorage.getItem(CLD_ANALYTICS_USER_ID_KEY); - - if (storageUserId) { - return storageUserId; - } - - const userId = uuidv4().replace(/-/g, ''); - window.localStorage.setItem(CLD_ANALYTICS_USER_ID_KEY, userId); - return userId; -}; - -export const getVideoViewId = () => { - return uuidv4().replace(/-/g, ''); -}; diff --git a/src/utils/validators.js b/src/utils/validators.js deleted file mode 100644 index c42c54e..0000000 --- a/src/utils/validators.js +++ /dev/null @@ -1,73 +0,0 @@ -export const throwErrorIfInvalid = (validationResult, errorHeader) => { - if (!validationResult.isValid) { - throw errorHeader ? `${errorHeader}:` + "\n\n" + validationResult.errorMessage : validationResult.errorMessage; - } -}; - -export const metadataValidator = (metadata) => { - if (typeof metadata !== 'object') { - return { - isValid: false, - errorMessage: 'Metadata param needs to be an object', - }; - } - - if (typeof metadata.cloudName !== 'string') { - return { - isValid: false, - errorMessage: 'You need to provide proper cloud name of your Cloudinary account [cloudName: string]', - }; - } - - if (typeof metadata.type === 'string' && metadata.type !== 'live') { - return { - isValid: false, - errorMessage: 'Property "type" can be either undefined or "live" value', - }; - } - - if (typeof metadata.publicId !== 'string') { - if (metadata.type === 'live') { - return { - isValid: false, - errorMessage: 'You need to provide proper public ID of your Live Stream output', - }; - } - - return { - isValid: false, - errorMessage: 'You need to provide proper video public ID of your video on your Cloudinary cloud [videoPublicId: string]', - }; - } - - return { - isValid: true, - }; -}; - -export const mainOptionsValidator = (mainOptions) => { - if (mainOptions && typeof mainOptions !== 'object') { - return { - isValid: false, - errorMessage: 'Options property must be an object', - }; - } - - return { - isValid: true, - }; -}; - -export const trackingOptionsValidator = (trackingOptions) => { - if (trackingOptions && typeof trackingOptions !== 'object') { - return { - isValid: false, - errorMessage: 'Options property must be an object', - }; - } - - return { - isValid: true, - }; -}; - diff --git a/src/utils/video-player-options.js b/src/utils/video-player-options.js deleted file mode 100644 index 0a32478..0000000 --- a/src/utils/video-player-options.js +++ /dev/null @@ -1,19 +0,0 @@ -const REGEX_SEMANTIC_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; -const NATIVE_VIDEO_PLAYER = 'native'; -const ALLOWED_VIDEO_PLAYER_TYPES = [NATIVE_VIDEO_PLAYER, 'cloudinary video player']; - -export const getVideoPlayerType = (videoPlayerType) => { - if (ALLOWED_VIDEO_PLAYER_TYPES.includes(videoPlayerType)) { - return videoPlayerType; - } - - return NATIVE_VIDEO_PLAYER; -}; - -export const getVideoPlayerVersion = (videoPlayerVersion) => { - if (typeof videoPlayerVersion === 'string' && REGEX_SEMANTIC_VERSION.test(videoPlayerVersion)) { - return videoPlayerVersion; - } - - return null; -}; From 2ca5b08af8997c216822df03b9535178e23ee157 Mon Sep 17 00:00:00 2001 From: jakubroch Date: Tue, 21 Jul 2026 14:37:27 +0200 Subject: [PATCH 2/2] feat: new events + maintenance --- .github/workflows/publish.yml | 12 +- .gitignore | 2 +- .npmignore | 2 +- README.md | 141 +++++++- dev/main.ts | 123 +++++++ package.json | 12 +- src/cloudinary-analytics.ts | 313 ++++++++++++++++++ src/events-collector.ts | 145 ++++++++ src/events.consts.ts | 26 ++ src/events/buffering-event.ts | 182 ++++++++++ src/events/ended-event.ts | 31 ++ src/events/error-event.ts | 16 + src/events/fullscreen-event.ts | 24 ++ src/events/metadata-event.ts | 26 ++ src/events/pause-event.ts | 16 + src/events/picture-in-picture-event.ts | 16 + src/events/play-event.ts | 12 + src/events/quality-change-event.ts | 46 +++ src/events/startup-event.ts | 31 ++ src/index.ts | 13 + .../nativeHtmlVideoPlayerAdapter.ts | 80 +++++ src/types.ts | 145 ++++++++ src/utils/customer-data.ts | 70 ++++ src/utils/events.ts | 78 +++++ src/utils/metadata-validator.ts | 44 +++ src/utils/page-events.ts | 47 +++ src/utils/send-beacon-request.ts | 20 ++ src/utils/unique-ids.ts | 19 ++ src/utils/validators.ts | 112 +++++++ src/utils/video-player-options.ts | 21 ++ tsconfig.json | 23 ++ tsconfig.types.json | 10 + types/globals.d.ts | 18 + webpack.config.js | 9 +- webpack.dev.config.js | 4 +- 35 files changed, 1868 insertions(+), 21 deletions(-) create mode 100644 dev/main.ts create mode 100644 src/cloudinary-analytics.ts create mode 100644 src/events-collector.ts create mode 100644 src/events.consts.ts create mode 100644 src/events/buffering-event.ts create mode 100644 src/events/ended-event.ts create mode 100644 src/events/error-event.ts create mode 100644 src/events/fullscreen-event.ts create mode 100644 src/events/metadata-event.ts create mode 100644 src/events/pause-event.ts create mode 100644 src/events/picture-in-picture-event.ts create mode 100644 src/events/play-event.ts create mode 100644 src/events/quality-change-event.ts create mode 100644 src/events/startup-event.ts create mode 100644 src/index.ts create mode 100644 src/player-adapters/nativeHtmlVideoPlayerAdapter.ts create mode 100644 src/types.ts create mode 100644 src/utils/customer-data.ts create mode 100644 src/utils/events.ts create mode 100644 src/utils/metadata-validator.ts create mode 100644 src/utils/page-events.ts create mode 100644 src/utils/send-beacon-request.ts create mode 100644 src/utils/unique-ids.ts create mode 100644 src/utils/validators.ts create mode 100644 src/utils/video-player-options.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.types.json create mode 100644 types/globals.d.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a77f6e5..928198c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,10 +22,13 @@ jobs: with: ref: 'refs/heads/master' + - name: Setup pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 20 registry-url: 'https://registry.npmjs.org' - name: Configure Git @@ -33,18 +36,15 @@ jobs: git config user.name "cloudinary-bot" git config user.email "cloudinary-bot@clodinary.com" - - name: Setup yarn - run: npm install -g yarn - - name: Install dependencies - run: yarn install --frozen-lockfile + run: pnpm install - name: Bump version run: npm version ${{ github.event.inputs.version-type }} -m "Bumping to version %s" --tag-version-prefix="" - name: Build shell: bash - run: yarn build + run: pnpm build - name: Publish packages run: npm publish --access="public" diff --git a/.gitignore b/.gitignore index 3f6f8f5..96aab62 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ tags # nodejs node_modules/ npm-debug.log -yarn-error.log +pnpm-debug.log # project env.js diff --git a/.npmignore b/.npmignore index e11cf97..d4bec2d 100644 --- a/.npmignore +++ b/.npmignore @@ -6,7 +6,7 @@ tags # nodejs node_modules/ npm-debug.log -yarn-error.log +pnpm-debug.log # project env.js diff --git a/README.md b/README.md index 83279d8..b8f552a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Cloudinary Video Analytics The Cloudinary Video Analytics package allows you to track analytics for your Cloudinary videos using video players other than the Cloudinary Video Player. The library targets the HTML5 video tag and is designed to work with any video player that use this tag. For more information view the [documentation](https://cloudinary.com/documentation/video_analytics). -### Usage +The package is written in TypeScript and ships its own type declarations. + +## Usage **1. Install the package**: ```shell @@ -12,7 +14,7 @@ npm i cloudinary-video-analytics ```js import { connectCloudinaryAnalytics } from 'cloudinary-video-analytics'; ``` -**3. Connect the analytics:**: +**3. Connect the analytics**: Connect the analytics by calling the `connectCloudinaryAnalytics` method and provide the video element as a parameter. For example, if your video element has the id ‘video-player’: ```js @@ -36,7 +38,140 @@ cloudinaryAnalytics.startManualTracking({ Auto and manual tracking cannot be used together for the same video element. Manual tracking should only be used in cases where you need to track certain videos, you want to track a video tag element which is dynamic, or you have custom domains which require providing `cloudName` and `publicId`. -### CodeSandbox examples +## API + +`connectCloudinaryAnalytics(videoElement, mainOptions?)` returns an object with the following methods: + +| Method | Description | +| --- | --- | +| `startAutoTracking(options?)` | Automatically detects and tracks Cloudinary videos played in the element. | +| `startManualTracking(metadata, options?)` | Tracks a specific video (or live stream) identified by `cloudName` / `publicId`. | +| `stopManualTracking()` | Stops the current tracking session. | +| `stopAutoTracking()` | Alias of `stopManualTracking` — stops the current tracking session. | +| `stopTracking()` | Stops whichever tracking (auto or manual) is currently active. | +| `reportCustomEvent(name, data?)` | Records a consumer-defined event into the current view (see [Custom events](#custom-events)). | + +### `metadata` (manual tracking) + +| Field | Type | Description | +| --- | --- | --- | +| `cloudName` | `string` | Your Cloudinary cloud name. Required. | +| `publicId` | `string` | Public ID of the video (or live stream output). Required. | +| `type` | `'live'` | Optional. Set to `'live'` to track a live stream. | + +### `options` (tracking options) + +| Field | Type | Description | +| --- | --- | --- | +| `customData` | `object \| () => object` | Up to five string fields (`customData1`…`customData5`); non-string values are ignored. | +| `videoPlayerType` | `string` | `'native'` (default) or `'cloudinary video player'`. | +| `videoPlayerVersion` | `string` | Semantic version of the player. | +| `customVideoUrlFallback` | `(videoUrl) => ({ cloudName, publicId })` | Resolves `cloudName`/`publicId` from a video URL (auto tracking / custom domains). | + +### `mainOptions` + +| Field | Type | Description | +| --- | --- | --- | +| `playerAdapter` | `PlayerAdapter` | Custom player adapter. Defaults to the built-in native HTML5 `