summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js
blob: 04f3e019f15163feba308faa0b1ba7c2c8a7385d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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