Analytics

Reports playback as events, for whatever analytics service you use.

import { analytics } from "react-video-kit/analytics";

Adds 774 B to a player906 B on its ownSource

Calls one function with an event each time something happens in playback. Play, seek, and pause the demo, and watch the events appear under it.

0:00−0:000:00

Play, seek, and pause. Each event appears here as onEvent receives it, newest first, with a heartbeat for every 10 seconds played.

  1. No events yet. Press play.

Usage

tsx
import { analytics } from "react-video-kit/analytics";import { VideoPlayer } from "react-video-kit/player";const plugins = [analytics({ onEvent: (event) => console.log(event) })];<VideoPlayer src="/video.mp4" plugins={plugins} />;

heartbeat sets the seconds of playback between heartbeat events, and defaults to 30. 0 turns them off. progress sets the percentages that send a progress event, and defaults to 10, 25, 50, and 75:

tsx
analytics({ onEvent: (event) => console.log(event), heartbeat: 10, progress: [25, 50, 75, 90] });

Send events to a service

tsx
analytics({  onEvent(event) {    navigator.sendBeacon("/api/video-events", JSON.stringify(event));  },});

Google Analytics

googleAnalytics turns these events into the ones Google Analytics 4 uses for video, with the same names and parameters as its own measurement of embedded YouTube videos, so the two line up in your reports. Play this demo, and watch what would be sent. It contacts nobody, because send replaces the real call.

0:00−0:000:00
  1. Nothing sent yet. Press play.
tsx
import { analytics } from "react-video-kit/analytics";import { googleAnalytics } from "react-video-kit/analytics/google";const plugins = [analytics({ onEvent: googleAnalytics({ title: "My video" }) })];
EventWhen it's sent
video_startPlayback first begins for this video.
video_progressPlayback passes 10, 25, 50, and 75 percent, once each. Seeking past a mark doesn't count.
video_completeThe video ends, after playback carried it past the last mark. Seeking to the end doesn't count.

Each has video_current_time, video_duration, video_percent, video_provider, video_title, video_url, and visible, which says whether the page was showing. Analytics already has dimensions for video_provider, video_title, video_url, and visible. To report on the other three, register them as custom dimensions or metrics.

With gtag.js

Keep Google's usual snippet on the page. The plugin finds window.gtag, and never loads Google's script itself:

html
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script><script>  window.dataLayer = window.dataLayer || [];  function gtag() {    dataLayer.push(arguments);  }  gtag("js", new Date());  gtag("config", "G-XXXXXXXXXX");</script>

On a page with more than one Google tag, measurementId sends these events only to yours:

tsx
googleAnalytics({ measurementId: "G-XXXXXXXXXX", title: "My video" });

With Tag Manager

Without gtag, each event goes to window.dataLayer, as { event: "video_start", video_percent: 0, ... }. In Tag Manager, add a Custom Event trigger matching video_start|video_progress|video_complete as a regular expression, Data Layer Variables for the parameters you want, and a Google Analytics event tag using them.

If Tag Manager's own YouTube trigger or enhanced measurement also covers this page, that only watches embedded YouTube players, so it won't count these videos twice.

More events

Google has no names for these, so they're custom events, and off until you ask:

tsx
googleAnalytics({ events: ["pause", "seek", "rebuffer", "quality-change", "error"] });
In eventsThe eventIts extra parameter
pausevideo_pause
seekvideo_seek
rebuffervideo_rebufferstalled_ms
quality-changevideo_quality_changevideo_quality
errorvideo_errorerror_code, error_fatal

error_code is one of the error codes. Google counts each parameter's value up to 100 characters, and each event up to 25 parameters, which these stay within apart from a long video_url.

  • video_url has no query string or hash. Signed URLs carry tokens there. Change that with url, which can also leave the URL out:

    tsx
    googleAnalytics({ url: (source) => new URL(source, location.href).pathname });googleAnalytics({ url: () => undefined });
  • video_error sends only the code, because a browser's message can include a URL.

  • Nothing here sets or reads consent. That stays with your site and your consent banner. With Consent Mode, set your defaults before the Google tag loads, and these events follow them like any others:

    js
    gtag("consent", "default", { analytics_storage: "denied" });// Once the viewer chooses:gtag("consent", "update", { analytics_storage: "granted" });

To send to something else that takes these names, give it send:

tsx
googleAnalytics({ send: (name, params) => myAnalytics.track(name, params) });

Events

typeWhen it firesExtra field
loadA source starts loading.
readyThe source moves from loading to ready.startupMs
startPlayback really begins for this video. Retrying it doesn't send this again, and neither does an ad.
playPlayback starts or resumes.
pausePlayback pauses, except when the video ended.
seekA seek finishes.
progressPlayback passes one of the progress marks, once each for this video.percent
rebuffer-startPlayback stalls after it has started, while not paused or recovering from a seek.
rebuffer-endThat stall stops, including because the viewer paused or started a seek.stalledMs
quality-changeThe active quality level changes to a known one.quality
endedThe video ends.complete
errorAnything onError hears, fatal or not. See the error codes.error
heartbeatEvery heartbeat seconds of video played.

Every event has type, position, duration, percent, and source.

FieldTypeDescription
typeAnalyticsEventTypeWhat happened.
positionnumberPlayback position when it happened, in seconds.
durationnumberThe video's length in seconds, which is 0 until known and Infinity for a live stream.
percentnumberHow far through the video that is, from 0 to 100. A progress event gives the mark it passed.
sourcestring | nullURL of the current source, or null when there isn't one.
startupMs?numberMilliseconds from load to ready, on ready.
stalledMs?numberMilliseconds spent stalled, on rebuffer-end.
quality?string | nullThe new level's id, on quality-change.
complete?booleanTrue on ended when playback, rather than a seek, carried the video past its last mark.
error?MediaErrorInfo | nullWhat went wrong, on error.

Behaviour

  • Heartbeats count video played. Seeks and source changes add nothing and don't reset the count, so adding heartbeats up gives a close measure of how much was watched. It counts video time, so playing at 2× reaches the next heartbeat twice as fast.
  • position comes from the player's clock when the event is sent, so seek reports where the seek landed.
  • Dragging the timeline can send several seek events, one for each seek that finishes.
  • A stall caused by a seek isn't counted as a rebuffer, including the moment after the seek finishes while playback picks back up.
  • The end of the video sends ended without a pause.
  • progress and complete follow playback, not position. Seeking past a mark doesn't send it, though playing through it later does. complete is true on ended when playback carried the video past its last mark, and has to be earned again on a second viewing.
  • Progress reads the clock about once a second, and never writes to the store, so it adds no renders.
  • onEvent runs inside the player's store update. Keep it quick, and don't let it throw, because that would stop the other subscribers in that update.

API

Options

OptionTypeDescription
onEvent(event: AnalyticsEvent) => voidCalled with each event as it happens.
heartbeat?numberSeconds of playback between heartbeats. 0 turns them off.
progress?readonly number[]The percentages that send a progress event once each, as playback passes them.

State

This plugin has no state of its own.

Commands

This plugin has no commands of its own.

Google Analytics API

Functions

googleAnalytics

Sends the analytics plugin's events to Google Analytics 4 under Google's own names for video. It uses the gtag or dataLayer already on the page: it never loads Google's script or changes consent, which stay with your site. Keeps no state, so players can share one.

(options?: GoogleAnalyticsOptions) => (event: AnalyticsEvent) => void

Types

GoogleAnalyticsExtra

Optional events beyond Google's own video events.

GoogleAnalyticsExtra
GoogleAnalyticsOptions
FieldTypeDescription
measurementId?stringSends only to this measurement ID, as send_to, for a page with more than one.
title?string | ((event: AnalyticsEvent) => string | undefined)The video_title, or a function to work it out from the event.
provider?stringThe video_provider. Defaults to react-video-kit.
events?readonly GoogleAnalyticsExtra[]Turns on events beyond video_start, video_progress, and video_complete.
url?((source: string) => string | undefined)Turns the source into video_url. By default that drops the query string and hash, which may hold tokens. Return undefined to send no URL.
send?((name: string, params: GoogleAnalyticsParams) => void)Sends the event yourself, in place of gtag or dataLayer.
GoogleAnalyticsParams

The parameters sent with an event.

GoogleAnalyticsParams