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 { parseURI } from 'util/lbryURI';
|
||||
import { getAuthToken } from 'util/saved-passwords';
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||
import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||
|
@ -33,6 +34,7 @@ const getClaimIdsFromUris = (uris) => {
|
|||
const recsys: Recsys = {
|
||||
entries: {},
|
||||
debug: false,
|
||||
|
||||
/**
|
||||
* Provides for creating, updating, and sending Clickstream data object Entries.
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Adds index of clicked recommendation to parent entry
|
||||
|
@ -124,6 +138,8 @@ const recsys: Recsys = {
|
|||
recsys.entries[claimId].recsysId = null;
|
||||
recsys.entries[claimId].recClaimIds = [];
|
||||
}
|
||||
|
||||
recsys.saveEntries();
|
||||
}
|
||||
recsys.log('createRecsysEntry', claimId);
|
||||
},
|
||||
|
@ -131,7 +147,7 @@ const recsys: Recsys = {
|
|||
/**
|
||||
* Send event for claimId
|
||||
* @param claimId
|
||||
* @param isTentative
|
||||
* @param isTentative Visibility change rather than tab closed.
|
||||
*/
|
||||
sendRecsysEntry: function (claimId, isTentative) {
|
||||
const shareTelemetry =
|
||||
|
@ -141,10 +157,6 @@ const recsys: Recsys = {
|
|||
const { events, ...entryData } = recsys.entries[claimId];
|
||||
const data = JSON.stringify(entryData);
|
||||
|
||||
if (!isTentative) {
|
||||
delete recsys.entries[claimId];
|
||||
}
|
||||
|
||||
return fetch(recsysEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -152,13 +164,36 @@ const recsys: Recsys = {
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: data,
|
||||
}).catch((err) => {
|
||||
})
|
||||
.then(() => {
|
||||
if (!isTentative) {
|
||||
delete recsys.entries[claimId];
|
||||
recsys.saveEntries();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('RECSYS: failed to send entry', err);
|
||||
});
|
||||
}
|
||||
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
|
||||
* @param claimId
|
||||
|
@ -260,6 +295,7 @@ const recsys: Recsys = {
|
|||
const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds;
|
||||
if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) {
|
||||
recsys.entries[claimId]['pageExitedAt'] = Date.now();
|
||||
recsys.saveEntries();
|
||||
// recsys.sendRecsysEntry(claimId); breaks pop out = off, not helping with browser close.
|
||||
}
|
||||
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
|
||||
// don't care about the URLs.
|
||||
lastViewedAnnouncement: ?string, // undefined = not seen in wallet.
|
||||
recsysEntries: { [ClaimId]: RecsysEntry }, // Persistent shadow copy. The main one resides in RecSys.
|
||||
};
|
||||
|
||||
declare type WatchHistory = {
|
||||
|
|
3
flow-typed/recsys.js
vendored
3
flow-typed/recsys.js
vendored
|
@ -1,10 +1,13 @@
|
|||
declare type Recsys = {
|
||||
entries: { [ClaimId]: RecsysEntry },
|
||||
debug: boolean,
|
||||
|
||||
saveEntries: () => void,
|
||||
onClickedRecommended: (parentClaimId: ClaimId, newClaimId: ClaimId) => void,
|
||||
onRecsLoaded: (claimId: ClaimId, uris: Array<string>, uuid: string) => void,
|
||||
createRecsysEntry: (claimId: ClaimId, parentUuid?: ?string, uuid?: string) => void,
|
||||
sendRecsysEntry: (claimId: ClaimId, isTentative?: boolean) => ?Promise<?Response>,
|
||||
sendEntries: (entries: ?{ [ClaimId]: RecsysEntry }, isResumedSend: boolean) => void,
|
||||
onRecsysPlayerEvent: (claimId: ClaimId, event: RecsysPlaybackEvent, isEmbedded: boolean) => void,
|
||||
log: (callName: string, claimId: ClaimId) => 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 useInterval from 'effects/use-interval';
|
||||
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_LIMIT = 2000;
|
||||
|
@ -173,6 +174,7 @@ function VideoViewer(props: Props) {
|
|||
|
||||
useInterval(
|
||||
() => {
|
||||
RecSys.saveEntries();
|
||||
if (playerRef.current && isPlaying && !isLivestreamClaim) {
|
||||
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_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||
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_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
||||
|
|
|
@ -44,6 +44,7 @@ import 'scss/all.scss';
|
|||
// @if TARGET='web'
|
||||
// These overrides can't live in web/ because they need to use the same instance of `Lbry`
|
||||
import apiPublishCallViaWeb from 'web/setup/publish';
|
||||
import { doSendPastRecsysEntries } from 'redux/actions/content';
|
||||
|
||||
// Sentry error logging setup
|
||||
// Will only work if you have a SENTRY_AUTH_TOKEN env
|
||||
|
@ -232,6 +233,7 @@ function AppWrapper() {
|
|||
useEffect(() => {
|
||||
if (persistDone) {
|
||||
app.store.dispatch(doToggle3PAnalytics(null, true));
|
||||
app.store.dispatch(doSendPastRecsysEntries());
|
||||
}
|
||||
}, [persistDone]);
|
||||
|
||||
|
|
|
@ -16,10 +16,12 @@ import { makeSelectUrlsForCollectionId } from 'redux/selectors/collections';
|
|||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doPurchaseUri } from 'redux/actions/file';
|
||||
import Lbry from 'lbry';
|
||||
import RecSys from 'recsys';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { selectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||
import { selectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
import { selectIsActiveLivestreamForUri } from 'redux/selectors/livestream';
|
||||
import { selectRecsysEntries } from 'redux/selectors/content';
|
||||
|
||||
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: {},
|
||||
loopList: undefined,
|
||||
lastViewedAnnouncement: '',
|
||||
recsysEntries: {},
|
||||
};
|
||||
|
||||
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_RECSYS_ENTRIES] = (state, action) => ({ ...state, recsysEntries: action.data });
|
||||
|
||||
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||
// return {
|
||||
// ...state,
|
||||
|
|
|
@ -27,6 +27,7 @@ export const selectPrimaryUri = (state: State) => selectState(state).primaryUri;
|
|||
export const selectListLoop = (state: State) => selectState(state).loopList;
|
||||
export const selectListShuffle = (state: State) => selectState(state).shuffleList;
|
||||
export const selectLastViewedAnnouncement = (state: State) => selectState(state).lastViewedAnnouncement;
|
||||
export const selectRecsysEntries = (state: State) => selectState(state).recsysEntries;
|
||||
|
||||
export const makeSelectIsPlaying = (uri: string) =>
|
||||
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', [
|
||||
'fileListPublishedSort',
|
||||
'fileListDownloadedSort',
|
||||
|
|
Loading…
Reference in a new issue