diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js new file mode 100644 index 0000000..f26b78b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js @@ -0,0 +1,386 @@ +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { PageRefreshPolicy, SearchAction, SearchFocusPage, SearchLandingPage, Shelf } from "../../api/models"; +import * as mediaRequestUtils from "../../common/builders/url-mapping-utils"; +import * as appStoreExperiments from "../../foundation/experimentation/app-store-experiments"; +import { ExperimentAreaId } from "../../foundation/experimentation/experiment-area-id"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; +import { iadInfoFromOnDeviceAdResponse, isAdPlacementEnabled } from "../ads/ad-common"; +import * as adIncidents from "../ads/ad-incident-recorder"; +import { fetchAds as landingAdFetchFetchAds } from "../ads/on-device-ad-fetch"; +import * as landingAdStitch from "../ads/on-device-ad-stitch"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as groupingCommon from "../grouping/grouping-common"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { areAppTagsEnabled } from "../util/app-tags-util"; +import { isFeatureEnabledForCurrentUser } from "../util/lottery"; +import { SearchPageType } from "./content/search-shelves"; +import * as searchLandingCohort from "./landing/search-landing-cohort"; +import * as searchLandingShelfController from "./landing/search-landing-shelf-controller"; +/** + * Determines whether or not the user will use the legacy Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @returns Whether or not the user will use the legacy Search Landing Page protocol + */ +function shouldUseProtocolV1(objectGraph) { + if (objectGraph.client.isVision) { + return false; // visionOS always uses the modern V2 protocol. + } + if (objectGraph.client.isWeb) { + return false; // the "web" expects to use the V2 protocol as well + } + if (!objectGraph.bag.supportsSearchLandingPageV2) { + return true; // Use V1 protocol when V2 is unsupported + } + // Use V1 protocol based on the bags rollout rate for V2 protocol. + return !isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.searchLandingPageV2RolloutRate); +} +async function fetchSearchLandingPage(objectGraph, fetchAds) { + if (shouldUseProtocolV1(objectGraph)) { + return await fetchSearchLandingPageV1(objectGraph, fetchAds); + } + if (objectGraph.bag.mediaAPISearchFocusEnabled) { + return await fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds); + } + return await fetchSearchLandingPageV2(objectGraph, fetchAds); +} +async function fetchSearchLandingPageV1(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); // for `extend=customArtwork` + searchLandingRequest.targetResourceType = "groupings"; + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + return await Promise.all([fetchSearchLanding, fetchAds]).then(([responseData, adResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV1(modifiedObjectGraph, responseData, adResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V1 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V1 Search Landing Page protocol + */ +function landingPageFromResponseV1(objectGraph, landingPageResponse, adResponse) { + const mediaApiGroupingDataArray = serverData.asArrayOrEmpty(landingPageResponse, "results.contents"); + const mediaApiGroupingData = mediaApiGroupingDataArray[0]; + if (serverData.isNullOrEmpty(mediaApiGroupingData)) { + return null; + } + if (!mediaRelationship.hasRelationship(mediaApiGroupingData, "tabs")) { + return null; + } + const groupingGenreAdamId = mediaAttributes.attributeAsString(mediaApiGroupingData, "id"); + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "Genre", mediaApiGroupingData.id, landingPageResponse); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + const groupingParseContext = { + shelves: [], + metricsPageInformation: pageInformation, + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + pageGenreAdamId: groupingGenreAdamId, + pageGenreId: mediaAttributes.attributeAsNumber(mediaApiGroupingData, "genre"), + hasAuthenticatedUser: serverData.isDefinedNonNull(objectGraph.user.dsid), + isSearchLandingPage: true, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse), + adIncidentRecorder: adIncidentRecorder, + }; + const flattenedGrouping = groupingCommon.flattenMediaApiGroupingData(objectGraph, mediaApiGroupingData); + groupingCommon.insertInitialShelvesIntoGroupingParseContext(objectGraph, flattenedGrouping, groupingParseContext); + const page = new SearchLandingPage(groupingParseContext.shelves); + // Page refresh + const refreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + page.pageRefreshPolicy = refreshPolicy; + // Ad Incidents + page.adIncidents = adIncidents.recordedIncidents(objectGraph, groupingParseContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, groupingParseContext.metricsPageInformation); + return page; +} +function makeSearchLandingRequestV2(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing:new-protocol") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) // for `extend=customArtwork` + .includingScopedRelationships("search-recommendations", ["contents"]) + .addingQuery("name", "search-landing"); + if (areAppTagsEnabled(objectGraph, "slp")) { + mediaRequestUtils.configureTagsForMediaRequest(searchLandingRequest); + } + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + searchLandingRequest.includingScopedAttributes("editorial-items", ["editorialClientParams"]); + } + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + if (objectGraph.client.isiOS) { + searchLandingRequest.addingQuery("meta", "adDisplayStyle"); + } + return searchLandingRequest; +} +async function fetchSearchLandingPageV2(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(([responseData, adResponse, amdResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV2(modifiedObjectGraph, responseData, adResponse, amdResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V2 Search Landing Page protocol + */ +function landingPageFromResponseV2(objectGraph, landingPageResponse, adResponse, impressionData) { + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchLanding", "SearchLanding", landingPageResponse); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + // Creates Search Landing Page Context + const landingPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse, landingPageResponse), + adIncidentRecorder: adIncidentRecorder, + pageType: SearchPageType.Landing, + recoImpressionData: impressionDemotion.impressionEventsFromData(objectGraph, impressionData), + }; + // Create the shelves for the page + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, landingPageContext); + // Add Unified Messaging placement to top of page for NLS BT. + const bubbleTipShelf = createNaturalLanguageSearchBubbleTipShelf(objectGraph); + if (bubbleTipShelf) { + landingPageContext.shelves.unshift(bubbleTipShelf); + } + const landingPage = new SearchLandingPage(landingPageContext.shelves); + // Page refresh + landingPage.pageRefreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + // Ad Incidents + landingPage.adIncidents = adIncidents.recordedIncidents(objectGraph, landingPageContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, landingPage, landingPageContext.metricsPageInformation); + return landingPage; +} +/** + * Creates the NLS BT shelf for SLP if enabled. + * @param objectGraph The app store object graph. + * @returns The shelf for the NLS BT shown on SLP, or undefined if bag has feature disabled. + */ +export function createNaturalLanguageSearchBubbleTipShelf(objectGraph) { + var _a; + if (!objectGraph.bag.isNaturalLanguageSearchEnabled && !objectGraph.bag.isNaturalLanguageSearchResultsEnabled) { + return undefined; // feature not enabled in the bag + } + const context = { + signal: { + lastNLSQueryDate: objectGraph.storage.retrieveString("lastNLSQueryDate"), + treatmentId: (_a = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.SearchLandingPage)) !== null && _a !== void 0 ? _a : null, + }, + }; + const shelf = groupingCommon.shelfForUnifiedMessage(objectGraph, "searchFocusHeader", context, "pullOnly"); + shelf.refreshUrl = `${Protocol.internal}:/${Path.searchLandingPage}/${Path.shelf}/?${Parameters.isSearchFocusHeaderShelf}=true`; + return shelf; +} +async function fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds).enablingFeature("search-focus-suggestions"); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise = null; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(async ([responseData, adResponse, amdResponse]) => { + return await fetchTimingMetricsBuilder.measureModelConstructionAsync(async () => await landingPageFromResponseV2WithFocusPage(modifiedObjectGraph, responseData, adResponse, amdResponse)); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol, but with search-focus feature enabled. + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the landing fetch + * @param landingAdResponse The response from the landingAd fetch + * @returns A `SearchLandingPage` model created from server response. + */ +async function landingPageFromResponseV2WithFocusPage(objectGraph, landingPageResponse, landingAdResponse, impressionData) { + // MAINTAINER'S NOTE: V3 protocol does not change any existing SLP v2 fields and is purely additives for focus page support + const landingPage = landingPageFromResponseV2(objectGraph, landingPageResponse, landingAdResponse, impressionData); + // Create Search Focus Page + return await fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse).then((focusPage) => { + landingPage.searchFocusPage = focusPage; + return landingPage; + }); +} +/** + * Creates `SearchFocusPage` model from the V3 Search Landing Page protocol, fetching search history if needed. + * @param objectGraph The App Store Object Graph + * @param focusPageResponse The response from the fetch + * @returns A `SearchFocusPage` model from the V3 Search Landing Page protocol + */ +async function fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse) { + var _a; + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchFocus", "Focus", landingPageResponse, " "); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + // Creates Search Focus Page Context + const focusPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + pageType: SearchPageType.Focus, + }; + const searchHistoryShelfMarker = searchLandingShelfController.firstShelfMarkerMatchingUseCase(landingPageResponse, focusPageContext, "recentSearches"); + // Skip fetching search history if there isn't a marker for it in SLP response. + if (isNothing(searchHistoryShelfMarker)) { + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + } + const searchHistoryDisplayCount = (_a = mediaAttributes.attributeAsNumber(searchHistoryShelfMarker, "displayCount")) !== null && _a !== void 0 ? _a : 0; + const fetchSearchHistory = objectGraph.onDeviceSearchHistoryManager.fetchRecentsWithLimit(searchHistoryDisplayCount); + return await fetchSearchHistory.then((searchHistory) => { + focusPageContext.searchHistory = searchHistory; + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + }); +} +function createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext) { + // Create the shelves for the focus page using same logic as landing page, but + // includes search history in page context to support `search-recommendations-marker`. + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, focusPageContext); + const focusPage = new SearchFocusPage(focusPageContext.shelves); + if (serverData.isNullOrEmpty(focusPage.shelves)) { + return null; + } + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, focusPage, focusPageContext.metricsPageInformation); + return focusPage; +} +async function fetchTrendingSearchesFallbackPage(objectGraph, fetchAds) { + const fetchRequest = { + url: objectGraph.bag.trendingSearchesURL, + }; + const trendingSearchesPromise = objectGraph.network.fetch(fetchRequest).then((response) => { + if (!response.ok) { + throw Error(`Bad Status code ${response.status} for ${fetchRequest.url}`); + } + return JSON.parse(response.body); + }); + return await Promise.all([trendingSearchesPromise, fetchAds]).then(([trendingSearchesData, adResponse]) => { + var _a; + const page = new SearchLandingPage(trendingSearchesShelvesForResponse(objectGraph, trendingSearchesData)); + const pageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "SearchLanding", "trending", ""); // old trending endpoint doesn't have metrics meta + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "SLPLOAD", "searchLanding"); // trending fallback never displays ad, so is always missed opportunity. + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + return page; + }); +} +/** + * Creates a trending searches shelves from the given JSON response. + * @param objectGraph The App Store Object Graph. + * @param response The API response JSON data. + * @return {Shelf[]} Trending searches shelves created from response. + */ +function trendingSearchesShelvesForResponse(objectGraph, response) { + return validation.context("trendingSearchesShelfForResponse", () => { + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const searches = serverData.asArrayOrEmpty(response, "trendingSearches").map((rawSearch) => { + const term = serverData.asString(rawSearch, "label"); + const searchAction = new SearchAction(term, term, serverData.asString(rawSearch, "url"), "trending"); + if (objectGraph.client.isPhone) { + searchAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return searchAction; + }); + let maxNumberOfSearches = 0; + switch (objectGraph.client.deviceType) { + case "pad": + maxNumberOfSearches = 10; + break; + case "phone": + maxNumberOfSearches = 7; + break; + default: + break; + } + const shelf = new Shelf("action"); + shelf.title = searches.length > 0 ? serverData.asString(response, "header.label") : null; + shelf.isHorizontal = false; + shelf.items = searches.slice(0, maxNumberOfSearches); + return [shelf]; + }); +} +export async function fetchPage(objectGraph) { + const fetchAds = isAdPlacementEnabled(objectGraph, "searchLanding") + ? landingAdFetchFetchAds(objectGraph, "searchLanding").catch(() => null) + : null; + return await fetchSearchLandingPage(objectGraph, fetchAds).catch(async (e) => { + // If the client has provided a `trendingSearchesURL`, we can fallback to that search + // mechanism if the search landing request fails. If `trendingSearchesURL` is not + // provided, we re-throw the original landing page error for the client to handle. + if (isSome(objectGraph.bag.trendingSearchesURL)) { + return await fetchTrendingSearchesFallbackPage(objectGraph, fetchAds); + } + else { + throw e; + } + }); +} +//# sourceMappingURL=search-landing-page-utils.js.map
\ No newline at end of file |
