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,
playerLoadedEvent: (string, ?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,
videoBufferEvent: (
StreamClaim,
@ -75,6 +75,7 @@ type Analytics = {
userId: string,
playerPoweredBy: string,
readyState: number,
isLivestream: boolean,
}
) => Promise<any>,
adsFetchedEvent: () => void,
@ -133,7 +134,7 @@ function getDeviceType() {
// variables initialized for watchman
let amountOfBufferEvents = 0;
let amountOfBufferTimeInMS = 0;
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond;
let videoType, userId, claimUrl, playerPoweredBy, videoPlayer, bitrateAsBitsPerSecond, isLivestream;
let lastSentTime;
// calculate data for backend, send them, and reset buffer data for next interval
@ -150,20 +151,27 @@ async function sendAndResetWatchmanData() {
lastSentTime = new Date();
let protocol;
if (videoType === 'application/x-mpegURL') {
if (videoType === 'application/x-mpegURL' && !isLivestream) {
protocol = 'hls';
// get bandwidth if it exists from the texttrack (so it's accurate if user changes quality)
// $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 {
protocol = 'stb';
}
// 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
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
const objectToSend = {
@ -175,8 +183,8 @@ async function sendAndResetWatchmanData() {
protocol,
player: playerPoweredBy,
user_id: userId.toString(),
position: Math.round(positionInVideo),
rel_position: Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
position: isLivestream ? 0 : Math.round(positionInVideo),
rel_position: isLivestream ? 0 : Math.round((positionInVideo / (totalDurationInSeconds * 1000)) * 100),
bitrate: bitrateAsBitsPerSecond,
bandwidth: undefined,
// ...(userDownloadBandwidthInBitsPerSecond && {bandwidth: userDownloadBandwidthInBitsPerSecond}), // add bandwidth if populated
@ -267,16 +275,26 @@ const analytics: Analytics = {
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
userId = passedUserId;
claimUrl = canonicalUrl;
playerPoweredBy = poweredBy;
isLivestream = isLivestreamClaim;
videoType = passedPlayer.currentSource().type;
videoPlayer = passedPlayer;
bitrateAsBitsPerSecond = videoBitrate;
sendPromMetric('time_to_start', timeToStartVideo);
!isLivestreamClaim && sendPromMetric('time_to_start', timeToStartVideo, playerPoweredBy);
},
error: (message) => {
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) {
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();
return fetch(url, { method: 'post' }).catch(function (error) {});
}

View file

@ -22,6 +22,7 @@ const VideoJsEvents = ({
embedded,
uri,
doAnalyticsView,
doAnalyticsBuffer,
claimRewards,
playerServerRef,
isLivestreamClaim,
@ -38,16 +39,28 @@ const VideoJsEvents = ({
clearPosition: (string) => void,
uri: string,
doAnalyticsView: (string, number) => any,
doAnalyticsBuffer: (string, any) => void,
claimRewards: () => void,
playerServerRef: any,
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
* @param e - event from videojs (from the plugin?)
* @param data - only has secondsToLoad property
*/
function doTrackingFirstPlay(e: Event, data: any) {
const playerPoweredBy = isLivestreamClaim ? 'lvs' : playerServerRef.current;
// how long until the video starts
let timeToStartVideo = data.secondsToLoad;
@ -63,10 +76,6 @@ const VideoJsEvents = ({
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
analytics.videoStartEvent(
claimId,
@ -75,7 +84,21 @@ const VideoJsEvents = ({
userId,
uri,
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);
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
player.on('tracking:firstplay', doTrackingFirstPlay);
// used for tracking buffering for watchman
player.on('tracking:buffered', doTrackingBuffered);
// hide forcing control bar show
player.on('canplaythrough', function () {
setTimeout(function () {

View file

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

View file

@ -204,16 +204,6 @@ function VideoViewer(props: Props) {
};
}, [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(
(playUri) => {
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('play', onPlay);
player.on('pause', (event) => onPause(event, player));
@ -519,6 +506,7 @@ function VideoViewer(props: Props) {
embedded={embedded}
claimValues={claim.value}
doAnalyticsView={doAnalyticsView}
doAnalyticsBuffer={doAnalyticsBuffer}
claimRewards={claimRewards}
uri={uri}
clearPosition={clearPosition}

View file

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