more analytics + fixes (#1476)

more analytics + refactor

- passes player with time to start (until we move this api to watchman)
- supports livestream metrics for buffering
- fixes bug with buffering over 10 second period
- less head calls by moving to videojs-events

* review fixes
This commit is contained in:
Thomas Zarebczan 2022-05-17 10:47:44 -04:00 committed by GitHub
parent be7193382c
commit 1d61d80009
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 72 additions and 35 deletions

View file

@ -63,7 +63,7 @@ type Analytics = {
tagFollowEvent: (string, boolean, ?string) => void, tagFollowEvent: (string, boolean, ?string) => void,
playerLoadedEvent: (string, ?boolean) => void, playerLoadedEvent: (string, ?boolean) => void,
playerVideoStartedEvent: (?boolean) => void, playerVideoStartedEvent: (?boolean) => void,
videoStartEvent: (?string, number, string, ?number, string, any, ?number) => void, videoStartEvent: (?string, number, string, ?number, string, any, ?number, boolean) => void,
videoIsPlaying: (boolean, any) => void, videoIsPlaying: (boolean, any) => void,
videoBufferEvent: ( videoBufferEvent: (
StreamClaim, StreamClaim,
@ -75,6 +75,7 @@ type Analytics = {
userId: string, userId: string,
playerPoweredBy: string, playerPoweredBy: string,
readyState: number, readyState: number,
isLivestream: boolean,
} }
) => Promise<any>, ) => Promise<any>,
adsFetchedEvent: () => void, adsFetchedEvent: () => void,
@ -133,7 +134,7 @@ function getDeviceType() {
// variables initialized for watchman // variables initialized for watchman
let amountOfBufferEvents = 0; let amountOfBufferEvents = 0;
let amountOfBufferTimeInMS = 0; let amountOfBufferTimeInMS = 0;
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond; let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond, isLivestream;
let lastSentTime; let lastSentTime;
// calculate data for backend, send them, and reset buffer data for next interval // calculate data for backend, send them, and reset buffer data for next interval
@ -150,20 +151,27 @@ async function sendAndResetWatchmanData() {
lastSentTime = new Date(); lastSentTime = new Date();
let protocol; let protocol;
if (videoType === 'application/x-mpegURL') { if (videoType === 'application/x-mpegURL' && !isLivestream) {
protocol = 'hls'; protocol = 'hls';
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality) // get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
// $FlowFixMe // $FlowFixMe
bitrateAsBitsPerSecond = videoPlayer.textTracks?.().tracks_[0]?.activeCues[0]?.value?.bandwidth; bitrateAsBitsPerSecond = videoPlayer.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
} else if (isLivestream) {
protocol = 'lvs';
// $FlowFixMe
bitrateAsBitsPerSecond = videoPlayer.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
} else { } else {
protocol = 'stb'; protocol = 'stb';
} }
// current position in video in MS // current position in video in MS
const positionInVideo = videoPlayer && Math.round(videoPlayer.currentTime()) * 1000; const positionInVideo = isLivestream ? 0 : videoPlayer && Math.round(videoPlayer.currentTime()) * 1000;
// get the duration marking the time in the video for relative position calculation // get the duration marking the time in the video for relative position calculation
const totalDurationInSeconds = videoPlayer && Math.round(videoPlayer.duration()); const totalDurationInSeconds = isLivestream ? 0 : videoPlayer && Math.round(videoPlayer.duration());
// temp: if buffering over the interval, the duration doesn't reset since we don't get an event
if (amountOfBufferTimeInMS > timeSinceLastIntervalSend) amountOfBufferTimeInMS = timeSinceLastIntervalSend;
// build object for watchman backend // build object for watchman backend
const objectToSend = { const objectToSend = {
@ -175,8 +183,8 @@ async function sendAndResetWatchmanData() {
protocol, protocol,
player: playerPoweredBy, player: playerPoweredBy,
user_id: userId.toString(), user_id: userId.toString(),
position: Math.round(positionInVideo), position: isLivestream ? 0 : Math.round(positionInVideo),
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100), rel_position: isLivestream ? 0 : Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
bitrate: bitrateAsBitsPerSecond, bitrate: bitrateAsBitsPerSecond,
bandwidth: undefined, bandwidth: undefined,
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated // ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
@ -267,16 +275,26 @@ const analytics: Analytics = {
startWatchmanIntervalIfNotRunning(); startWatchmanIntervalIfNotRunning();
} }
}, },
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => { videoStartEvent: (
claimId,
timeToStartVideo,
poweredBy,
passedUserId,
canonicalUrl,
passedPlayer,
videoBitrate,
isLivestreamClaim
) => {
// populate values for watchman when video starts // populate values for watchman when video starts
userId = passedUserId; userId = passedUserId;
claimUrl = canonicalUrl; claimUrl = canonicalUrl;
playerPoweredBy = poweredBy; playerPoweredBy = poweredBy;
isLivestream = isLivestreamClaim;
videoType = passedPlayer.currentSource().type; videoType = passedPlayer.currentSource().type;
videoPlayer = passedPlayer; videoPlayer = passedPlayer;
bitrateAsBitsPerSecond = videoBitrate; bitrateAsBitsPerSecond = videoBitrate;
sendPromMetric('time_to_start', timeToStartVideo); !isLivestreamClaim && sendPromMetric('time_to_start', timeToStartVideo, playerPoweredBy);
}, },
error: (message) => { error: (message) => {
return new Promise((resolve) => { return new Promise((resolve) => {
@ -469,10 +487,10 @@ function sendGaEvent(event: string, params?: { [string]: string | number }) {
} }
} }
function sendPromMetric(name: string, value?: number) { function sendPromMetric(name: string, value?: number, player: string) {
if (IS_WEB) { if (IS_WEB) {
let url = new URL(SDK_API_PATH + '/metric/ui'); let url = new URL(SDK_API_PATH + '/metric/ui');
const params = { name: name, value: value ? value.toString() : '' }; const params = { name: name, value: value ? value.toString() : '', player: player };
url.search = new URLSearchParams(params).toString(); url.search = new URLSearchParams(params).toString();
return fetch(url, { method: 'post' }).catch(function (error) {}); return fetch(url, { method: 'post' }).catch(function (error) {});
} }

View file

@ -22,6 +22,7 @@ const VideoJsEvents = ({
embedded, embedded,
uri, uri,
doAnalyticsView, doAnalyticsView,
doAnalyticsBuffer,
claimRewards, claimRewards,
playerServerRef, playerServerRef,
isLivestreamClaim, isLivestreamClaim,
@ -38,16 +39,28 @@ const VideoJsEvents = ({
clearPosition: (string) => void, clearPosition: (string) => void,
uri: string, uri: string,
doAnalyticsView: (string, number) => any, doAnalyticsView: (string, number) => any,
doAnalyticsBuffer: (string, any) => void,
claimRewards: () => void, claimRewards: () => void,
playerServerRef: any, playerServerRef: any,
isLivestreamClaim: boolean, isLivestreamClaim: boolean,
}) => { }) => {
function doTrackingBuffered(e: Event, data: any) {
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
data.playPoweredBy = playerPoweredBy;
data.isLivestream = isLivestreamClaim;
// $FlowFixMe
data.bitrateAsBitsPerSecond = this.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH;
doAnalyticsBuffer(uri, data);
}
/** /**
* Analytics functionality that is run on first video start * Analytics functionality that is run on first video start
* @param e - event from videojs (from the plugin?) * @param e - event from videojs (from the plugin?)
* @param data - only has secondsToLoad property * @param data - only has secondsToLoad property
*/ */
function doTrackingFirstPlay(e: Event, data: any) { function doTrackingFirstPlay(e: Event, data: any) {
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
// how long until the video starts // how long until the video starts
let timeToStartVideo = data.secondsToLoad; let timeToStartVideo = data.secondsToLoad;
@ -63,10 +76,6 @@ const VideoJsEvents = ({
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds); bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
} }
// figure out what server the video is served from and then run start analytic event
// server string such as 'eu-p6'
const playerPoweredBy = playerServerRef.current;
// populates data for watchman, sends prom and matomo event // populates data for watchman, sends prom and matomo event
analytics.videoStartEvent( analytics.videoStartEvent(
claimId, claimId,
@ -75,7 +84,21 @@ const VideoJsEvents = ({
userId, userId,
uri, uri,
this, // pass the player this, // pass the player
bitrateAsBitsPerSecond bitrateAsBitsPerSecond,
isLivestreamClaim
);
} else {
// populates data for watchman, sends prom and matomo event
analytics.videoStartEvent(
claimId,
0,
playerPoweredBy,
userId,
uri,
this, // pass the player
// $FlowFixMe
this.tech(true).vhs?.playlists?.media?.().attributes?.BANDWIDTH,
isLivestreamClaim
); );
} }
@ -191,6 +214,9 @@ const VideoJsEvents = ({
player.on('error', onError); player.on('error', onError);
// custom tracking plugin, event used for watchman data, and marking view/getting rewards // custom tracking plugin, event used for watchman data, and marking view/getting rewards
player.on('tracking:firstplay', doTrackingFirstPlay); player.on('tracking:firstplay', doTrackingFirstPlay);
// used for tracking buffering for watchman
player.on('tracking:buffered', doTrackingBuffered);
// hide forcing control bar show // hide forcing control bar show
player.on('canplaythrough', function () { player.on('canplaythrough', function () {
setTimeout(function () { setTimeout(function () {

View file

@ -94,6 +94,7 @@ type Props = {
toggleVideoTheaterMode: () => void, toggleVideoTheaterMode: () => void,
claimRewards: () => void, claimRewards: () => void,
doAnalyticsView: (string, number) => void, doAnalyticsView: (string, number) => void,
doAnalyticsBuffer: (string, any) => void,
uri: string, uri: string,
claimValues: any, claimValues: any,
clearPosition: (string) => void, clearPosition: (string) => void,
@ -151,6 +152,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
toggleVideoTheaterMode, toggleVideoTheaterMode,
claimValues, claimValues,
doAnalyticsView, doAnalyticsView,
doAnalyticsBuffer,
claimRewards, claimRewards,
uri, uri,
clearPosition, clearPosition,
@ -203,6 +205,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
claimId, claimId,
embedded, embedded,
doAnalyticsView, doAnalyticsView,
doAnalyticsBuffer,
claimRewards, claimRewards,
uri, uri,
playerServerRef, playerServerRef,

View file

@ -204,16 +204,6 @@ function VideoViewer(props: Props) {
}; };
}, [embedded, videoPlaybackRate]); }, [embedded, videoPlaybackRate]);
// TODO: analytics functionality
function doTrackingBuffered(e: Event, data: any) {
if (!isLivestreamClaim) {
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
data.playerPoweredBy = response.headers.get('x-powered-by');
doAnalyticsBuffer(uri, data);
});
}
}
const doPlay = useCallback( const doPlay = useCallback(
(playUri) => { (playUri) => {
setDoNavigate(false); setDoNavigate(false);
@ -409,9 +399,6 @@ function VideoViewer(props: Props) {
} }
}); });
// used for tracking buffering for watchman
player.on('tracking:buffered', doTrackingBuffered);
player.on('ended', () => setEnded(true)); player.on('ended', () => setEnded(true));
player.on('play', onPlay); player.on('play', onPlay);
player.on('pause', (event) => onPause(event, player)); player.on('pause', (event) => onPause(event, player));
@ -519,6 +506,7 @@ function VideoViewer(props: Props) {
embedded={embedded} embedded={embedded}
claimValues={claim.value} claimValues={claim.value}
doAnalyticsView={doAnalyticsView} doAnalyticsView={doAnalyticsView}
doAnalyticsBuffer={doAnalyticsBuffer}
claimRewards={claimRewards} claimRewards={claimRewards}
uri={uri} uri={uri}
clearPosition={clearPosition} clearPosition={clearPosition}

View file

@ -480,22 +480,24 @@ export function doAnalyticsView(uri, timeToStart) {
export function doAnalyticsBuffer(uri, bufferData) { export function doAnalyticsBuffer(uri, bufferData) {
return (dispatch, getState) => { return (dispatch, getState) => {
const isLivestream = bufferData.isLivestream;
const state = getState(); const state = getState();
const claim = selectClaimForUri(state, uri); const claim = selectClaimForUri(state, uri);
const user = selectUser(state); const user = selectUser(state);
const { const {
value: { video, audio, source }, value: { video, audio, source },
} = claim; } = claim;
const timeAtBuffer = parseInt(bufferData.currentTime * 1000); const timeAtBuffer = isLivestream ? 0 : parseInt(bufferData.currentTime * 1000);
const bufferDuration = parseInt(bufferData.secondsToLoad * 1000); const bufferDuration = parseInt(bufferData.secondsToLoad * 1000);
const fileDurationInSeconds = (video && video.duration) || (audio && audio.duration); const fileDurationInSeconds = isLivestream ? 0 : (video && video.duration) || (audio && audio.duration);
const fileSize = source.size; // size in bytes const fileSize = isLivestream ? 0 : source.size; // size in bytes
const fileSizeInBits = fileSize * 8; const fileSizeInBits = isLivestream ? '0' : fileSize * 8;
const bitRate = parseInt(fileSizeInBits / fileDurationInSeconds); const bitRate = isLivestream ? bufferData.bitrateAsBitsPerSecond : parseInt(fileSizeInBits / fileDurationInSeconds);
const userId = user && user.id.toString(); const userId = user && user.id.toString();
// if there's a logged in user, send buffer event data to watchman // if there's a logged in user, send buffer event data to watchman
if (userId) { if (userId) {
analytics.videoBufferEvent(claim, { analytics.videoBufferEvent(claim, {
isLivestream,
timeAtBuffer, timeAtBuffer,
bufferDuration, bufferDuration,
bitRate, bitRate,