include time to start if available in file_view analytics

This commit is contained in:
seanyesmunt 2018-08-03 11:54:10 -04:00
parent 2b1a6ce3e3
commit c5c62a2698
4 changed files with 119 additions and 36 deletions

View file

@ -44,13 +44,19 @@ const analytics: Analytics = {
} }
analyticsEnabled = enabled; analyticsEnabled = enabled;
}, },
apiLogView: (uri: string, outpoint: string, claimId: string): void => { apiLogView: (uri: string, outpoint: string, claimId: string, timeToStart?: number): void => {
if (analyticsEnabled) { if (analyticsEnabled) {
Lbryio.call('file', 'view', { const params = {
uri, uri,
outpoint, outpoint,
claim_id: claimId, claim_id: claimId,
}).catch(() => {}); };
if (timeToStart) {
params.time_to_start = timeToStart;
}
Lbryio.call('file', 'view', params).catch(() => {});
} }
}, },
}; };

View file

@ -34,10 +34,21 @@ class VideoPlayer extends React.PureComponent {
componentDidMount() { componentDidMount() {
const container = this.media; const container = this.media;
const { downloadCompleted, contentType, changeVolume, volume, position, claim } = this.props; const {
downloadCompleted,
contentType,
changeVolume,
volume,
position,
claim,
startedPlayingCb,
} = this.props;
const loadedMetadata = () => { const loadedMetadata = () => {
this.setState({ hasMetadata: true, startedPlaying: true }); this.setState({ hasMetadata: true, startedPlaying: true });
if (startedPlayingCb) {
startedPlayingCb();
}
this.media.children[0].play(); this.media.children[0].play();
}; };

View file

@ -1,6 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import analytics from 'analytics';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import Player from './internal/player'; import Player from './internal/player';
@ -48,17 +49,48 @@ type Props = {
class FileViewer extends React.PureComponent<Props> { class FileViewer extends React.PureComponent<Props> {
constructor() { constructor() {
super(); super();
(this: any).playContent = this.playContent.bind(this); (this: any).playContent = this.playContent.bind(this);
(this: any).handleKeyDown = this.handleKeyDown.bind(this); (this: any).handleKeyDown = this.handleKeyDown.bind(this);
(this: any).logTimeToStart = this.logTimeToStart.bind(this);
(this: any).startedPlayingCb = undefined;
// Don't add these variables to state because we don't need to re-render when their values change
(this: any).startTime = undefined;
(this: any).playTime = undefined;
} }
componentDidMount() { componentDidMount() {
const { fileInfo } = this.props;
if (!fileInfo) {
this.startedPlayingCb = this.logTimeToStart;
}
this.handleAutoplay(this.props); this.handleAutoplay(this.props);
window.addEventListener('keydown', this.handleKeyDown); window.addEventListener('keydown', this.handleKeyDown);
} }
componentDidUpdate(prev: Props) { componentDidUpdate(prev: Props) {
const { fileInfo } = this.props;
if (this.props.uri !== prev.uri) {
// User just directly navigated to another piece of content
if (this.startTime && !this.playTime) {
// They started playing a file but it didn't start streaming
// Fire the analytics event with the previous file
this.fireAnalyticsEvent(prev.claim);
}
this.startTime = null;
this.playTime = null;
// If this new file is already downloaded, remove the startedPlayingCallback
if (fileInfo && this.startedPlayingCb) {
this.startedPlayingCb = null;
} else if (!fileInfo && !this.startedPlayingCb) {
this.startedPlayingCb = this.logTimeToStart;
}
}
if ( if (
this.props.autoplay !== prev.autoplay || this.props.autoplay !== prev.autoplay ||
this.props.fileInfo !== prev.fileInfo || this.props.fileInfo !== prev.fileInfo ||
@ -73,6 +105,14 @@ class FileViewer extends React.PureComponent<Props> {
} }
componentWillUnmount() { componentWillUnmount() {
const { claim } = this.props;
if (this.startTime && !this.playTime) {
// The user is navigating away before the file started playing, or a play time was never set
// Currently will not be set for files that don't use render-media
this.fireAnalyticsEvent(claim);
}
this.props.cancelPlay(); this.props.cancelPlay();
window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keydown', this.handleKeyDown);
} }
@ -107,9 +147,42 @@ class FileViewer extends React.PureComponent<Props> {
playContent() { playContent() {
const { play, uri } = this.props; const { play, uri } = this.props;
if (this.startedPlayingCb) {
this.startTime = Date.now();
}
play(uri); play(uri);
} }
logTimeToStart() {
const { claim } = this.props;
if (this.startTime) {
this.playTime = Date.now();
this.fireAnalyticsEvent(claim, this.startTime, this.playTime);
}
}
fireAnalyticsEvent = (claim, startTime, playTime) => {
const { name, claim_id: claimId, txid, nout } = claim;
// ideally outpoint would exist inside of claim information
// we can use it after https://github.com/lbryio/lbry/issues/1306 is addressed
const outpoint = `${txid}:${nout}`;
let timeToStart;
if (playTime && startTime) {
timeToStart = playTime - startTime;
}
analytics.apiLogView(`${name}#${claimId}`, outpoint, claimId, timeToStart);
};
startedPlayingCb: ?() => void;
startTime: ?number;
playTime: ?number;
render() { render() {
const { const {
metadata, metadata,
@ -178,6 +251,7 @@ class FileViewer extends React.PureComponent<Props> {
uri={uri} uri={uri}
paused={mediaPaused} paused={mediaPaused}
position={mediaPosition} position={mediaPosition}
startedPlayingCb={this.startedPlayingCb}
/> />
)} )}
</div> </div>

View file

@ -31,7 +31,6 @@ import {
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import setBadge from 'util/setBadge'; import setBadge from 'util/setBadge';
import setProgressBar from 'util/setProgressBar'; import setProgressBar from 'util/setProgressBar';
import analytics from 'analytics';
const DOWNLOAD_POLL_INTERVAL = 250; const DOWNLOAD_POLL_INTERVAL = 250;
@ -224,13 +223,6 @@ export function doStartDownload(uri, outpoint) {
export function doDownloadFile(uri, streamInfo) { export function doDownloadFile(uri, streamInfo) {
return dispatch => { return dispatch => {
dispatch(doStartDownload(uri, streamInfo.outpoint)); dispatch(doStartDownload(uri, streamInfo.outpoint));
analytics.apiLogView(
`${streamInfo.claim_name}#${streamInfo.claim_id}`,
streamInfo.outpoint,
streamInfo.claim_id
);
dispatch(doClaimEligiblePurchaseRewards()); dispatch(doClaimEligiblePurchaseRewards());
}; };
} }
@ -244,6 +236,29 @@ export function doSetPlayingUri(uri) {
}; };
} }
function handleLoadVideoError(uri, errorType = '') {
return (dispatch, getState) => {
// suppress error when another media is playing
const { playingUri } = getState().content;
if (playingUri && playingUri === uri) {
dispatch({
type: ACTIONS.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doSetPlayingUri(null));
if (errorType === 'timeout') {
doNotify({ id: MODALS.FILE_TIMEOUT }, { uri });
} else {
dispatch(
doAlertError(
`Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.io/faq/support for support.`
)
);
}
}
};
}
export function doLoadVideo(uri) { export function doLoadVideo(uri) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
@ -270,29 +285,6 @@ export function doLoadVideo(uri) {
}; };
} }
function handleLoadVideoError(uri, errorType = '') {
return (dispatch, getState) => {
// suppress error when another media is playing
const { playingUri } = getState().content;
if (playingUri && playingUri === uri) {
dispatch({
type: ACTIONS.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doSetPlayingUri(null));
if (errorType === 'timeout') {
doNotify({ id: MODALS.FILE_TIMEOUT }, { uri });
} else {
dispatch(
doAlertError(
`Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.io/faq/support for support.`
)
);
}
}
};
}
export function doPurchaseUri(uri, specificCostInfo) { export function doPurchaseUri(uri, specificCostInfo) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();