Video Tracking – Stages With Code

Going to try to keep this short and simple. However, feel free to ask additional questions / thoughts, etc. There’s a lot to digest and share. So, I may not have hit every point exactly but again, don’t hesitate to ask additional questions, what to do a deeper dive in a topic etc. This just became a very long response that I could probably have continued with an even longer response. Thanks.

Video Metrics

I haven’t seen your video metrics so it’s a little difficult to weigh in. However, if the majority are simply play, stop, maybe length watched, those metrics may not provide a lot of value. What I mean by that, is that they may not be actionable. But if I understand what you are trying to do … is to figure out if a person is ready to bail.

The problem is … all users are different. Some are daily users, some once a month, etc. Users vary and you’d really have to build up a user profile to determine their characteristics. For example, I like coffee but since I only drink one mug – on average, every other day. It may take me about 2 months before I finish my bag of coffee and in the meantime, I have the coffee company sending me emails that “they miss me” and I find that annoying. On the other hand, my mom probably goes through a bag on a weekly basis.

Using GA to track individual users is probably not the best way to track a user. Yes, I use GA to look at some user behavior but most of my tracking is at the whole population. Reason why I don’t use GA – for the most part, to track an individual user is because I don’t get much out of it and secondly, due to quotas, sampling, etc. … there’s not enough granular detail.

Now, I do track video metrics. For example, I track play events. And as part of the play event, I also capture the following (not all inclusive): if it was a scene or movie, the title, the id, the video format, and some other attributes.

Now, what I do with those metrics: I continually monitor the count of play events. If they start skewing (outside the expected range / limits), I start digging in and figuring why we are less (or more) play events than typical. We may have had a release that impacted play events. And with that, I can see if it’s a particular platform (Windows, iOS, etc.), browser, device or is it global. Because if users don’t consume content, we don’t make money since they are not using up their time banks.

Because I track the other meta data, i.e.: title, id, etc. … we use that data to help populate “most popular” / “trending” queues, etc.

Now, I do the above with a variety of GA metrics, i.e.: sign-ups, logins, reset password, etc etc. and continually monitor those metrics to ensure our site is performing well.

So, in short – I do monitor or look at individuals, but I usually look at the total population.

Login

I think that is a very useful metric. Our users can manually login at each visit or set “remember me” in which the system automatically logs the user in. Although you don’t have “remember me”, your login is like our “remember me”.

For metrics, I still track Logins. Even if a user has “remember me”, I still capture a login event. Although the user didn’t physically login, the system still had to login on their behalf. So, when that occurs, I capture a login event. But I also capture other meta information on the event such as: Login Type … manual or remember me (as an example).

But again, I don’t necessarily track individual users. I tried that and it’s like me coffee example above.

Content Preferences

As noted above, we use GA to help populate a variety of queues. So, if you are in a category detail page – say “Bukkake”, a most popular queue will be shown that is focused on Bukkake for that detail page.

We do have some functionality that allows users to include / exclude content. So, self-driven. And users can tag up content. So, we do have favorites, etc. but users can create their own lists, etc.

But I agree, you can use data to help populate queues, etc. with engagement etc. And you can probably get down to the individual level but most of our stuff is at the population level. But you need fall back mechanisms since sometimes there isn’t enough data to display purposes. You either populate additional data or had – for example, the queue if not enough data.

But monitoring content preferences can be tricky. For example, just about every item in the gay library is bare back and little content tagged as Bukkake (example – not sure if that is true). So, can be difficult to monitor since the content type varies (and sometimes tremendously), in a library.

Cohort Analysis

Really good idea. Just define which a group type you’d like to analyze. You can define a variety of types. We tried this. Data was good but it was hard to act on. But again, try some cohort analysis. It’s a great feature that you may need to experiment with.

Other things I would also recommend:

  • create a funnel in GA since it’s now available. That will help with understanding when / where people bail, etc.
  • create a funnel (maybe the same exact funnel) in Clarity. This would be very powerful as you get recordings and can watch users in tandem with the GA metrics. I’ve discovered users do things that I wouldn’t expect, and we fixed some things due to that.

Predictive Modeling

Sure, this would be a very nice feature. May be a little challenging but worth it if able to implement. As I noted above, I try to collect a variety of metrics to understand if a particular metric is within – or not, expected range and if not, why. But also use the metrics to try to determine if things are stable, increasing or decreasing. Or, if we made a change, does it have a positive or not, impact … or no impact.

So, not truly predictive but trying to use data to understand the future. A true Predictive Modeling feature would be very beneficial, but I think it would be a bit of work. For me, I use a variety of methods (with examples above), on trying to monitor the health / use of our applications.

Re-engagement Strategies

We really don’t have one, but we are looking into such strategies. We are also evaluating a rewards system. However, probably like you, we do have the following (examples – not all inclusive):

  • If a new member signs up and after a period of some time, they don’t buy anything, we’ll may send them a note with some free minutes.

  • If a person has a subscription (X-Pass) – this is like your model), and they cancel, we may offer them a discount if they re-subscribe.

    • Just found out, if the user is on the Gay Orientation, they get an offer for a 30 Days Free on NakedSword. So, we are also promoting your white label. Not in production currently but is in active development and scheduled to be part of the upcoming release.

In short though, we are heavily working on re-engagement strategies.

Thanks.

Gary.


MY RESPONSE (make more professional but stil approachable)

thanks – apologies – I read this over the weekend – but just got a chance to get back to you. DAMMIT – I hate it when a good idea – just isn’t one –

Doing my best to keep this short – I know you are a busy guy.

I very much appreciate all the info – looking at it –

it seems like using login data (I have to check how our system “auto logs” in users – that was not in the front end code that I saw)

+ some audience / segmenting (I added a 2 audience groups last week)

should get us on the road to working out churn and doing some cohort analysis – (trying to keep it high level as you suggested)

  • a funnel is a good idea – I actually had some success with the one I did for Tubes (first time it actually coughed up the data I wanted – so feel better about them)
  • clarity funnel as well – I need to put a post-it in the middle of my screen to remind me to check / work in that –

OK – thanks – churn and cohorts are new to me and I wasn’t sure exactly how to get to them – but I can see their value –

  • stage 4 stuff –

    • I can pull back on the play / pause etc. events –
    • but still set up some more concise tracking for the video ads etc. – once we do start using it we will want to have that –

The stages

ALL RELATED TO THIS:

STAGE ONE – just catching the play event – adding calls to analytics and watching the play button to see if it is clicked.

import React, { useEffect } from 'react';
import { useLocation } from '@reach/router';
import { onEventGtag, useGtag } from "../../../hook/useGtag";

const VideoPlayer = () => {
const location = useLocation();

// Track page view on route change
useEffect(() => {
useGtag({
event: 'page_view',
name: 'video_watch',
data: {
page_title: location.pathname, // title extraction Round 2
page_location: window.location.href,
page_path: location.pathname,
event_label: 'video_page',
event_category: 'page_view',
logged: window.user && window.user.logged ? 'Yes' : 'No'
}
});
}, [location]);

// Handle play button click
const handlePlay = () => {
onEventGtag({
event: 'videoPlay',
data: {
event_category: 'video_click',
event_action: 'Play',
event_label: location.pathname,
hostname: window.location.hostname,
logged: window.user && window.user.logged ? 'Yes' : 'No'
}
});
};

return (


Play


);
};

export default VideoPlayer;

STAGE TWO

import React, { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { onEventGtag, useGtag } from "../../../hook/useGtag";
import videojs from 'video.js'; // Assuming you use video.js for the video player

const VideoPlayer = () => {
const videoRef = useRef(null);
const location = useLocation(); // Get current location

// Function to track page view
const trackPageView = () => {
useGtag({
event: 'videoWatch',
data: {
page_title: location.pathname,
page_location: window.location.href,
page_path: location.pathname,
event_label: 'video_page',
event_category: 'video',
event_action: 'video_page_view',
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout'
}
});
};

// Extract movie ID, title, and scene number from the URL
const extractMovieInfo = () => {
const path = location.pathname;
const pathParts = path.split('/');
let movieId = '';
let movieTitle = '';
let sceneNumber = '';

if (pathParts.length >= 3 && pathParts[1] === 'movies') {
movieId = pathParts[2];
}
if (pathParts.length >= 4 && pathParts[1] === 'movies') {
movieTitle = decodeURIComponent(pathParts[3]);
}
if (pathParts.length >= 6 && pathParts[5] === 'scene') {
sceneNumber = pathParts[6];
}

return { movieId, movieTitle, sceneNumber };
};

// Handle play button click
const handlePlay = () => {
const { movieId, movieTitle, sceneNumber } = extractMovieInfo();

// Keeping videoPlay event from stage one
onEventGtag({
event: 'videoPlay',
data: {
event_category: 'video',
event_action: `Play - ${movieId || 'Unknown ID'} - ${movieTitle || 'Unknown Title'} - ${sceneNumber || 'Unknown Scene'}`,
event_label: 'video_click',
event_url: location.pathname,
hostname: window.location.hostname,
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene'
}
});

// Additional push - videoPlayButtonClicked
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPlayButtonClicked',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene',
});
}
};

useEffect(() => {
// route change track
trackPageView();

const videoElement = videoRef.current;
const player = videojs(videoElement);

const handlePlayEvent = () => {
const { movieId, movieTitle, sceneNumber } = extractMovieInfo();

if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPlay',
event_category: 'video_click',
videoStatus: 'playing',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene',
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout'
});
}
};

const handlePause = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPause',
event_category: 'video_click',
videoStatus: 'paused',
});
}
};

const handleEnded = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoEnded',
event_category: 'video_click',
videoStatus: 'ended',
});
}
};

player.on('play', handlePlayEvent);
player.on('pause', handlePause);
player.on('ended', handleEnded);

return () => {
player.off('play', handlePlayEvent);
player.off('pause', handlePause);
player.off('ended', handleEnded);
player.dispose();
};
}, [location]); // Rerun when the location changes

return (




Play



);
};

export default VideoPlayer;

STAGE 3

Stage Three: would capture -Picture-in-Picture, Volume, Full screen button actions.

MEMBERS: Casting, favorites, playlists (button click – full playlist tracking wold be a different event.)

Outside Player Buttons: GoToMovieIcon (to full movie), Stills, Downloads

import React, { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { onEventGtag, useGtag } from "../../../hook/useGtag";
import videojs from 'video.js'; // Assuming you use video.js for the video player
import { FullPlayer } from '@falconstudios/ns-player';

const VideoPlayer = (props) => {
const videoRef = useRef(null);
const location = useLocation(); // Get current location

// Extract memberid from localStorage
const getNatsMemberId = () => {
const userData = localStorage.getItem('fe-user-data');
if (userData) {
try {
const userObj = JSON.parse(userData);
return userObj.memberid || 'loggedoutuser';
} catch (e) {
console.error('Error parsing user data:', e);
return 'loggedoutuser';
}
}
return 'loggedoutuser';
};

const natsMemberId = getNatsMemberId();

// Function to track page view
const trackPageView = () => {
useGtag({
event: 'videoWatch',
data: {
page_title: location.pathname,
page_location: window.location.href,
page_path: location.pathname,
event_label: 'video_page',
event_category: 'video',
event_action: 'video_page_view',
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout',
nats_member_id: natsMemberId
}
});
};

// Extract movie ID, title, and scene number from the URL
const extractMovieInfo = () => {
const path = location.pathname;
const pathParts = path.split('/');
let movieId = '';
let movieTitle = '';
let sceneNumber = '';

if (pathParts.length >= 3 && pathParts[1] === 'movies') {
movieId = pathParts[2];
}
if (pathParts.length >= 4 && pathParts[1] === 'movies') {
movieTitle = decodeURIComponent(pathParts[3]);
}
if (pathParts.length >= 6 && pathParts[5] === 'scene') {
sceneNumber = pathParts[6];
}

return { movieId, movieTitle, sceneNumber };
};

// Handle play button click
const handlePlay = () => {
const { movieId, movieTitle, sceneNumber } = extractMovieInfo();

// Keeping videoPlay event from stage one
onEventGtag({
event: 'videoPlay',
data: {
event_category: 'video',
event_action: `Play - ${movieId || 'Unknown ID'} - ${movieTitle || 'Unknown Title'} - ${sceneNumber || 'Unknown Scene'}`,
event_label: 'video_click',
event_url: location.pathname,
hostname: window.location.hostname,
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene',
nats_member_id: natsMemberId
}
});

// Additional push - videoPlayButtonClicked
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPlayButtonClicked',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene',
nats_member_id: natsMemberId
});
}
};

useEffect(() => {
// Route change track
trackPageView();

const videoElement = videoRef.current;
const player = videojs(videoElement);

const handlePlayEvent = () => {
const { movieId, movieTitle, sceneNumber } = extractMovieInfo();

if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPlay',
event_category: 'video_click',
videoStatus: 'playing',
movieId: movieId || 'Unknown ID',
movieTitle: movieTitle || 'Unknown Title',
sceneNumber: sceneNumber || 'Unknown Scene',
logged: window.user && window.user.logged ? 'Yes' : 'No',
userType: window.user && window.user.userType ? window.user.userType : 'nonmember',
userStatus: window.user && window.user.userStatus ? window.user.userStatus : 'loggedout',
nats_member_id: natsMemberId
});
}
};

const handlePause = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPause',
event_category: 'video_click',
videoStatus: 'paused',
nats_member_id: natsMemberId
});
}
};

const handleEnded = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoEnded',
event_category: 'video_click',
videoStatus: 'ended',
nats_member_id: natsMemberId
});
}
};

// STEP 3 EVENTS

const handlePictureInPicture = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoPictureInPicture',
event_category: 'video_click',
videoStatus: 'picture_in_picture',
nats_member_id: natsMemberId
});
}
};

const handleCasting = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoCasting',
event_category: 'video_click',
videoStatus: 'casting',
nats_member_id: natsMemberId
});
}
};

const handleVolumeChange = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoVolumeChange',
event_category: 'video_click',
videoStatus: 'volume_change',
volume: player.volume(),
nats_member_id: natsMemberId
});
}
};

const handleMute = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoMute',
event_category: 'video_click',
videoStatus: 'muted',
nats_member_id: natsMemberId
});
}
};

const handleFullScreen = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoFullScreen',
event_category: 'video_click',
videoStatus: 'full_screen',
nats_member_id: natsMemberId
});
}
};

const handleScan = () => {
if (window.dataLayer) {
window.dataLayer.push({
event: 'videoScan',
event_category: 'video_click',
videoStatus: 'scanned',
currentTime: player.currentTime(),
nats_member_id: natsMemberId
});
}
};

player.on('play', handlePlayEvent);
player.on('pause', handlePause);
player.on('ended', handleEnded);
player.on('enterpictureinpicture', handlePictureInPicture);
player.on('caststart', handleCasting);
player.on('volumechange', handleVolumeChange);
player.on('mute', handleMute);
player.on('fullscreenchange', handleFullScreen);
player.on('timeupdate', handleScan);

return () => {
player.off('play', handlePlayEvent);
player.off('pause', handlePause);
player.off('ended', handleEnded);
player.off('enterpictureinpicture', handlePictureInPicture);
player.off('caststart', handleCasting);
player.off('volumechange', handleVolumeChange);
player.off('mute', handleMute);
player.off('fullscreenchange', handleFullScreen);
player.off('timeupdate', handleScan);
player.dispose();
};
}, [location]); // Rerun when the location changes

useEffect(() => {
const { movieTitle, sceneNumber } = extractMovieInfo();
if (movieTitle || sceneNumber) {
document.title = `Watching: ${movieTitle || 'Unknown Title'}${sceneNumber ? ` - Scene ${sceneNumber}` : ''}`;
}
}, [location]);

const onPlusIconClick = (movieInfo) => {
// Handle adding to playlist
if (window.dataLayer) {
window.dataLayer.push({
event: 'addToPlaylist',
event_category: 'Playlist',
event_action: 'Add',
movieId: movieInfo.id,
movieTitle: movieInfo.title,
nats_member_id: natsMemberId
});
}
};

const onHeartIconClick = (movieInfo) => {
// Handle adding to favorites
if (window.dataLayer) {
window.dataLayer.push({
event: 'addToFavorites',
event_category: 'Favorites',
event_action: 'Add',
movieId: movieInfo.id,
movieTitle: movieInfo.title,
nats_member_id: natsMemberId
});
}
};

const isUserLoggedIn = () => {
// Check if user is logged in
return Promise.resolve(window.user && window.user.logged ? true : false);
};

const addVideoToViewingHistory = (data) => {
// Handle adding/removing video from viewing history
if (window.dataLayer) {
window.dataLayer.push({
event: 'viewingHistory',
event_category: 'ViewingHistory',
event_action: 'Add',
movieId: data.id,
movieTitle: data.title,
nats_member_id: natsMemberId
});
}
return Promise.resolve();
};

const handleButtonClick = (eventName) => {
const { movieId } = extractMovieInfo();
if (window.dataLayer) {
window.dataLayer.push({
event: eventName,
event_category: 'Navigation',
movieId: movieId || 'Unknown ID',
nats_member_id: natsMemberId
});
}
};

const data = {
ads: [],
onPlusIconClick: onPlusIconClick,
isHeartIconFilled: isHeartIconFilled,
onHeartIconClick: onHeartIconClick,
isUserLoggedIn: isUserLoggedIn,
onAddClick: onAddClick,
onFirstPlay: addVideoToViewingHistory,
onPlayerConfigChange: onPlayerConfigChange,
playerConfiguration,
videos
};

return (

data={data}
onPlayerSetupFinished={onPlayerSetupFinished}
{...props}
/>
handleButtonClick('addToFavorites')}>Add to Favorites
handleButtonClick('addToPlaylist')}>Add to Playlist
handleButtonClick('fullMovieButton')}>Full Movie
handleButtonClick('seeStills')}>See Stills
handleButtonClick('downloadMovie')}>Download Movie

);
};

export default VideoPlayer;

STAGE FOUR

  • NOW pull back on some of the video click / play enents
  • set up tracking for the vidoe ads – we may nto be using it now – but once we do would be good to have that ready