From fab45df90eaa72308d3eb0117e1f92ec051c7523 Mon Sep 17 00:00:00 2001 From: zeppi Date: Tue, 31 Aug 2021 21:17:38 -0400 Subject: [PATCH 1/3] recsys wip better logging fix floating player popout playing uri bug with recsys lint add empty entries to create use beacon; fire on visibilitychange cleanup, not record recs if not seen ifweb recsys beacon recsys handle embeds, cleanup use history.listen to trigger events fix recsys embed bug bugfix more default data cleaner cleaner --- .flowconfig | 1 + ui/component/claimList/view.jsx | 11 +- ui/component/claimListDiscover/view.jsx | 2 - ui/component/claimPreview/view.jsx | 15 +- ui/component/recommendedContent/index.js | 21 +- ui/component/recommendedContent/view.jsx | 58 ++-- .../internal/plugins/videojs-recsys/plugin.js | 120 ++------ .../viewers/videoViewer/internal/videojs.jsx | 3 + ui/component/viewers/videoViewer/view.jsx | 17 +- ui/recsys.js | 257 ++++++++++++++++++ ui/redux/reducers/content.js | 37 --- ui/redux/selectors/content.js | 32 --- ui/redux/selectors/search.js | 7 +- 13 files changed, 343 insertions(+), 238 deletions(-) create mode 100644 ui/recsys.js diff --git a/.flowconfig b/.flowconfig index ffc05b99e..9e73c0bd0 100644 --- a/.flowconfig +++ b/.flowconfig @@ -31,6 +31,7 @@ module.name_mapper='^modal\(.*\)$' -> '/ui/modal\1' module.name_mapper='^app\(.*\)$' -> '/ui/app\1' module.name_mapper='^native\(.*\)$' -> '/ui/native\1' module.name_mapper='^analytics\(.*\)$' -> '/ui/analytics\1' +module.name_mapper='^recsys\(.*\)$' -> '/ui/recsys\1' module.name_mapper='^rewards\(.*\)$' -> '/ui/rewards\1' module.name_mapper='^i18n\(.*\)$' -> '/ui/i18n\1' module.name_mapper='^effects\(.*\)$' -> '/ui/effects\1' diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 79796ff5e..4da171d4f 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -27,7 +27,6 @@ type Props = { onScrollBottom?: (any) => void, page?: number, pageSize?: number, - id?: string, // If using the default header, this is a unique ID needed to persist the state of the filter setting persistedStorageKey?: string, showHiddenByUser: boolean, @@ -47,7 +46,7 @@ type Props = { searchOptions?: any, collectionId?: string, showNoSourceClaims?: boolean, - onClick?: (e: any, index?: number) => void, + onClick?: (e: any, claim?: ?Claim, index?: number) => void, }; export default function ClaimList(props: Props) { @@ -56,7 +55,6 @@ export default function ClaimList(props: Props) { uris, headerAltControls, loading, - id, persistedStorageKey, empty, defaultSort, @@ -110,9 +108,9 @@ export default function ClaimList(props: Props) { setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW); } - function handleClaimClicked(e, index) { + function handleClaimClicked(e, claim, index) { if (onClick) { - onClick(e, index); + onClick(e, claim, index); } } @@ -200,7 +198,6 @@ export default function ClaimList(props: Props) { {injectedItem && index === 4 &&
  • {injectedItem}
  • } handleClaimClicked(e, claim, index)} /> ))} diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index 99dfb6196..843541054 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -583,7 +583,6 @@ function ClaimListDiscover(props: Props) { )} )} import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */) @@ -67,7 +66,7 @@ type Props = { actions: boolean | Node | string | number, properties: boolean | Node | string | number | ((Claim) => Node), empty?: Node, - onClick?: (e: any, index?: number) => any, + onClick?: (e: any, claim?: ?Claim, index?: number) => any, streamingUrl: ?string, getFile: (string) => void, customShouldHide?: (Claim) => boolean, @@ -90,7 +89,6 @@ type Props = { disableNavigation?: boolean, mediaDuration?: string, date?: any, - containerId?: string, // ID or name of the container (e.g. ClaimList, HOC, etc.) that this is in. indexInContainer?: number, // The index order of this component within 'containerId'. channelSubCount?: number, }; @@ -154,7 +152,6 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { isCollectionMine, collectionUris, disableNavigation, - containerId, indexInContainer, channelSubCount, } = props; @@ -223,7 +220,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { const handleNavLinkClick = (e) => { if (onClick) { - onClick(e, indexInContainer); + onClick(e, claim, indexInContainer); // not sure indexInContainer is used for anything. } e.stopPropagation(); }; @@ -232,10 +229,9 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { to: { pathname: navigateUrl, search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '', - state: containerId ? { [CONTAINER_ID]: containerId } : undefined, }, - onClick: (e) => handleNavLinkClick(e), - onAuxClick: (e) => handleNavLinkClick(e), + onClick: handleNavLinkClick, + onAuxClick: handleNavLinkClick, }; // do not block abandoned and nsfw claims if showUserBlocked is passed @@ -281,14 +277,13 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { function handleOnClick(e) { if (onClick) { - onClick(e, indexInContainer); + onClick(e, claim, indexInContainer); } if (claim && !pending && !disableNavigation) { history.push({ pathname: navigateUrl, search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '', - state: containerId ? { [CONTAINER_ID]: containerId } : undefined, }); } } diff --git a/ui/component/recommendedContent/index.js b/ui/component/recommendedContent/index.js index ea5cf0421..efd71ec50 100644 --- a/ui/component/recommendedContent/index.js +++ b/ui/component/recommendedContent/index.js @@ -7,14 +7,19 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content'; import RecommendedContent from './view'; -const select = (state, props) => ({ - mature: makeSelectClaimIsNsfw(props.uri)(state), - recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state), - nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state), - isSearching: selectIsSearching(state), - isAuthenticated: selectUserVerifiedEmail(state), - claim: makeSelectClaimForUri(props.uri)(state), -}); +const select = (state, props) => { + const claim = makeSelectClaimForUri(props.uri)(state); + const { claim_id: claimId } = claim; + return { + mature: makeSelectClaimIsNsfw(props.uri)(state), + recommendedContentUris: makeSelectRecommendedContentForUri(props.uri)(state), + nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state), + isSearching: selectIsSearching(state), + isAuthenticated: selectUserVerifiedEmail(state), + claim, + claimId, + }; +}; const perform = (dispatch) => ({ doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)), diff --git a/ui/component/recommendedContent/view.jsx b/ui/component/recommendedContent/view.jsx index 3adfa8fcf..fdf366272 100644 --- a/ui/component/recommendedContent/view.jsx +++ b/ui/component/recommendedContent/view.jsx @@ -1,8 +1,6 @@ // @flow import { SHOW_ADS } from 'config'; import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { v4 as uuidv4 } from 'uuid'; import ClaimList from 'component/claimList'; import ClaimListDiscover from 'component/claimListDiscover'; import Ads from 'web/component/ads'; @@ -10,14 +8,14 @@ import Card from 'component/common/card'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import Button from 'component/button'; import classnames from 'classnames'; -import { CONTAINER_ID } from 'constants/navigation'; +import RecSys from 'recsys'; const VIEW_ALL_RELATED = 'view_all_related'; const VIEW_MORE_FROM = 'view_more_from'; type Props = { uri: string, - recommendedContent: Array, + recommendedContentUris: Array, nextRecommendedUri: string, isSearching: boolean, doFetchRecommendedContent: (string, boolean) => void, @@ -25,7 +23,7 @@ type Props = { isAuthenticated: boolean, claim: ?StreamClaim, doRecommendationUpdate: (claimId: string, urls: Array, id: string, parentId: string) => void, - doRecommendationClicked: (claimId: string, index: number) => void, + claimId: string, }; export default React.memo(function RecommendedContent(props: Props) { @@ -33,23 +31,20 @@ export default React.memo(function RecommendedContent(props: Props) { uri, doFetchRecommendedContent, mature, - recommendedContent, + recommendedContentUris, nextRecommendedUri, isSearching, isAuthenticated, claim, - doRecommendationUpdate, - doRecommendationClicked, + claimId, } = props; const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED); - const [recommendationId, setRecommendationId] = React.useState(''); const [recommendationUrls, setRecommendationUrls] = React.useState(); - const history = useHistory(); const signingChannel = claim && claim.signing_channel; const channelName = signingChannel ? signingChannel.name : null; const isMobile = useIsMobile(); const isMedium = useIsMediumScreen(); - + const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys; React.useEffect(() => { function moveAutoplayNextItemToTop(recommendedContent) { let newList = recommendedContent; @@ -72,33 +67,27 @@ export default React.memo(function RecommendedContent(props: Props) { } } - const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContent); + const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContentUris); if (claim && !listEq(recommendationUrls, newRecommendationUrls)) { - const parentId = (history.location.state && history.location.state[CONTAINER_ID]) || ''; - const id = uuidv4(); - setRecommendationId(id); setRecommendationUrls(newRecommendationUrls); - - doRecommendationUpdate(claim.claim_id, newRecommendationUrls, id, parentId); } - }, [ - recommendedContent, - nextRecommendedUri, - recommendationUrls, - setRecommendationUrls, - claim, - doRecommendationUpdate, - history.location.state, - ]); + }, [recommendedContentUris, nextRecommendedUri, recommendationUrls, setRecommendationUrls, claim]); React.useEffect(() => { doFetchRecommendedContent(uri, mature); }, [uri, mature, doFetchRecommendedContent]); - function handleRecommendationClicked(e: any, index: number) { + React.useEffect(() => { + // Right now we only want to record the recs if they actually saw them. + if (recommendationUrls && recommendationUrls.length && nextRecommendedUri && viewMode === VIEW_ALL_RELATED) { + onRecommendationsLoaded(claimId, recommendationUrls); + } + }, [recommendationUrls, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]); + + function handleRecommendationClicked(e, clickedClaim, index: number) { if (claim) { - doRecommendationClicked(claim.claim_id, index); + onRecommendationClicked(claim.claim_id, clickedClaim.claim_id); } } @@ -133,7 +122,6 @@ export default React.memo(function RecommendedContent(props: Props) {
    {viewMode === VIEW_ALL_RELATED && ( response.json()) - .then((data) => { - // console.log(`Recsys response data:`, data); - }); - } catch (error) { - // console.error(`Recsys Error`, error); - } -} - const defaults = { - endpoint: recsysEndpoint, - recsysId: recsysId, videoId: null, userId: 0, debug: false, + embedded: false, }; const Component = videojs.getComponent('Component'); @@ -98,16 +45,13 @@ class RecsysPlugin extends Component { // Plugin started if (options.debug) { - this.log(`Created recsys plugin for: videoId:${options.videoId}, userId:${options.userId}`); + this.log(`Created recsys plugin for: videoId:${options.videoId}`); } // To help with debugging, we'll add a global vjs object with the video js player window.vjs = player; this.player = player; - - this.recsysEvents = []; - this.loadedAt = Date.now(); this.lastTimeUpdate = null; this.currentTimeUpdate = null; this.inPause = false; @@ -124,57 +68,37 @@ class RecsysPlugin extends Component { player.on('dispose', (event) => this.onDispose(event)); } - addRecsysEvent(recsysEvent) { - // For now, don't do client-side preprocessing. I think there - // are browser inconsistencies and preprocessing loses too much info. - this.recsysEvents.push(recsysEvent); - } - - getRecsysEvents() { - return this.recsysEvents; - } - - sendRecsysEvents() { - const event = createRecsys( - this.options_.videoId, - this.options_.userId, - this.getRecsysEvents(), - this.loadedAt, - false - ); - - if (event) { - sendRecsysEvents(event); - } - } - onPlay(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.start, this.player.currentTime()); this.log('onPlay', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent, this.options_.embedded); this.inPause = false; this.lastTimeUpdate = recsysEvent.offset; } onPause(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime()); this.log('onPause', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); this.inPause = true; } onEnded(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime()); this.log('onEnded', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } onRateChange(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime(), this.player.playbackRate()); + const recsysEvent = newRecsysPlayerEvent( + PlayerEvent.event.speed, + this.player.currentTime(), + this.player.playbackRate() + ); this.log('onRateChange', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } onTimeUpdate(event) { @@ -212,19 +136,19 @@ class RecsysPlugin extends Component { if (fromTime !== curTime) { // This removes duplicates that aren't useful. - const recsysEvent = newRecsysEvent(RecsysData.event.scrub, fromTime, curTime); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.scrub, fromTime, curTime); this.log('onSeeked', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } } onDispose(event) { - this.sendRecsysEvents(); + RecSys.onPlayerDispose(this.options_.videoId, this.options_.embedded); } log(...args) { if (this.options_.debug) { - // console.log(`Recsys Debug:`, JSON.stringify(args)); + console.log(`Recsys Player Debug:`, JSON.stringify(args)); } } } diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 41751838c..451bb0b7a 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -54,6 +54,7 @@ type Props = { startMuted: boolean, autoplay: boolean, autoplaySetting: boolean, + embedded: boolean, toggleVideoTheaterMode: () => void, adUrl: ?string, claimId: ?string, @@ -194,6 +195,7 @@ export default React.memo(function VideoJs(props: Props) { const { autoplay, autoplaySetting, + embedded, startMuted, source, sourceType, @@ -590,6 +592,7 @@ export default React.memo(function VideoJs(props: Props) { player.recsys({ videoId: claimId, userId: userId, + embedded: embedded, }); } diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index d7e00b965..905187ded 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -125,7 +125,7 @@ function VideoViewer(props: Props) { const [isPlaying, setIsPlaying] = useState(false); const [ended, setEnded] = useState(false); const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false); - const [isEndededEmbed, setIsEndededEmbed] = useState(false); + const [isEndedEmbed, setIsEndedEmbed] = useState(false); const vjsCallbackDataRef: any = React.useRef(); const previousUri = usePrevious(uri); const embedded = useContext(EmbedContext); @@ -142,7 +142,7 @@ function VideoViewer(props: Props) { useEffect(() => { if (uri && previousUri && uri !== previousUri) { setShowAutoplayCountdown(false); - setIsEndededEmbed(false); + setIsEndedEmbed(false); setIsLoading(false); } }, [uri, previousUri]); @@ -236,7 +236,7 @@ function VideoViewer(props: Props) { } if (embedded) { - setIsEndededEmbed(true); + setIsEndedEmbed(true); } else if (!collectionId && autoplayNext) { setShowAutoplayCountdown(true); } else if (collectionId) { @@ -247,7 +247,7 @@ function VideoViewer(props: Props) { } }, [ embedded, - setIsEndededEmbed, + setIsEndedEmbed, autoplayMedia, setShowAutoplayCountdown, adUrl, @@ -264,7 +264,7 @@ function VideoViewer(props: Props) { setIsLoading(false); setIsPlaying(true); setShowAutoplayCountdown(false); - setIsEndededEmbed(false); + setIsEndedEmbed(false); setReplay(false); setDoNavigate(false); analytics.videoIsPlaying(true, player); @@ -391,7 +391,7 @@ function VideoViewer(props: Props) {
    @@ -403,8 +403,8 @@ function VideoViewer(props: Props) { doReplay={() => setReplay(true)} /> )} - {isEndededEmbed && } - {embedded && !isEndededEmbed && } + {isEndedEmbed && } + {embedded && !isEndedEmbed && } {/* disable this loading behavior because it breaks when player.play() promise hangs */} {isLoading && } @@ -458,6 +458,7 @@ function VideoViewer(props: Props) { videoTheaterMode={videoTheaterMode} playNext={doPlayNext} playPrevious={doPlayPrevious} + embedded={embedded} /> )}
    diff --git a/ui/recsys.js b/ui/recsys.js new file mode 100644 index 000000000..1cd2d65a9 --- /dev/null +++ b/ui/recsys.js @@ -0,0 +1,257 @@ +import { selectUser } from 'redux/selectors/user'; +import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'; +import { v4 as Uuidv4 } from 'uuid'; +import { parseURI, SETTINGS, makeSelectClaimForUri } from 'lbry-redux'; +import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content'; +import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings'; +import { history } from './store'; + +const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; +const recsysId = 'lighthouse-v0'; + +const getClaimIdsFromUris = (uris) => { + return uris + ? uris.map((uri) => { + try { + const { claimId } = parseURI(uri); + return claimId; + } catch (e) { + return []; + } + }) + : []; +}; + +const recsys = { + entries: {}, + debug: true, + /** + * Provides for creating, updating, and sending Clickstream data object Entries. + * Entries are Created either when recommendedContent loads, or when recommendedContent is clicked. + * If recommended content is clicked, An Entry with parentUuid is created. + * On page load, find an empty entry with your claimId, or create a new entry and record to it. + * The entry will be populated with the following: + * - parentUuid // optional + * - Uuid + * - claimId + * - recommendedClaims [] // optionally empty + * - playerEvents [] // optionally empty + * - recommendedClaimsIndexClicked [] // optionally empty + * - UserId + * - pageLoadedAt + * - isEmbed + * - pageExitedAt + * - recsysId // optional + */ + + /** + * Function: onClickedRecommended() + * Called when RecommendedContent was clicked. + * Adds index of clicked recommendation to parent entry + * Adds new Entry with parentUuid for destination page + * @param parentClaimId: string, + * @param newClaimId: string, + */ + onClickedRecommended: function (parentClaimId, newClaimId) { + const parentEntry = recsys.entries[parentClaimId] ? recsys.entries[parentClaimId] : null; + const parentUuid = parentEntry['uuid']; + const parentRecommendedClaims = parentEntry['recClaimIds'] || []; + const parentClickedIndexes = parentEntry['recClickedVideoIdx'] || []; + const indexClicked = parentRecommendedClaims.indexOf(newClaimId); + + if (parentUuid) { + recsys.createRecsysEntry(newClaimId, parentUuid); + } + parentClickedIndexes.push(indexClicked); + recsys.log('onClickedRecommended', { parentClaimId, newClaimId }); + }, + + /** + * Page was loaded. Get or Create entry and populate it with default data, plus recommended content, recsysId, etc. + * Called from recommendedContent component + */ + onRecsLoaded: function (claimId, uris) { + if (window.store) { + const state = window.store.getState(); + if (!recsys.entries[claimId]) { + recsys.createRecsysEntry(claimId); + } + const claimIds = getClaimIdsFromUris(uris); + recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId; + recsys.entries[claimId]['pageLoadedAt'] = Date.now(); + recsys.entries[claimId]['recClaimIds'] = claimIds; + } + recsys.log('onRecsLoaded', claimId); + }, + + /** + * Creates an Entry with optional parentUuid + * @param: claimId: string + * @param: parentUuid: string (optional) + */ + createRecsysEntry: function (claimId, parentUuid) { + if (window.store && claimId) { + const state = window.store.getState(); + const { id: userId } = selectUser(state); + if (parentUuid) { + // Make a stub entry that will be filled out on page load + recsys.entries[claimId] = { + uuid: Uuidv4(), + parentUuid: parentUuid, + uid: userId || null, // selectUser + claimId: claimId, + recClickedVideoIdx: [], + pageLoadedAt: Date.now(), + events: [], + }; + } else { + recsys.entries[claimId] = { + uuid: Uuidv4(), + uid: userId, // selectUser + claimId: claimId, + pageLoadedAt: Date.now(), + recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId, + recClaimIds: [], + recClickedVideoIdx: [], + events: [], + }; + } + } + recsys.log('createRecsysEntry', claimId); + }, + + /** + * Send event for claimId + * @param claimId + * @param isTentative + */ + sendRecsysEntry: function (claimId, isTentative) { + const shareTelemetry = + IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data); + + if (recsys.entries[claimId] && shareTelemetry) { + if (isTentative) { + recsys.entries[claimId]['tentative'] = true; + } + const data = JSON.stringify(recsys.entries[claimId]); + try { + navigator.sendBeacon(recsysEndpoint, data); + if (!isTentative) { + delete recsys.entries[claimId]; + } + } catch (error) { + console.log('no beacon for you', error); + } + } + recsys.log('sendRecsysEntry', claimId); + }, + + /** + * A player event fired. Get the Entry for the claimId, and add the events + * @param claimId + * @param event + */ + onRecsysPlayerEvent: function (claimId, event, isEmbedded) { + if (!recsys.entries[claimId]) { + recsys.createRecsysEntry(claimId); + // do something to show it's floating or autoplay + } + if (isEmbedded) { + recsys.entries[claimId]['isEmbed'] = true; + } + recsys.entries[claimId].events.push(event); + recsys.log('onRecsysPlayerEvent', claimId); + }, + log: function (callName, claimId) { + if (recsys.debug) { + console.log(`Call: ***${callName}***, ClaimId: ${claimId}, Recsys Entries`, Object.assign({}, recsys.entries)); + } + }, + + /** + * Player closed. Check to see if primaryUri = playingUri + * if so, send the Entry. + */ + onPlayerDispose: function (claimId, isEmbedded) { + if (window.store) { + const state = window.store.getState(); + const playingUri = selectPlayingUri(state); + const primaryUri = selectPrimaryUri(state); + const onFilePage = playingUri === primaryUri; + if (!onFilePage || isEmbedded) { + if (isEmbedded) { + recsys.entries[claimId]['isEmbed'] = true; + } + recsys.sendRecsysEntry(claimId); + } + } + recsys.log('PlayerDispose', claimId); + }, + + // /** + // * File page unmount or change event + // * Check to see if playingUri, floatingEnabled, primaryUri === playingUri + // * If not, send the Entry. + // * If floating enabled, leaving file page will pop out player, leading to + // * more events until player is disposed. Don't send unless floatingPlayer playingUri + // */ + // onLeaveFilePage: function (primaryUri) { + // if (window.store) { + // const state = window.store.getState(); + // const claim = makeSelectClaimForUri(primaryUri)(state); + // const claimId = claim ? claim.claim_id : null; + // const playingUri = selectPlayingUri(state); + // const actualPlayingUri = playingUri && playingUri.uri; + // // const primaryUri = selectPrimaryUri(state); + // const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state); + // // When leaving page, if floating player is enabled, play will continue. + // if (claimId) { + // recsys.entries[claimId]['pageExitedAt'] = Date.now(); + // } + // const shouldSend = + // (claimId && floatingPlayer && actualPlayingUri && actualPlayingUri !== primaryUri) || !floatingPlayer || !actualPlayingUri; + // if (shouldSend) { + // recsys.sendRecsysEntry(claimId); + // } + // recsys.log('LeaveFile', claimId); + // } + // }, + + /** + * Navigate event + * Send all claimIds that aren't currently playing. + */ + onNavigate: function () { + if (window.store) { + const state = window.store.getState(); + const playingUri = selectPlayingUri(state); + const actualPlayingUri = playingUri && playingUri.uri; + const claim = makeSelectClaimForUri(actualPlayingUri)(state); + const playingClaimId = claim ? claim.claim_id : null; + // const primaryUri = selectPrimaryUri(state); + const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state); + // When leaving page, if floating player is enabled, play will continue. + Object.keys(recsys.entries).forEach((claimId) => { + const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds; + if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) { + recsys.entries[claimId]['pageExitedAt'] = Date.now(); + recsys.sendRecsysEntry(claimId); + } + recsys.log('OnNavigate', claimId); + }); + } + }, +}; +// @if TARGET='web' +document.addEventListener('visibilitychange', function logData() { + if (document.visibilityState === 'hidden') { + Object.keys(recsys.entries).map((claimId) => recsys.sendRecsysEntry(claimId, true)); + } +}); +// @endif + +history.listen(() => { + recsys.onNavigate(); +}); + +export default recsys; diff --git a/ui/redux/reducers/content.js b/ui/redux/reducers/content.js index b7580d92c..315df9b2a 100644 --- a/ui/redux/reducers/content.js +++ b/ui/redux/reducers/content.js @@ -116,43 +116,6 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => { reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] }); -reducers[ACTIONS.RECOMMENDATION_UPDATED] = (state, action) => { - const { claimId, urls, id, parentId } = action.data; - const recommendationId = Object.assign({}, state.recommendationId); - const recommendationParentId = Object.assign({}, state.recommendationParentId); - const recommendationUrls = Object.assign({}, state.recommendationUrls); - const recommendationClicks = Object.assign({}, state.recommendationClicks); - - if (urls && urls.length > 0) { - recommendationId[claimId] = id; - recommendationParentId[claimId] = parentId; - recommendationUrls[claimId] = urls; - recommendationClicks[claimId] = []; - } else { - delete recommendationId[claimId]; - delete recommendationParentId[claimId]; - delete recommendationUrls[claimId]; - delete recommendationClicks[claimId]; - } - - return { ...state, recommendationId, recommendationParentId, recommendationUrls, recommendationClicks }; -}; - -reducers[ACTIONS.RECOMMENDATION_CLICKED] = (state, action) => { - const { claimId, index } = action.data; - const recommendationClicks = Object.assign({}, state.recommendationClicks); - - if (state.recommendationUrls[claimId] && index >= 0 && index < state.recommendationUrls[claimId].length) { - if (recommendationClicks[claimId]) { - recommendationClicks[claimId].push(index); - } else { - recommendationClicks[claimId] = [index]; - } - } - - return { ...state, recommendationClicks }; -}; - // reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => { // return { // ...state, diff --git a/ui/redux/selectors/content.js b/ui/redux/selectors/content.js index 03f4025a4..208b9818b 100644 --- a/ui/redux/selectors/content.js +++ b/ui/redux/selectors/content.js @@ -3,7 +3,6 @@ import { createSelector } from 'reselect'; import { makeSelectClaimForUri, selectClaimsByUri, - makeSelectClaimsInChannelForCurrentPageState, makeSelectClaimIsNsfw, makeSelectClaimIsMine, makeSelectMediaTypeForUri, @@ -11,7 +10,6 @@ import { parseURI, makeSelectContentTypeForUri, makeSelectFileNameForUri, - selectClaimIdsByUri, } from 'lbry-redux'; import { makeSelectRecommendedContentForUri } from 'redux/selectors/search'; import { selectMutedChannels } from 'redux/selectors/blocked'; @@ -155,18 +153,6 @@ export const selectRecentHistory = createSelector(selectHistory, (history) => { return history.slice(0, RECENT_HISTORY_AMOUNT); }); -export const makeSelectCategoryListUris = (uris: ?Array, channel: string) => - createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), (channelClaims) => { - if (uris) return uris; - - if (channelClaims) { - const CATEGORY_LIST_SIZE = 10; - return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`); - } - - return null; - }); - export const makeSelectShouldObscurePreview = (uri: string) => createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => { return isClaimMature && !showMatureContent; @@ -247,21 +233,3 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) => return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance; } ); - -export const makeSelectRecommendationId = (claimId: string) => - createSelector(selectState, (state) => state.recommendationId[claimId]); - -export const makeSelectRecommendationParentId = (claimId: string) => - createSelector(selectState, (state) => state.recommendationParentId[claimId]); - -export const makeSelectRecommendedClaimIds = (claimId: string) => - createSelector(selectState, selectClaimIdsByUri, (state, claimIdsByUri) => { - const recommendationUrls = state.recommendationUrls[claimId]; - if (recommendationUrls) { - return recommendationUrls.map((url) => claimIdsByUri[url]); - } - return undefined; - }); - -export const makeSelectRecommendationClicks = (claimId: string) => - createSelector(selectState, (state) => state.recommendationClicks[claimId]); diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index 1d97b1976..5005eadfc 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -98,7 +98,7 @@ export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) => createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => { // TODO: DRY this out. let poweredBy; - if (claim) { + if (claim && claimId) { const isMature = isClaimNsfw(claim); const { title } = claim.value; @@ -204,3 +204,8 @@ export const makeSelectIsResolvingWinningUri = (query: string = '') => { } ); }; + +export const makeSelectUrlForClaimId = (claimId: string) => + createSelector( + makeSelectClaimForClaimId(claimId), (claim) => claim ? claim.canonical_url || claim.permanent_url : null + ); -- 2.45.3 From 9af7da4297ebfa86d5a693c5e1c148d9d841eb9a Mon Sep 17 00:00:00 2001 From: zeppi Date: Thu, 2 Sep 2021 17:14:30 -0400 Subject: [PATCH 2/3] remove tentative --- ui/recsys.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/recsys.js b/ui/recsys.js index 1cd2d65a9..e3292ba9c 100644 --- a/ui/recsys.js +++ b/ui/recsys.js @@ -130,9 +130,6 @@ const recsys = { IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data); if (recsys.entries[claimId] && shareTelemetry) { - if (isTentative) { - recsys.entries[claimId]['tentative'] = true; - } const data = JSON.stringify(recsys.entries[claimId]); try { navigator.sendBeacon(recsysEndpoint, data); -- 2.45.3 From fad810ce4a175b27905cde2897ff44f5ae52ba51 Mon Sep 17 00:00:00 2001 From: zeppi Date: Thu, 2 Sep 2021 18:18:30 -0400 Subject: [PATCH 3/3] disable recsys debug logging --- ui/recsys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/recsys.js b/ui/recsys.js index e3292ba9c..f5d8ae106 100644 --- a/ui/recsys.js +++ b/ui/recsys.js @@ -24,7 +24,7 @@ const getClaimIdsFromUris = (uris) => { const recsys = { entries: {}, - debug: true, + debug: false, /** * Provides for creating, updating, and sending Clickstream data object Entries. * Entries are Created either when recommendedContent loads, or when recommendedContent is clicked. -- 2.45.3