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