From 486a557d753ec6edf9ca31b9f4ead9ac4e0c28ec Mon Sep 17 00:00:00 2001 From: infinite-persistence <64950861+infinite-persistence@users.noreply.github.com> Date: Wed, 22 Jun 2022 21:43:54 +0800 Subject: [PATCH] Recsys: capture and use `x-uuid` from search results (#1727) * Recsys/FYP: add documentation. * Recsys: capture and use `x-uuid` from search results Ticket: 1717 --- extras/recsys/recsys.js | 24 ++++++++---- flow-typed/search.js | 11 +++++- ui/component/recommendedContent/view.jsx | 1 + ui/redux/actions/search.js | 11 ++++-- ui/redux/reducers/search.js | 4 +- ui/redux/selectors/search.js | 48 +++++++++++------------- ui/util/handle-fetch.js | 3 +- 7 files changed, 60 insertions(+), 42 deletions(-) diff --git a/extras/recsys/recsys.js b/extras/recsys/recsys.js index adbe249e1..8ba27a7d5 100644 --- a/extras/recsys/recsys.js +++ b/extras/recsys/recsys.js @@ -1,8 +1,7 @@ // @flow import { RECSYS_ENDPOINT } from 'config'; import { selectUser } from 'redux/selectors/user'; -import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'; -import { v4 as Uuidv4 } from 'uuid'; +import { selectRecommendedMetaForClaimId } from 'redux/selectors/search'; import { parseURI } from 'util/lbryURI'; import { getAuthToken } from 'util/saved-passwords'; import * as ACTIONS from 'constants/action_types'; @@ -16,7 +15,7 @@ import { selectIsSubscribedForClaimId } from 'redux/selectors/subscriptions'; import { history } from 'ui/store'; const recsysEndpoint = RECSYS_ENDPOINT; -const recsysId = 'lighthouse-v0'; +const DEFAULT_RECSYS_ID = 'lighthouse-v0'; const getClaimIdsFromUris = (uris) => { return uris @@ -81,15 +80,23 @@ const recsys: Recsys = { * Page was loaded. Get or Create entry and populate it with default data, * plus recommended content, recsysId, etc. * Called from recommendedContent component + * + * @param claimId The ID of the content the recommendations are for. + * @param uris The recommended uris for `claimId`. + * @param uuid Specific uuid to use (e.g. for FYP); uses the recommendation's + * uuid otherwise. */ onRecsLoaded: function (claimId, uris, uuid = '') { if (window && window.store) { const state = window.store.getState(); + const recommendedMeta = selectRecommendedMetaForClaimId(state, claimId); + if (!recsys.entries[claimId]) { - recsys.createRecsysEntry(claimId, null, uuid); + recsys.createRecsysEntry(claimId, null, uuid || recommendedMeta.uuid); } + const claimIds = getClaimIdsFromUris(uris); - recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId; + recsys.entries[claimId]['recsysId'] = recommendedMeta.poweredBy || DEFAULT_RECSYS_ID; recsys.entries[claimId]['pageLoadedAt'] = Date.now(); // It is possible that `claimIds` include `null | undefined` entries @@ -107,18 +114,20 @@ const recsys: Recsys = { * Creates an Entry with optional parentUuid * @param: claimId: string * @param: parentUuid: string (optional) - * @param: uuid: string Specific uuid to use. + * @param uuid Specific uuid to use (e.g. for FYP); uses the recommendation's + * uuid otherwise. */ createRecsysEntry: function (claimId, parentUuid, uuid = '') { if (window && window.store && claimId) { const state = window.store.getState(); + const recommendedMeta = selectRecommendedMetaForClaimId(state, claimId); const user = selectUser(state); const userId = user ? user.id : null; // Make a stub entry that will be filled out on page load // $FlowIgnore: not everything is defined since this is a stub recsys.entries[claimId] = { - uuid: uuid || Uuidv4(), + uuid: uuid || recommendedMeta.uuid, claimId: claimId, recClickedVideoIdx: [], pageLoadedAt: Date.now(), @@ -154,6 +163,7 @@ const recsys: Recsys = { IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data); if (recsys.entries[claimId] && shareTelemetry) { + // Exclude `events` in the submission https://github.com/OdyseeTeam/odysee-frontend/issues/1317 const { events, ...entryData } = recsys.entries[claimId]; const data = JSON.stringify(entryData); diff --git a/flow-typed/search.js b/flow-typed/search.js index c53e3ca41..7f016eb74 100644 --- a/flow-typed/search.js +++ b/flow-typed/search.js @@ -28,7 +28,7 @@ declare type SearchOptions = { declare type SearchState = { options: SearchOptions, - resultsByQuery: {}, + resultsByQuery: { [string]: { uris: Array, recsys: string, uuid: string } }, results: Array, hasReachedMaxResultsLength: {}, searching: boolean, @@ -36,6 +36,12 @@ declare type SearchState = { personalRecommendations: { gid: string, uris: Array, fetched: boolean }, }; +declare type SearchResults = { + body: Array<{ name: string, claimId: string}>, + poweredBy: string, + uuid: string, +}; + declare type SearchSuccess = { type: ACTIONS.SEARCH_SUCCESS, data: { @@ -43,7 +49,8 @@ declare type SearchSuccess = { from: number, size: number, uris: Array, - recsys: string, + poweredBy: string, + uuid: string, }, }; diff --git a/ui/component/recommendedContent/view.jsx b/ui/component/recommendedContent/view.jsx index 165fb96ec..6858eccb9 100644 --- a/ui/component/recommendedContent/view.jsx +++ b/ui/component/recommendedContent/view.jsx @@ -88,6 +88,7 @@ export default React.memo(function RecommendedContent(props: Props) { // e.g. never in a floating popup. With that, we can grab the FYP ID from // the search param directly. Otherwise, the parent component would need to // pass it. + // @see https://www.notion.so/FYP-Design-Notes-727782dde2cb485290c530ae96a34285 const { search } = location; const urlParams = new URLSearchParams(search); const fypId = urlParams.get(FYP_ID); diff --git a/ui/redux/actions/search.js b/ui/redux/actions/search.js index 459b8cb30..d93ebba07 100644 --- a/ui/redux/actions/search.js +++ b/ui/redux/actions/search.js @@ -171,11 +171,12 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( const cmd = isSearchingRecommendations ? lighthouse.searchRecommendations : lighthouse.search; cmd(queryWithOptions) - .then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => { - const { body: result, poweredBy } = data; + .then((data: SearchResults) => { + const { body: result, poweredBy, uuid } = data; const uris = processLighthouseResults(result); if (isSearchingRecommendations) { + // Temporarily resolve using `claim_search` until the SDK bug is fixed. const claimIds = result.map((x) => x.claimId); dispatch(doResolveClaimIds(claimIds)).finally(() => { dispatch({ @@ -185,7 +186,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( from: from, size: size, uris, - recsys: poweredBy, + poweredBy, + uuid, }, }); }); @@ -201,7 +203,8 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( from: from, size: size, uris, - recsys: poweredBy, + poweredBy, + uuid, }, }); diff --git a/ui/redux/reducers/search.js b/ui/redux/reducers/search.js index 7aa2bdf7e..d96110945 100644 --- a/ui/redux/reducers/search.js +++ b/ui/redux/reducers/search.js @@ -32,7 +32,7 @@ export default handleActions( searching: true, }), [ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => { - const { query, uris, from, size, recsys } = action.data; + const { query, uris, from, size, poweredBy: recsys, uuid } = action.data; const normalizedQuery = createNormalizedSearchKey(query); const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris']; @@ -44,7 +44,7 @@ export default handleActions( // The returned number of urls is less than the page size, so we're on the last page const noMoreResults = size && uris.length < size; - const results = { uris: newUris, recsys }; + const results = { uris: newUris, recsys, uuid }; return { ...state, searching: false, diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index b8f02a7bd..48c2ec490 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -2,6 +2,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectClaimsByUri, + selectClaimForClaimId, makeSelectClaimForUri, makeSelectClaimForClaimId, selectClaimIsNsfwForUri, @@ -25,8 +26,7 @@ export const selectState = (state: State): SearchState => state.search; export const selectSearchValue: (state: State) => string = (state) => selectState(state).searchQuery; export const selectSearchOptions: (state: State) => SearchOptions = (state) => selectState(state).options; export const selectIsSearching: (state: State) => boolean = (state) => selectState(state).searching; -export const selectSearchResultByQuery: (state: State) => { [string]: Array } = (state) => - selectState(state).resultsByQuery; +export const selectSearchResultByQuery = (state: State) => selectState(state).resultsByQuery; export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array } = (state) => selectState(state).hasReachedMaxResultsLength; export const selectMentionSearchResults: (state: State) => Array = (state) => selectState(state).results; @@ -142,34 +142,30 @@ export const selectRecommendedContentForUri = createCachedSelector( } )((state, uri) => String(uri)); -export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) => - createSelector( - makeSelectClaimForClaimId(claimId), - selectShowMatureContent, - selectSearchResultByQuery, - (claim, matureEnabled, searchUrisByQuery) => { - // TODO: DRY this out. - let poweredBy; - if (claim && claimId) { - const isMature = isClaimNsfw(claim); - const { title } = claim.value; - if (!title) { - return; - } +export const selectRecommendedMetaForClaimId = createCachedSelector( + selectClaimForClaimId, + selectShowMatureContent, + selectSearchResultByQuery, + (claim, matureEnabled, searchUrisByQuery) => { + if (claim && claim?.value?.title && claim.claim_id) { + const isMature = isClaimNsfw(claim); + const title = claim.value.title; - const options = getRecommendationSearchOptions(matureEnabled, isMature, claimId); - const normalizedSearchQuery = getRecommendationSearchKey(title, options); + const options = getRecommendationSearchOptions(matureEnabled, isMature, claim.claim_id); + const normalizedSearchQuery = getRecommendationSearchKey(title, options); - const searchResult = searchUrisByQuery[normalizedSearchQuery]; - if (searchResult) { - poweredBy = searchResult.recsys; - } else { - return normalizedSearchQuery; - } + const searchResult = searchUrisByQuery[normalizedSearchQuery]; + if (searchResult) { + return { + poweredBy: searchResult.recsys, + uuid: searchResult.uuid, + }; + } else { + return normalizedSearchQuery; } - return poweredBy; } - ); + } +)((state, claimId) => String(claimId)); export const makeSelectWinningUriForQuery = (query: string) => { const uriFromQuery = `lbry://${query}`; diff --git a/ui/util/handle-fetch.js b/ui/util/handle-fetch.js index 4686b9b1a..8abe4332c 100644 --- a/ui/util/handle-fetch.js +++ b/ui/util/handle-fetch.js @@ -2,8 +2,9 @@ export default function handleFetchResponse(response: Response): Promise { const headers = response.headers; const poweredBy = headers.get('x-powered-by'); + const uuid = headers.get('x-uuid'); return response.status === 200 - ? response.json().then((body) => ({ body, poweredBy })) + ? response.json().then((body) => ({ body, poweredBy, uuid })) : Promise.reject(new Error(response.statusText)); }