diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/ads')
5 files changed, 1006 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js new file mode 100644 index 0000000..6216dba --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js @@ -0,0 +1,433 @@ +/** + * Common utilities for dealing with Ads. + */ +import { isNothing, isSome } from "@jet/environment"; +import { Lockup, TodayCard, TodayCardMediaMediumLockupWithAlignedRegion, TodayCardMediaMediumLockupWithScreenshots, TodayCardMediaSingleLockup, } from "../../api/models"; +import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { attributeAsString } from "../../foundation/media/attributes"; +import { IAdSearchInformation } from "../metrics/helpers/models"; +import { platformSupportsAdverts } from "../search/search-ads"; +import { shouldTodayAdBeCondensed } from "../../foundation/experimentation/today-ad-experiments"; +import { getTemplateTypeForMediumAdFromLockupWithCustomCreative, getTemplateTypeForMediumAdFromLockupWithScreenshots, searchAdMissedOpportunityFromId, } from "../lockups/ad-lockups"; +import { contentAttributeAsString } from "../content/attributes"; +import { currentLocation, currentPosition } from "../metrics/helpers/location"; +// region placement logic +/** + * A helper function to check if the given AdPlacement is enabled. + * @param objectGraph The object graph. + * @param adPlacementKey The ad placement to check. Matches values sent in the bag. + * @returns A boolean indicating if the provided AdPlacement is enabled. + */ +export function isAdPlacementEnabled(objectGraph, placementType) { + if (!platformSupportsAdverts(objectGraph)) { + return false; + } + switch (placementType) { + case "searchLanding": + // In transitioning from the legacy bag key `isSearchLandingAdsEnabled` to the new key `enabledAdPlacements`, + // we will enable SLP ads if either of the keys indicates ads are enabled for SLP. + const slpPlacementBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isSome(slpPlacementBagValue)) { + return (objectGraph.bag.isSearchLandingAdsEnabled || + objectGraph.bag.enabledAdPlacements.includes(slpPlacementBagValue)); + } + else { + return objectGraph.bag.isSearchLandingAdsEnabled; + } + case "searchResults": + return true; // Legacy + case "today": + const todayBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNull(todayBagValue)) { + return false; + } + return objectGraph.bag.enabledAdPlacements.includes(todayBagValue) && isSome(todayAdStyle(objectGraph)); + case "productPageYMAL": + case "productPageYMALDuringDownload": + const placementBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNull(placementBagValue)) { + return false; + } + return objectGraph.bag.enabledAdPlacements.includes(placementBagValue); + default: + return false; + } +} +/** + * Get the style to use for the Today ad. + * A return type of `undefined` indicates the ad style is unsupported on the client. + * @param objectGraph The Object Graph. + * @returns A style, or undefined if it's unsupported. + */ +export function todayAdStyle(objectGraph) { + if (objectGraph.bag.todayAdMediumLockupScreenshotEnabled) { + return "mediumLockup"; + } + if (objectGraph.bag.todayAdCondensedEnabled) { + // Only supported on iPhone. + if (!objectGraph.client.isPhone) { + return undefined; + } + return "singleLockup"; + } + if (shouldTodayAdBeCondensed(objectGraph)) { + return "singleLockup"; + } + else { + return undefined; + } +} +/** + * Get the timeout of the ad fetch for a given placement from the bag values. + * @param objectGraph The object graph. + * @param placementType The ad placement. + * @param isDeltaTimeout Whether the timeout we're looking for is a delta timeout between parallel organic and ad requests completing. + * See `Ads.setTimeoutForCurrentOnDeviceAdFetch` for more details on how this is used. + * @returns The timeout. + */ +export function adFetchTimeoutForPlacement(objectGraph, placementType, isDeltaTimeout) { + var _a, _b, _c, _d, _e; + const timeouts = objectGraph.bag.adPlacementTimeouts; + const defaultTimeout = 0.3; + switch (placementType) { + case "searchResults": + return isDeltaTimeout ? null : (_a = timeouts === null || timeouts === void 0 ? void 0 : timeouts["search-results-in-seconds"]) !== null && _a !== void 0 ? _a : defaultTimeout; + case "searchLanding": + return isDeltaTimeout ? null : (_b = objectGraph.bag.searchLandingAdFetchTimeout) !== null && _b !== void 0 ? _b : defaultTimeout; // Legacy value. + case "today": + // Today uses its timeout value as a delta after the organic request finishes. + return isDeltaTimeout ? (_c = timeouts === null || timeouts === void 0 ? void 0 : timeouts["today-in-seconds"]) !== null && _c !== void 0 ? _c : defaultTimeout : null; + case "productPageYMAL": + return isDeltaTimeout ? null : (_d = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-in-seconds"]) !== null && _d !== void 0 ? _d : defaultTimeout; + case "productPageYMALDuringDownload": + return isDeltaTimeout ? null : (_e = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-during-download-in-seconds"]) !== null && _e !== void 0 ? _e : defaultTimeout; + default: + return defaultTimeout; + } +} +/** + * Convert the value describing an ad placement from the App Store representation to the bag representation. + * @param placementType The bag value for different ad placements + * @returns The equivalent bag placement value as a string. + */ +function adPlacementBagValueForAdPlacementType(placementType) { + switch (placementType) { + case "searchResults": + return "search-results"; + case "searchLanding": + return "search-landing"; + case "today": + return "today"; + case "productPageYMAL": + return "product-page-ymal"; + case "productPageYMALDuringDownload": + return "product-page-ymal-during-download"; + default: + return undefined; + } +} +/** + * The minimum number of landscape media items for a Today ad placement. + * The app must satisfy this or `todayAdPlacementMinimumPortraitMedia` to be shown. + */ +export const todayAdPlacementMinimumLandscapeMedia = 5; +/** + * The minimum portrait media for a Today ad placement. + * The app must satisfy this or `todayAdPlacementMinimumLandscapeMedia` to be shown. + */ +export const todayAdPlacementMinimumPortraitMedia = 4; +/** + * Get a count of the media in each orientation specifically for a Today ad placement. + * This is specific to Today because the placement only supports one video. + * @param trailers A set of trailers for an app. + * @param screenshots A set of screenshots for an app. + * @returns A count of media in each orientation, respecting the placement's rules. + */ +export function getMediaOrientationCountsForTodayPlacement(trailers, screenshots) { + var _a, _b, _c, _d; + // Grab the first array of platform screenshots and trailers - the ones the ad will display. + const platformScreenshotsArtwork = (_b = (_a = screenshots[0]) === null || _a === void 0 ? void 0 : _a.artwork) !== null && _b !== void 0 ? _b : []; + const platformTrailersVideos = (_d = (_c = trailers[0]) === null || _c === void 0 ? void 0 : _c.videos) !== null && _d !== void 0 ? _d : []; + // Split media into orientations. + const portraitScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isPortrait()); + const landscapeScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isLandscape()); + // Take a maximum of one video of each orientation. + const portraitVideos = platformTrailersVideos.filter((video) => video.preview.isPortrait()).slice(0, 1); + const landscapeVideos = platformTrailersVideos.filter((video) => video.preview.isLandscape()).slice(0, 1); + const portraitCount = portraitScreenshots.length + portraitVideos.length; + const landscapeCount = landscapeScreenshots.length + landscapeVideos.length; + return { + landscape: landscapeCount, + portrait: portraitCount, + }; +} +/** + * A convenience method to determine if a position is ad eligible. + * @param placementType The type of placement for an ad. + * @param locationTracker The location tracker being used to build the items. Used to determine the current shelf and position within the shelf. + * @returns A boolean indicating if the current position is considered ad eligible. + */ +export function isAdEligible(placementType, locationTracker) { + var _a; + if (isNothing(locationTracker) || isNothing(placementType)) { + return false; + } + const shelfId = (_a = currentLocation(locationTracker)) === null || _a === void 0 ? void 0 : _a.id; + if (isNothing(shelfId)) { + return false; + } + const eligibleIndex = adEligibleIndexForType(placementType, shelfId); + if (isNothing(eligibleIndex)) { + return false; + } + const index = currentPosition(locationTracker); + return index === eligibleIndex; +} +/** + * Inserts a missed opportunity on the correct slot in the page if an ad isn't already present. This looks for + * types conforming to SearchAdOpportunityProviding and handles all of the construction for the metadata associated + * with a SearchAdOpportunity. + * @param objectGraph The object graph. + * @param shelves Completed shelves that are constructed for the page. + * @param placementType The location where the ad will be shown. + * @param shelfIdentifier The specific shelf identifier we should be evaluating against. This is not inferred to support things like Today Page that can use a shelf-per-item. + * @param pageInformation Metrics information for the page. + */ +export function applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, shelves, placementType, shelfIdentifier, pageInformation) { + var _a, _b, _c; + if (!platformSupportsAdverts(objectGraph) || isNull(pageInformation.iAdInfo)) { + return; + } + const adEligibleIndex = adEligibleIndexForType(placementType, shelfIdentifier); + if (isNothing(adEligibleIndex)) { + return; + } + // Opportunities aren't present when we've recorded certain missed opportunities + let missedOpportunityReason = null; + if (typeof pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason === "string") { + missedOpportunityReason = pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason; + } + if (isNothing(missedOpportunityReason) || + missedOpportunityReason.length === 0 || + missedOpportunityReason === "EDITORIALTAKEOVER" || + missedOpportunityReason === "SLPLOAD") { + return; + } + const allShelfItems = []; + for (const shelf of shelves) { + // searchResult is handled in a separate non-shelf workflow + const isValidContentType = shelf.contentType === "smallLockup" || shelf.contentType === "todayCard"; + if (!isValidContentType) { + continue; + } + const nextShelfItems = shelf.items; + if (isDefinedNonNull(nextShelfItems) && nextShelfItems.length > 0) { + allShelfItems.push(...nextShelfItems); + } + } + if (allShelfItems.length <= adEligibleIndex) { + return; + } + const adEligibleShelfItem = allShelfItems[adEligibleIndex]; + const isTodayCard = adEligibleShelfItem instanceof TodayCard; + const isSmallLockup = adEligibleShelfItem instanceof Lockup; + const shelfItemMedia = isTodayCard ? adEligibleShelfItem.media : null; + const lockupTypeIsMediumScreenshotFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithScreenshots; + const lockupTypeIsMediumCreativeFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithAlignedRegion; + const lockupHasPlacedCondensedTodayAd = isDefinedNonNull(shelfItemMedia) && + shelfItemMedia instanceof TodayCardMediaSingleLockup && + isDefinedNonNull(shelfItemMedia.condensedAdLockupWithIconBackground.lockup.searchAdOpportunity); + const lockupHasPlacedMediumAdScreenshots = lockupTypeIsMediumScreenshotFormat && + isDefinedNonNull(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.lockup.searchAdOpportunity); + const lockupHasPlacedMediumAdCreative = lockupTypeIsMediumCreativeFormat && + isDefinedNonNull(shelfItemMedia.mediumAdLockupWithAlignedRegionBackground.lockup.searchAdOpportunity); + const lockupHasPlacedSmallLockup = isSmallLockup && isDefinedNonNull(adEligibleShelfItem.searchAdOpportunity); + // If we've already processed the ad data and placed it, we don't want to indicate that there's a missed opportunity + if (lockupHasPlacedCondensedTodayAd || + lockupHasPlacedMediumAdScreenshots || + lockupHasPlacedMediumAdCreative || + lockupHasPlacedSmallLockup) { + return; + } + adEligibleShelfItem.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, pageInformation); + (_a = adEligibleShelfItem.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setMissedOpportunityReason(missedOpportunityReason !== null && missedOpportunityReason !== void 0 ? missedOpportunityReason : "NOAD"); + if (lockupTypeIsMediumScreenshotFormat) { + (_b = adEligibleShelfItem.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType(getTemplateTypeForMediumAdFromLockupWithScreenshots(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.screenshots[0])); + } + else if (lockupTypeIsMediumCreativeFormat) { + adEligibleShelfItem.searchAdOpportunity.setTemplateType(getTemplateTypeForMediumAdFromLockupWithCustomCreative()); + } + else { + (_c = adEligibleShelfItem.searchAdOpportunity) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); + } +} +/** + * This is a bit of a workaround for `isAdEligible` function in this file. Ideally this is bag-driven, but I need to figure out + * how to make it work. `shelfIdentifier` doesn't seem to work correctly, except for the YMAL ad on PP + * @param placementType The ad placement type to evaluate for ad eligibility + * @param shelfIdentifier The identifier for the shelf to evaluate for ad eligibility + * @returns The equivalent bag placement value as a string. + */ +function adEligibleIndexForType(placementType, shelfIdentifier) { + var _a; + const adEligibleBagRepresentation = { + today: [ + { + shelfIdentifier: "today", + adEligibleIndex: 1, + }, + ], + productPageYMAL: [ + { + shelfIdentifier: "customers-also-bought-apps", + adEligibleIndex: 0, + }, + ], + searchLanding: [ + { + shelfIdentifier: "R8802", + adEligibleIndex: 0, + }, + ], + searchResults: [ + { + shelfIdentifier: "search-results", + adEligibleIndex: 0, + }, + ], + }; + const matchingSlot = ((_a = adEligibleBagRepresentation[placementType]) !== null && _a !== void 0 ? _a : []).find((element) => { + return element.shelfIdentifier === shelfIdentifier; + }); + if (isDefinedNonNullNonEmpty(matchingSlot) && isDefinedNonNull(matchingSlot.adEligibleIndex)) { + return matchingSlot.adEligibleIndex; + } + else { + return undefined; + } +} +// endregion +// region iad data +export function iadInfoFromOnDeviceAdResponse(objectGraph, placementType, adResponse, flattenedTodayFeed = null) { + var _a, _b; + if (!platformSupportsAdverts(objectGraph) || isNull(adResponse)) { + return null; + } + return new IAdSearchInformation(objectGraph, placementType, IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, (_a = adResponse === null || adResponse === void 0 ? void 0 : adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo, flattenedTodayFeed), adResponse.iAdId, adResponse.clientRequestId, undefined, (_b = adResponse.onDeviceAd) === null || _b === void 0 ? void 0 : _b.positionInfo); +} +/** + * Key added to `Data`'s `attributes` field so downstream builders can use this field. + * This is currently generated by JS when we create `SponsoredSearchAdverts` + */ +export const instanceIdAttributeKey = "jet_native_advert_instanceid"; +/** + * Returns the native advert id if one was annotated during `applyNativeAdvertData` + */ +export function advertInstanceIdForData(objectGraph, data) { + return attributeAsString(data, instanceIdAttributeKey); +} +/** + * Decorates `instanceId` that identifies an ad to given `Data` for consumption in builders. + */ +export function decorateAdInstanceIdOnData(objectGraph, data, instanceId) { + if (isDefinedNonNullNonEmpty(data === null || data === void 0 ? void 0 : data.attributes)) { + data.attributes[instanceIdAttributeKey] = instanceId; + } +} +// endregion +// region ad localization +/** + * Whether the available localization data in an ad is valid for the defined `adsOverrideLanguage`, if any. + * If no `adsOverrideLanguage` is set, `true` is returned. + * The logic for a valid set of data differs based on the passed in placement and if an ad display style + * is defined (for SRP only). + * @param objectGraph The Object Graph. + * @param data The app data. + * @param onDeviceAd Optionally, an `OnDeviceAdvert` - this is only used as a fallback for SLP ads, + * where the meta resource object is attached to the original ad request, but not to the subsequent + * hydration request. For this case only, we must use a combination of these two requests to identify + * whether the localization is valid. + * @param adDisplayStyle: Optionally, an ad display style - this is used for SRP ads, where the data used + * in the ad UI depends on which ad display style is being used. + * @returns A boolean indicating if the available localization data is valid. + */ +export function isAdLocalizationValid(objectGraph, data, onDeviceAd, adDisplayStyle) { + var _a, _b, _c, _d, _e; + const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; + // No localization requirements if there is no adsOverrideLanguage. + if (isNullOrEmpty(adsOverrideLanguage) || isNullOrEmpty(data)) { + return true; + } + let metaResource = (_a = data.meta) === null || _a === void 0 ? void 0 : _a.resource; + // If the `metaResource in `data` is missing, attempt to fall back to the `onDeviceAd` values + // which are provided for SLP. + if (isNullOrEmpty(metaResource) && isDefinedNonNullNonEmpty(onDeviceAd)) { + metaResource = (_e = (_d = (_c = (_b = onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.appMetadata) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.meta) === null || _e === void 0 ? void 0 : _e.resource; + } + // If `metaResource` still don't exist, we can't validate localization. + if (isNullOrEmpty(metaResource)) { + // The meta resource attributes object is missing, assume we don't have the correct localizations. + return false; + } + // Title + // Title is always used. Check it's localized. + const titleLocale = attributeAsString(metaResource, "name.locale"); + if (titleLocale !== adsOverrideLanguage) { + return false; + } + // Subtitle + // Subtitle is constructed using either the subtitle field, or falling back to the category. + // If the subtitle text is present, we need to validate the locale. If not, we don't. + const subtitle = contentAttributeAsString(objectGraph, data, "subtitle"); + const subtitleLocale = contentAttributeAsString(objectGraph, metaResource, "subtitle.locale"); + if (isDefinedNonNullNonEmpty(subtitle) && subtitleLocale !== adsOverrideLanguage) { + return false; + } + // SRP advertising text + // SRP ads can optionally provide a 3rd set of text, `advertisingText`. + // It's only used when the `adDisplayStyle` is `TEXT`. + if (adDisplayStyle === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + // The text used can be defined by the ad payload inside the `data`. We need to use that path + // to get both the text and the locale for that text. + const iAdTextKey = attributeAsString(data, "iad.format.text"); + if (isSome(iAdTextKey) && iAdTextKey !== "none") { + let attributePath; + const attributePathLocale = iAdTextKey; + // In the case where the path is "description", we need to use different paths for the text and the locale. + // This is due to a Media API limitation where the locale for "description" is just under that key, rather + // than "description.standard", which is the actual path for the text content. + if (iAdTextKey === "description") { + attributePath = "description.standard"; + } + else { + attributePath = iAdTextKey; + } + const advertisingText = contentAttributeAsString(objectGraph, data, attributePath); + const advertisingTextLocale = contentAttributeAsString(objectGraph, metaResource, attributePathLocale.concat(".locale")); + if (isDefinedNonNullNonEmpty(advertisingText) && advertisingTextLocale !== adsOverrideLanguage) { + return false; + } + } + } + return true; +} +// endregion +// region metrics +/** + * Retrieve the eligible slot positions for a given ad placement type. + * @param objectGraph The Object Graph. + * @param placementType The placement the ad is for. + * @returns An array of eligible slot positions for the provided placement. + */ +export function eligibleSlotPositionsForAdPlacement(objectGraph, placementType) { + if (isNothing(placementType)) { + return undefined; + } + const placementTypeBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNothing(placementTypeBagValue)) { + return undefined; + } + return objectGraph.bag.adPlacementEligibleSlotPositions[placementTypeBagValue]; +} +// endregion +//# sourceMappingURL=ad-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js new file mode 100644 index 0000000..6dac34a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js @@ -0,0 +1,116 @@ +/** + * Ad Incident Recorder manages the Journey and Figaro events when rendering a page. + * This exists so managing journey and figaro metrics is a bit more sane. + */ +"use strict"; +import { isNothing } from "@jet/environment"; +import { DiscardAdIncident } from "../../api/models"; +import { isDefinedNonNull, isNull, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { advertInstanceIdForData } from "./ad-common"; +/** + * Create a new ad incident recorder + */ +export function newRecorder(objectGraph, iAdInfo) { + return { + incidents: [], + iAdInfo: iAdInfo, + }; +} +/** + * Get recorded incidents from recorder + */ +export function recordedIncidents(objectGraph, recorder) { + if (isNull(recorder) || isNullOrEmpty(recorder.incidents)) { + return null; + } + return recorder.incidents; +} +/** + * Record that building a lockup from data failed. + */ +export function recordLockupFromDataFailed(objectGraph, recorder, lockupData) { + var _a, _b; + const instanceId = advertInstanceIdForData(objectGraph, lockupData); + if (isNothing(instanceId)) { + return; + } + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "advertDataMalformed"); + addIncident(recorder, incident); + (_a = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "METADATA", (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType); +} +/** + * Record events that may occur when we try to fetch an on-device ad + data + */ +export function recordAdResponseEventsIfNeeded(objectGraph, recorder, response) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + if (isNullOrEmpty(response === null || response === void 0 ? void 0 : response.failureReason)) { + return; // No failure to report + } + const instanceId = (_a = response === null || response === void 0 ? void 0 : response.onDeviceAd) === null || _a === void 0 ? void 0 : _a.instanceId; + const placementType = (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType; + switch (response === null || response === void 0 ? void 0 : response.failureReason) { + case "mapiFetchFail": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "advertDataMalformed"); + addIncident(recorder, incident); + } + (_c = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _c === void 0 ? void 0 : _c.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "cppAssetsMissing": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "cppAssetsMissing"); + addIncident(recorder, incident); + } + (_d = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _d === void 0 ? void 0 : _d.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "insufficientAssets": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "insufficientAssets"); + addIncident(recorder, incident); + } + (_e = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _e === void 0 ? void 0 : _e.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "noAdAvailable": + // no journey metric + (_f = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _f === void 0 ? void 0 : _f.setMissedOpportunity(objectGraph, "NOAD", placementType); + break; + case "timeout": + // no journey metric + (_g = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _g === void 0 ? void 0 : _g.setMissedOpportunity(objectGraph, "APSLA", placementType); + break; + case "localizationNotAvailable": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "localizationNotAvailable"); + addIncident(recorder, incident); + } + (_h = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _h === void 0 ? void 0 : _h.setMissedOpportunity(objectGraph, "NOLOC", placementType); + break; + case "policyAdDrop": + // no journey metric + (_j = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _j === void 0 ? void 0 : _j.setMissedOpportunity(objectGraph, "ODP_NOAD", placementType); + break; + default: + break; + } +} +// region Internals +/** + * Add incident on `AdIncidentRecorder` + */ +function addIncident(recorder, incident) { + if (isNull(recorder) || isNull(incident)) { + return; + } + recorder.incidents.push(incident); +} +//# sourceMappingURL=ad-incident-recorder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js new file mode 100644 index 0000000..c9fc6b9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js @@ -0,0 +1,73 @@ +/** + * Handles stitching ads into pages. + * + * At a high level: + * 1. Setup `AdStitcher` w/ content to stitch. + * 2. Pass to builder + * 3. Destructively consume tasks during building. + */ +"use strict"; +import { isNull } from "../../foundation/json-parsing/server-data"; +/** + * Creates a string identifier representing the position information for an ad stitch task. + * @param positionInfo The position information object to create an identifier for. + * @returns The string identifier. + */ +function stringIdentifierForPositionInfo(positionInfo) { + return `${positionInfo.shelfIdentifier}.${positionInfo.slot}`; +} +// region exports +/** + * Create a new ad stitcher + */ +export function newAdStitcher() { + return { + tasks: {}, + }; +} +/// Add task +export function addTask(stitcher, task) { + if (isNull(stitcher)) { + return; + } + const positionIdentifier = stringIdentifierForPositionInfo(task.positionInfo); + stitcher.tasks[positionIdentifier] = task; +} +/** + * Consume a single task, if available, for a given position information. + * @param stitcher The relevant ad stitcher. + * @param positionInfo The position information to check for an available task. + * @returns A task for the position, or null if unavailable. + */ +export function consumeTask(stitcher, positionInfo) { + if (isNull(stitcher)) { + return null; + } + const positionIdentifier = stringIdentifierForPositionInfo(positionInfo); + const task = stitcher.tasks[positionIdentifier]; + if (isNull(task)) { + return null; + } + delete stitcher.tasks[positionIdentifier]; + return task; +} +/** + * Consume all tasks for a given shelf identifier. Useful where ad data is being stitched in prior to view models being built. + * @param stitcher The relevant ad stitcher. + * @param shelfIdentifier The identifier for the shelf to get all available tasks for. + * @returns An array of relevant tasks for the shelf. + */ +export function consumeTasksForShelfIdentifier(stitcher, shelfIdentifier) { + if (isNull(stitcher)) { + return []; + } + const tasksForShelf = []; + Object.entries(stitcher.tasks).forEach(([key, value]) => { + if (key.startsWith(shelfIdentifier)) { + tasksForShelf.push(value); + delete stitcher.tasks[key]; + } + }); + return tasksForShelf; +} +//# sourceMappingURL=ad-stitcher.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js new file mode 100644 index 0000000..04f3e01 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js @@ -0,0 +1,286 @@ +/** + * 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
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js new file mode 100644 index 0000000..4cded1e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js @@ -0,0 +1,98 @@ +/** + * Performs ad stitching for Search Landing page using `AdStitcher` + */ +"use strict"; +import { isNothing, isSome } from "@jet/environment"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import * as adStitch from "./ad-stitcher"; +// region Constants +export const searchLandingPagePositionInfo = { + shelfIdentifier: "first", + slot: 0, +}; +/// shelf identifier for ad stitcher position info +export const searchLandingPageAdShelfIdentifier = "SLPPage"; +// region Setup +/** + * Get a positionInfo from a locationTracker and index. + * @param locationTracker A location tracker to indicate the current parsing position. + * @param index The index of the current position. + * @returns A constructed `AdStitcherPositionInfo`. + */ +export function todayPositionInfoForLocationTrackerAndIndex(locationTracker, index) { + return { + shelfIdentifier: locationTracker.rootPosition.toString(), + slot: index, + }; +} +/** + * Creates an Ad stitcher for on device adverts *specifically for the product page YMAL shelf* + * This is specific to this position as it hardcodes a shelf identifier. + * @param adResponse Ad response to configure stitcher with. + */ +export function adStitcherForOnDeviceProductPageYMALAdvertData(objectGraph, adResponse) { + var _a; + const rawPositionInfo = (_a = adResponse === null || adResponse === void 0 ? void 0 : adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo; + if (isNothing(rawPositionInfo) || isNothing(adResponse)) { + return null; + } + let shelfIdentifier; + switch (adResponse.placementType) { + case "productPageYMAL": + shelfIdentifier = "customers-also-bought-apps"; + break; + case "productPageYMALDuringDownload": + shelfIdentifier = "customers-also-bought-apps-download"; + break; + default: + break; + } + if (isNothing(shelfIdentifier)) { + return null; + } + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + const adjustedRawSlot = rawPositionInfo.slot - 1; + const positionInfo = { + shelfIdentifier: shelfIdentifier, + slot: adjustedRawSlot, + }; + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, positionInfo); +} +/** + * Creates an Ad stitcher for on device adverts *specifically for search landing page* + * This is specific to SLP because we have a hardcoded position. + * @param adResponse Ad response to configure stitchcher with. + * @param landingPageResponse Search page response to configure the positionInfo. + */ +export function adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse, landingPageResponse) { + var _a; + const adMeta = (landingPageResponse === null || landingPageResponse === void 0 ? void 0 : landingPageResponse.meta) || null; + const slot = (_a = adMeta === null || adMeta === void 0 ? void 0 : adMeta.adDisplayStyle) === null || _a === void 0 ? void 0 : _a.slot; + if (isSome(slot)) { + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, { + shelfIdentifier: searchLandingPageAdShelfIdentifier, + slot: slot, + }); + } + else { + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, searchLandingPagePositionInfo); + } +} +function adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, positionInfo) { + const mediaResponse = adResponse === null || adResponse === void 0 ? void 0 : adResponse.mediaResponse; + if (isNothing(mediaResponse) || isSome(adResponse === null || adResponse === void 0 ? void 0 : adResponse.failureReason)) { + return null; + } + const stitcher = adStitch.newAdStitcher(); + /** + * Stitch ad data to first lockup + */ + const firstAdData = dataFromDataContainer(objectGraph, mediaResponse); + const firstLockupAdTask = { + data: firstAdData, + positionInfo: positionInfo, + }; + adStitch.addTask(stitcher, firstLockupAdTask); + return stitcher; +} +//# sourceMappingURL=on-device-ad-stitch.js.map
\ No newline at end of file |
