1318: Persist recsys data for send after tab close
- Add ability to store `entries` into Redux. - Sync to redux in the same interval as when playing position is saved (re-use timer). - On startup, send any stashed entries and clear them.
This commit is contained in:
parent
ac44b771ff
commit
6d6d95237a
10 changed files with 87 additions and 9 deletions
|
@ -5,6 +5,7 @@ import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'
|
||||||
import { v4 as Uuidv4 } from 'uuid';
|
import { v4 as Uuidv4 } from 'uuid';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import { getAuthToken } from 'util/saved-passwords';
|
import { getAuthToken } from 'util/saved-passwords';
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
|
@ -33,6 +34,7 @@ const getClaimIdsFromUris = (uris) => {
|
||||||
const recsys: Recsys = {
|
const recsys: Recsys = {
|
||||||
entries: {},
|
entries: {},
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides for creating, updating, and sending Clickstream data object Entries.
|
* Provides for creating, updating, and sending Clickstream data object Entries.
|
||||||
* Entries are Created either when recommendedContent loads, or when recommendedContent is clicked.
|
* Entries are Created either when recommendedContent loads, or when recommendedContent is clicked.
|
||||||
|
@ -40,6 +42,18 @@ const recsys: Recsys = {
|
||||||
* On page load, find an empty entry with your claimId, or create a new entry and record to it.
|
* On page load, find an empty entry with your claimId, or create a new entry and record to it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves existing entries to persistence storage (in this case, Redux).
|
||||||
|
*/
|
||||||
|
saveEntries: function () {
|
||||||
|
if (window && window.store) {
|
||||||
|
window.store.dispatch({
|
||||||
|
type: ACTIONS.SET_RECSYS_ENTRIES,
|
||||||
|
data: recsys.entries,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when RecommendedContent was clicked.
|
* Called when RecommendedContent was clicked.
|
||||||
* Adds index of clicked recommendation to parent entry
|
* Adds index of clicked recommendation to parent entry
|
||||||
|
@ -124,6 +138,8 @@ const recsys: Recsys = {
|
||||||
recsys.entries[claimId].recsysId = null;
|
recsys.entries[claimId].recsysId = null;
|
||||||
recsys.entries[claimId].recClaimIds = [];
|
recsys.entries[claimId].recClaimIds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recsys.saveEntries();
|
||||||
}
|
}
|
||||||
recsys.log('createRecsysEntry', claimId);
|
recsys.log('createRecsysEntry', claimId);
|
||||||
},
|
},
|
||||||
|
@ -131,7 +147,7 @@ const recsys: Recsys = {
|
||||||
/**
|
/**
|
||||||
* Send event for claimId
|
* Send event for claimId
|
||||||
* @param claimId
|
* @param claimId
|
||||||
* @param isTentative
|
* @param isTentative Visibility change rather than tab closed.
|
||||||
*/
|
*/
|
||||||
sendRecsysEntry: function (claimId, isTentative) {
|
sendRecsysEntry: function (claimId, isTentative) {
|
||||||
const shareTelemetry =
|
const shareTelemetry =
|
||||||
|
@ -141,10 +157,6 @@ const recsys: Recsys = {
|
||||||
const { events, ...entryData } = recsys.entries[claimId];
|
const { events, ...entryData } = recsys.entries[claimId];
|
||||||
const data = JSON.stringify(entryData);
|
const data = JSON.stringify(entryData);
|
||||||
|
|
||||||
if (!isTentative) {
|
|
||||||
delete recsys.entries[claimId];
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(recsysEndpoint, {
|
return fetch(recsysEndpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -152,13 +164,36 @@ const recsys: Recsys = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: data,
|
body: data,
|
||||||
}).catch((err) => {
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (!isTentative) {
|
||||||
|
delete recsys.entries[claimId];
|
||||||
|
recsys.saveEntries();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
console.log('RECSYS: failed to send entry', err);
|
console.log('RECSYS: failed to send entry', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
recsys.log('sendRecsysEntry', claimId);
|
recsys.log('sendRecsysEntry', claimId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sendEntries: function (entries, isResumedSend) {
|
||||||
|
if (entries) {
|
||||||
|
if (Object.keys(recsys.entries).length !== 0) {
|
||||||
|
// Typically called on startup only.
|
||||||
|
console.warn('RECSYS: sendEntries() called on non-empty state. Data will be overwritten.');
|
||||||
|
}
|
||||||
|
|
||||||
|
recsys.entries = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(recsys.entries).forEach((claimId) => {
|
||||||
|
recsys.entries[claimId].isResumedSend = isResumedSend;
|
||||||
|
recsys.sendRecsysEntry(claimId, false); // Send and delete.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A player event fired. Get the Entry for the claimId, and add the events
|
* A player event fired. Get the Entry for the claimId, and add the events
|
||||||
* @param claimId
|
* @param claimId
|
||||||
|
@ -260,6 +295,7 @@ const recsys: Recsys = {
|
||||||
const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds;
|
const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds;
|
||||||
if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) {
|
if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) {
|
||||||
recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
||||||
|
recsys.saveEntries();
|
||||||
// recsys.sendRecsysEntry(claimId); breaks pop out = off, not helping with browser close.
|
// recsys.sendRecsysEntry(claimId); breaks pop out = off, not helping with browser close.
|
||||||
}
|
}
|
||||||
recsys.log('OnNavigate', claimId);
|
recsys.log('OnNavigate', claimId);
|
||||||
|
|
1
flow-typed/content.js
vendored
1
flow-typed/content.js
vendored
|
@ -16,6 +16,7 @@ declare type ContentState = {
|
||||||
// can cast it to a boolean. That, or rename the variable to `shuffle` if you
|
// can cast it to a boolean. That, or rename the variable to `shuffle` if you
|
||||||
// don't care about the URLs.
|
// don't care about the URLs.
|
||||||
lastViewedAnnouncement: ?string, // undefined = not seen in wallet.
|
lastViewedAnnouncement: ?string, // undefined = not seen in wallet.
|
||||||
|
recsysEntries: { [ClaimId]: RecsysEntry }, // Persistent shadow copy. The main one resides in RecSys.
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type WatchHistory = {
|
declare type WatchHistory = {
|
||||||
|
|
3
flow-typed/recsys.js
vendored
3
flow-typed/recsys.js
vendored
|
@ -1,10 +1,13 @@
|
||||||
declare type Recsys = {
|
declare type Recsys = {
|
||||||
entries: { [ClaimId]: RecsysEntry },
|
entries: { [ClaimId]: RecsysEntry },
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
|
|
||||||
|
saveEntries: () => void,
|
||||||
onClickedRecommended: (parentClaimId: ClaimId, newClaimId: ClaimId) => void,
|
onClickedRecommended: (parentClaimId: ClaimId, newClaimId: ClaimId) => void,
|
||||||
onRecsLoaded: (claimId: ClaimId, uris: Array<string>, uuid: string) => void,
|
onRecsLoaded: (claimId: ClaimId, uris: Array<string>, uuid: string) => void,
|
||||||
createRecsysEntry: (claimId: ClaimId, parentUuid?: ?string, uuid?: string) => void,
|
createRecsysEntry: (claimId: ClaimId, parentUuid?: ?string, uuid?: string) => void,
|
||||||
sendRecsysEntry: (claimId: ClaimId, isTentative?: boolean) => ?Promise<?Response>,
|
sendRecsysEntry: (claimId: ClaimId, isTentative?: boolean) => ?Promise<?Response>,
|
||||||
|
sendEntries: (entries: ?{ [ClaimId]: RecsysEntry }, isResumedSend: boolean) => void,
|
||||||
onRecsysPlayerEvent: (claimId: ClaimId, event: RecsysPlaybackEvent, isEmbedded: boolean) => void,
|
onRecsysPlayerEvent: (claimId: ClaimId, event: RecsysPlaybackEvent, isEmbedded: boolean) => void,
|
||||||
log: (callName: string, claimId: ClaimId) => void,
|
log: (callName: string, claimId: ClaimId) => void,
|
||||||
onPlayerDispose: (claimId: ClaimId, isEmbedded: boolean, totalPlayingTime: number) => void,
|
onPlayerDispose: (claimId: ClaimId, isEmbedded: boolean, totalPlayingTime: number) => void,
|
||||||
|
|
|
@ -29,6 +29,7 @@ import debounce from 'util/debounce';
|
||||||
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
|
||||||
import useInterval from 'effects/use-interval';
|
import useInterval from 'effects/use-interval';
|
||||||
import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors';
|
import { lastBandwidthSelector } from './internal/plugins/videojs-http-streaming--override/playlist-selectors';
|
||||||
|
import RecSys from 'recsys';
|
||||||
|
|
||||||
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||||
// const PLAY_TIMEOUT_LIMIT = 2000;
|
// const PLAY_TIMEOUT_LIMIT = 2000;
|
||||||
|
@ -173,6 +174,7 @@ function VideoViewer(props: Props) {
|
||||||
|
|
||||||
useInterval(
|
useInterval(
|
||||||
() => {
|
() => {
|
||||||
|
RecSys.saveEntries();
|
||||||
if (playerRef.current && isPlaying && !isLivestreamClaim) {
|
if (playerRef.current && isPlaying && !isLivestreamClaim) {
|
||||||
handlePosition(playerRef.current);
|
handlePosition(playerRef.current);
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,6 +144,7 @@ export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
||||||
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
||||||
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||||
export const SET_LAST_VIEWED_ANNOUNCEMENT = 'SET_LAST_VIEWED_ANNOUNCEMENT';
|
export const SET_LAST_VIEWED_ANNOUNCEMENT = 'SET_LAST_VIEWED_ANNOUNCEMENT';
|
||||||
|
export const SET_RECSYS_ENTRIES = 'SET_RECSYS_ENTRIES';
|
||||||
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
||||||
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||||
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
||||||
|
|
|
@ -44,6 +44,7 @@ import 'scss/all.scss';
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
// These overrides can't live in web/ because they need to use the same instance of `Lbry`
|
// These overrides can't live in web/ because they need to use the same instance of `Lbry`
|
||||||
import apiPublishCallViaWeb from 'web/setup/publish';
|
import apiPublishCallViaWeb from 'web/setup/publish';
|
||||||
|
import { doSendPastRecsysEntries } from 'redux/actions/content';
|
||||||
|
|
||||||
// Sentry error logging setup
|
// Sentry error logging setup
|
||||||
// Will only work if you have a SENTRY_AUTH_TOKEN env
|
// Will only work if you have a SENTRY_AUTH_TOKEN env
|
||||||
|
@ -232,6 +233,7 @@ function AppWrapper() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persistDone) {
|
if (persistDone) {
|
||||||
app.store.dispatch(doToggle3PAnalytics(null, true));
|
app.store.dispatch(doToggle3PAnalytics(null, true));
|
||||||
|
app.store.dispatch(doSendPastRecsysEntries());
|
||||||
}
|
}
|
||||||
}, [persistDone]);
|
}, [persistDone]);
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,12 @@ import { makeSelectUrlsForCollectionId } from 'redux/selectors/collections';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doPurchaseUri } from 'redux/actions/file';
|
import { doPurchaseUri } from 'redux/actions/file';
|
||||||
import Lbry from 'lbry';
|
import Lbry from 'lbry';
|
||||||
|
import RecSys from 'recsys';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { selectCostInfoForUri, Lbryio } from 'lbryinc';
|
import { selectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||||
import { selectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
import { selectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
|
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
|
||||||
|
import { selectRecsysEntries } from 'redux/selectors/content';
|
||||||
|
|
||||||
const DOWNLOAD_POLL_INTERVAL = 1000;
|
const DOWNLOAD_POLL_INTERVAL = 1000;
|
||||||
|
|
||||||
|
@ -383,3 +385,30 @@ export function doSetLastViewedAnnouncement(hash: string) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doSetRecsysEntries(entries: { [ClaimId]: RecsysEntry }) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SET_RECSYS_ENTRIES,
|
||||||
|
data: entries,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends any lingering recsys entries from the previous session and deletes it.
|
||||||
|
*
|
||||||
|
* Should only be called on startup, before a new cycle of recsys data is
|
||||||
|
* collected.
|
||||||
|
*
|
||||||
|
* @returns {(function(Dispatch, GetState): void)|*}
|
||||||
|
*/
|
||||||
|
export function doSendPastRecsysEntries() {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const entries = selectRecsysEntries(state);
|
||||||
|
if (entries) {
|
||||||
|
RecSys.sendEntries(entries, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ const defaultState: ContentState = {
|
||||||
recommendationClicks: {},
|
recommendationClicks: {},
|
||||||
loopList: undefined,
|
loopList: undefined,
|
||||||
lastViewedAnnouncement: '',
|
lastViewedAnnouncement: '',
|
||||||
|
recsysEntries: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
|
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
|
||||||
|
@ -121,6 +122,8 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [
|
||||||
|
|
||||||
reducers[ACTIONS.SET_LAST_VIEWED_ANNOUNCEMENT] = (state, action) => ({ ...state, lastViewedAnnouncement: action.data });
|
reducers[ACTIONS.SET_LAST_VIEWED_ANNOUNCEMENT] = (state, action) => ({ ...state, lastViewedAnnouncement: action.data });
|
||||||
|
|
||||||
|
reducers[ACTIONS.SET_RECSYS_ENTRIES] = (state, action) => ({ ...state, recsysEntries: action.data });
|
||||||
|
|
||||||
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||||
// return {
|
// return {
|
||||||
// ...state,
|
// ...state,
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const selectPrimaryUri = (state: State) => selectState(state).primaryUri;
|
||||||
export const selectListLoop = (state: State) => selectState(state).loopList;
|
export const selectListLoop = (state: State) => selectState(state).loopList;
|
||||||
export const selectListShuffle = (state: State) => selectState(state).shuffleList;
|
export const selectListShuffle = (state: State) => selectState(state).shuffleList;
|
||||||
export const selectLastViewedAnnouncement = (state: State) => selectState(state).lastViewedAnnouncement;
|
export const selectLastViewedAnnouncement = (state: State) => selectState(state).lastViewedAnnouncement;
|
||||||
|
export const selectRecsysEntries = (state: State) => selectState(state).recsysEntries;
|
||||||
|
|
||||||
export const makeSelectIsPlaying = (uri: string) =>
|
export const makeSelectIsPlaying = (uri: string) =>
|
||||||
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
|
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
|
||||||
|
|
|
@ -45,7 +45,7 @@ function enableBatching(reducer) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentFilter = createFilter('content', ['positions', 'history', 'lastViewedAnnouncement']);
|
const contentFilter = createFilter('content', ['positions', 'history', 'lastViewedAnnouncement', 'recsysEntries']);
|
||||||
const fileInfoFilter = createFilter('fileInfo', [
|
const fileInfoFilter = createFilter('fileInfo', [
|
||||||
'fileListPublishedSort',
|
'fileListPublishedSort',
|
||||||
'fileListDownloadedSort',
|
'fileListDownloadedSort',
|
||||||
|
|
Loading…
Reference in a new issue