/** * Handles fetching Ads for all on-device ad placements * This is currently SLP, Today, Product Page. * * # Ad fetching for on-device placements * On-Device placements are unpersonalized and rely on CDN caching. To show ads, we: * 1. Fetch from an on-device cache of ads (managed by PromotedContent framework) for the specific placement * 2. Fetching ad data from MAPI * 2. Stitch ad onto page data... */ import * as serverData from "../../foundation/json-parsing/server-data"; import { Request } from "../../foundation/media/data-fetching"; import { dataFromDataContainer } from "../../foundation/media/data-structure"; import { fetchData } from "../../foundation/media/network"; import { buildURLFromRequest } from "../../foundation/media/url-builder"; import { Parameters } from "../../foundation/network/url-constants"; import { offerDataFromData } from "../offers/offers"; import { productVariantDataForData, shouldFetchCustomAttributes } from "../product-page/product-page-variants"; import { todayTabODPTimeoutUseCase } from "../personalization/on-device-recommendations-today"; import { setTimeoutForRequestKey } from "../util/timeout-manager-util"; import { adLogger } from "../search/search-ads"; import * as adCommon from "./ad-common"; import { getSelectedCustomCreativeId } from "../search/custom-creative"; import { isSome } from "@jet/environment"; // region exports /** * Fetch ads for the given placement type leveraging on-device ads cache. * @param objectGraph the object graph. * @param placementType the placement type to fetch the ad for. * @param adamId the adamId of the app for which the product page is being viewed, to provide a relevant ad. Only required for product page placements. * @returns a promise containing an ad. */ export async function fetchAds(objectGraph, placementType, adamId) { const timeout = adCommon.adFetchTimeoutForPlacement(objectGraph, placementType, false); const request = new Request(objectGraph); switch (placementType) { case "today": request.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); switch (adCommon.todayAdStyle(objectGraph)) { case "mediumLockup": if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { request.includingAttributes(["customScreenshotsByTypeForAd", "adCreativeArtwork"]); } else { request.includingAttributes(["customScreenshotsByTypeForAd"]); } } else { request.includingAttributes(["customScreenshotsByTypeForAd"]); } break; default: break; } break; case "productPageYMAL": case "productPageYMALDuringDownload": request.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); break; default: break; } /** * Ad not available content filtering */ const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; if (serverData.isDefinedNonNullNonEmpty(adsOverrideLanguage)) { request.enablingFeature("adsLocaleMetadata").addingQuery("l", adsOverrideLanguage); } const requestMetaFields = buildURLFromRequest(objectGraph, request).query; try { const onDeviceResponse = await objectGraph.ads.fetchOnDeviceAdPlacement(placementType, timeout, requestMetaFields, adamId); return await handleAdResponse(objectGraph, onDeviceResponse, placementType); } catch { return null; } } /** * Handle the response from an on device ad request. * @param objectGraph The App Store Object Graph. * @param onDeviceResponse The response from the on device ad fetcher. * @param placementType The placement this request was for. * @returns A promise containing an ad. */ async function handleAdResponse(objectGraph, onDeviceResponse, placementType) { var _a, _b, _c, _d, _e, _f; if (serverData.isNullOrEmpty(onDeviceResponse.clientRequestId)) { onDeviceResponse.clientRequestId = objectGraph.random.nextUUID(); adLogger(objectGraph, `clientRequestId was nil. Assigned ${onDeviceResponse.clientRequestId}`); } const aggregateResponse = { clientRequestId: onDeviceResponse.clientRequestId, iAdId: onDeviceResponse.iAdId, placementType: (_b = (_a = onDeviceResponse === null || onDeviceResponse === void 0 ? void 0 : onDeviceResponse.ad) === null || _a === void 0 ? void 0 : _a.placementType) !== null && _b !== void 0 ? _b : placementType, }; // Failed w/o Ad from device. if (onDeviceResponse.failureReason) { aggregateResponse.failureReason = onDeviceResponse.failureReason; return aggregateResponse; } // Set the basic ad info received on the response. aggregateResponse.onDeviceAd = onDeviceResponse.ad; // Ad requests should return with at least some basic app metadata. // Note: On pre-SydneyC builds, this is expected to be null for the SLP placement. let mediaResponse = (_c = onDeviceResponse.ad) === null || _c === void 0 ? void 0 : _c.appMetadata; // Get the currently available app data from the data container. const appData = dataFromDataContainer(objectGraph, mediaResponse); // We should only attempt to fetch the full app data if the app data provided to us via // Promoted Content is incomplete. This should only be the case for SLP ads - Chainlink // placements should arrive with complete app data. // We check a couple of attributes here as a way to be sure it's hydrated. Sometimes // a single attribute can be misleading. if (serverData.isNullOrEmpty((_d = appData === null || appData === void 0 ? void 0 : appData.attributes) === null || _d === void 0 ? void 0 : _d.name) || serverData.isNullOrEmpty((_e = appData === null || appData === void 0 ? void 0 : appData.attributes) === null || _e === void 0 ? void 0 : _e.platformAttributes) || serverData.isNullOrEmpty(offerDataFromData(objectGraph, appData))) { try { const adRequest = createRequestForOnDeviceAd(objectGraph, onDeviceResponse.ad); mediaResponse = await fetchData(objectGraph, adRequest); } catch (e) { adLogger(objectGraph, `fetchAds request failed - ${e}`); aggregateResponse.failureReason = "mapiFetchFail"; } } // The app data should now be complete, set it on the response. if (serverData.isDefinedNonNullNonEmpty((_f = dataFromDataContainer(objectGraph, mediaResponse)) === null || _f === void 0 ? void 0 : _f.attributes)) { aggregateResponse.mediaResponse = decorateiAdAttributeFromOnDeviceAdResponse(objectGraph, mediaResponse, onDeviceResponse); // Check the localization is valid for the ad. if (!adCommon.isAdLocalizationValid(objectGraph, dataFromDataContainer(objectGraph, mediaResponse), aggregateResponse.onDeviceAd)) { adLogger(objectGraph, `fetchAds request failed - localization not available`); aggregateResponse.failureReason = "localizationNotAvailable"; } const metadataFailReason = checkAppMetadataIsValidForPlacement(objectGraph, aggregateResponse, placementType); if (serverData.isDefinedNonNull(metadataFailReason)) { adLogger(objectGraph, `fetchAds request failed - ${metadataFailReason}`); aggregateResponse.failureReason = metadataFailReason; } } return aggregateResponse; } /** * Indicates that an organic request kicked off parallel to an ad fetch has completed. * This gives us an opportunity to enforce a timeout on the ad request for the time beyond the organic request. * @param objectGraph The object graph. * @param placementType The placement for which the parallel organic request finished. */ export function parallelOrganicRequestDidFinish(objectGraph, placementType) { const timeout = adCommon.adFetchTimeoutForPlacement(objectGraph, placementType, true); if (serverData.isNull(timeout)) { return; } objectGraph.ads.setTimeoutForCurrentOnDeviceAdFetch(placementType, timeout); setTimeoutForRequestKey(objectGraph, timeout, todayTabODPTimeoutUseCase); } // endregion // region internals /** * Create an request for on-device adverts * @param ad The on device ad to fetch MAPI data for. */ function createRequestForOnDeviceAd(objectGraph, ad) { const request = new Request(objectGraph) .withIdOfType(ad.adamId, "apps") .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)) .includingAttributes(["customScreenshotsByTypeForAd"]); if (serverData.isDefinedNonNullNonEmpty(ad.cppIds)) { request.addingQuery(Parameters.productVariantID, ad.cppIds[0]); } // If there is an `adsOverrideLanguage`, attach it to this request too. const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; if (serverData.isDefinedNonNullNonEmpty(adsOverrideLanguage)) { request.addingQuery("l", adsOverrideLanguage); } return request; } /** * Decorate `iad` attribute with contents of `OnDeviceAdvert`. * * @param mediaResponse The data from of MAPI request * @param ad Ad data that was fetched independent of response. */ function decorateiAdAttributeFromOnDeviceAdResponse(objectGraph, mediaResponse, adResponse) { const adData = dataFromDataContainer(objectGraph, mediaResponse); if (serverData.isNullOrEmpty(adData) || serverData.isNull(adData.attributes)) { adLogger(objectGraph, "decorateiAdAttributeFromOnDeviceAd cannot decorate for malformed response"); return null; // The data is incompatible with `iad` decoration. Return `null` to let builder report error. } const onDeviceAd = adResponse.ad; const lineItem = `${onDeviceAd.adamId}|${onDeviceAd.metadata}`; // Create `IAdAttributes` and stitch onto `mediaResponse` const iadAttributes = { clientRequestId: adResponse.clientRequestId, impressionId: onDeviceAd.impressionId, metadata: onDeviceAd.metadata, privacy: onDeviceAd.privacy, lineItem: lineItem, }; const metaContainer = dataFromDataContainer(objectGraph, onDeviceAd.appMetadata); if (serverData.isDefinedNonNullNonEmpty(adData.meta) && serverData.isDefinedNonNullNonEmpty(metaContainer) && serverData.isDefinedNonNullNonEmpty(metaContainer.meta)) { adData.meta.passthroughAdInfo = metaContainer.meta.passthroughAdInfo; if (isSome(onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.alignedRegionDetails)) { adData.meta.alignedRegionDetails = onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.alignedRegionDetails[0]; } } switch (onDeviceAd.placementType) { case "today": // Enable images for specific placements. const imagesEnabled = adCommon.todayAdStyle(objectGraph) === "mediumLockup"; iadAttributes.format = { images: imagesEnabled, text: "", userRating: false, }; break; case "searchLanding": iadAttributes.format = { images: true, text: "", userRating: false, }; break; default: break; } adData.attributes["iad"] = iadAttributes; adCommon.decorateAdInstanceIdOnData(objectGraph, adData, onDeviceAd.instanceId); return mediaResponse; } /** * Checks whether the metadata returned for the app is considered valid for the given placement. * Some ad placements have special rules to be shown. These rules are validated here. * @param objectGraph The App Store object graph. * @param adResponse The ad response. * @param placementType The placement the given ad data is to be placed in. * @returns A fail reason if there is one. Otherwise null. */ function checkAppMetadataIsValidForPlacement(objectGraph, adResponse, placementType) { switch (placementType) { case "today": return checkAppMetadataIsValidForToday(objectGraph, adResponse); default: return null; } } /** * Checks the metadata returned for the app is considered valid **specifically** for the Today placement. * * Today ad placements must: * - Include a valid cppId in the `meta` field of the data that matches the cppId in the ad result, and * - Have enough assets to fulfill the template requirement: * - Portrait: at least 4 assets, one of which can be a video. * - Landscape: at least 5 assets, one of which can be a video. * * @param objectGraph The App Store object graph. * @param adResponse The ad response. * @returns A fail reason if there is one. Otherwise null. */ function checkAppMetadataIsValidForToday(objectGraph, adResponse) { var _a, _b, _c; const adData = dataFromDataContainer(objectGraph, adResponse.mediaResponse); const productVariantData = productVariantDataForData(objectGraph, adData); const hasCPP = (_b = (_a = adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.cppIds) === null || _b === void 0 ? void 0 : _b.includes(productVariantData.productPageId); if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { const alignedRegionDetails = (_c = adResponse.onDeviceAd) === null || _c === void 0 ? void 0 : _c.alignedRegionDetails; const creativeID = serverData.asString(alignedRegionDetails === null || alignedRegionDetails === void 0 ? void 0 : alignedRegionDetails[0], "apAssetId"); const selectedCustomCreativeId = getSelectedCustomCreativeId(adData); const hasCreative = creativeID === selectedCustomCreativeId; // First check there is a cppId for the ad that matches the `meta`. if (!hasCPP && !hasCreative) { // Then check if there is appMetadata for the custom creative ad. return "cppAssetsMissing"; } } else { if (!hasCPP) { return "cppAssetsMissing"; } } return null; } // endregion //# sourceMappingURL=on-device-ad-fetch.js.map