import { PageInvocationPoint } from "@jet/environment/types/metrics"; import { isSome } from "@jet/environment/types/optional"; import * as models from "../../../api/models"; import * as serverData from "../../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../../foundation/media/attributes"; import * as mediaDataStructure from "../../../foundation/media/data-structure"; import { ResponseMetadata } from "../../../foundation/network/network"; import { Parameters } from "../../../foundation/network/url-constants"; import * as urls from "../../../foundation/network/urls"; import * as dateUtil from "../../../foundation/util/date-util"; import * as objects from "../../../foundation/util/objects"; import * as content from "../../content/content"; import { productVariantDataForData } from "../../product-page/product-page-variants"; import * as guidedSearch from "../../search/guided-search/guided-search-metrics"; import * as searchAds from "../../search/search-ads"; import * as metricsBuilder from "../builder"; import * as misc from "./misc"; import * as metricsModels from "./models"; import * as metricsUtil from "./util"; import { searchMetricsDataSetID } from "../../search/search-common"; //* ************************* //* Page Metrics //* ************************* export function metricsPageInformationFromMarketingItemMediaApiResponse(objectGraph, pageType, pageId, marketingItemData) { var _a; const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, pageType, pageId, marketingItemData); if (serverData.isDefinedNonNull(marketingItemData)) { pageInformation.mercuryMetricsData = (_a = metricsUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData)) !== null && _a !== void 0 ? _a : undefined; } return pageInformation; } export function metricsPageInformationFromMediaApiResponse(objectGraph, pageType, pageId, response, pageDetails, overrideIAdInfo) { var _a, _b, _c; const pageInformation = (_a = serverData.asInterface(response, ResponseMetadata.pageInformation, {})) !== null && _a !== void 0 ? _a : {}; const timingMetrics = serverData.asInterface(response, ResponseMetadata.timingValues); const pageUrl = serverData.asString(response, ResponseMetadata.requestedUrl); let productVariantData; const responsePageBaseFields = pageInformation; responsePageBaseFields.pageType = pageType; responsePageBaseFields.pageId = pageId; if (pageDetails) { responsePageBaseFields["pageDetails"] = pageDetails; } if (pageType === "Software") { const softwareData = mediaDataStructure.dataFromDataContainer(objectGraph, response); if (isSome(softwareData)) { const name = mediaAttributes.attributeAsString(softwareData, "name"); const artistName = mediaAttributes.attributeAsString(softwareData, "artistName"); responsePageBaseFields["pageDetails"] = `${artistName}_${name}`; // To distinguish normal apps from Arcade apps if (content.isArcadeSupported(objectGraph, softwareData)) { responsePageBaseFields["softwareType"] = "Arcade"; } productVariantData = productVariantDataForData(objectGraph, softwareData); } } else if (pageType === "Genre") { responsePageBaseFields["pageDetails"] = `${pageType}_${pageId}`; } else if (pageType === "Search") { responsePageBaseFields["pageDetails"] = "Apps"; } else if (pageType === "SearchLanding" && pageId === "SearchLanding") { responsePageBaseFields["pageDetails"] = `${pageType}_${pageId}`; } const pageInfo = new metricsModels.MetricsPageInformation((_b = metricsUtil.sanitizedMetricsDictionary(responsePageBaseFields)) !== null && _b !== void 0 ? _b : {}); if (timingMetrics !== null) { pageInfo.timingMetrics = timingMetrics; if (isSome(pageUrl)) { pageInfo.pageUrl = pageUrl; } } // For product pages, every shelf is creating it's a new page information. There should be one top-level one, shared for page. // Revisit in: rdar://77227964 (Metrics: ProductPage: Shelves should share page information instead of building it per-shelf.) if (serverData.isDefinedNonNull(productVariantData)) { pageInfo.productVariantData = productVariantData; } const iAdInfo = overrideIAdInfo !== null && overrideIAdInfo !== void 0 ? overrideIAdInfo : metricsModels.iAdInformationFromMediaApiResponse(objectGraph, pageId, response); if (isSome(iAdInfo)) { pageInfo.iAdInfo = iAdInfo; } pageInfo.recoMetricsData = (_c = mediaDataStructure.metricsFromMediaApiObject(response)) !== null && _c !== void 0 ? _c : undefined; return pageInfo; } export function fakeMetricsPageInformation(objectGraph, pageType, pageId, pageDetails, iAdClickInfo) { var _a; const page = new metricsModels.MetricsPageInformation({ pageType: pageType, pageId: pageId, page: `${pageType}_${pageId}`, pageDetails: pageDetails, }); if (iAdClickInfo) { // rdar://72607206 (Gibraltar: Tech Debt: Population of iAd fields in Product Page) const placementType = metricsModels.IAdSearchInformation.placementTypeFromPlacementId(objectGraph, serverData.asString(iAdClickInfo, "iAdPlacementId")); page.iAdInfo = new metricsModels.IAdSearchInformation(objectGraph, placementType, metricsModels.IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, null, null), (_a = serverData.asString(iAdClickInfo, "iAdId")) !== null && _a !== void 0 ? _a : undefined); page.iAdInfo.applyClickFieldsFromPageRequest(pageId, iAdClickInfo); } return page; } export function addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation, fieldsModifier, isDefaultBrowser) { if (serverData.isNull(pageInformation)) { return; } page.pageMetrics.pageFields = misc.fieldsFromPageInformation(pageInformation); page.pageMetrics.addManyInstructions(impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier)); page.pageMetrics.addData(pageEventFromPageData(objectGraph, pageInformation, fieldsModifier, isDefaultBrowser), [ PageInvocationPoint.pageEnter, ]); page.pageMetrics.addData(pageExitEventFromPageData(objectGraph, pageInformation, fieldsModifier), [ PageInvocationPoint.pageExit, ]); page.pageMetrics.pageRenderFields = pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier); page.pageRenderMetrics = pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier); if (pageRequiresBackEvent(page)) { page.pageMetrics.addData(backEventFromPageData(objectGraph, pageInformation, fieldsModifier), [ PageInvocationPoint.backButton, ]); } const fetchTimingMetricsBuilder = objectGraph.fetchTimingMetricsBuilder; if (isSome(fetchTimingMetricsBuilder)) { fetchTimingMetricsBuilder.decorate(page); } } /** * @param page The page to check * @returns Whether this page requires a back event to be sent when the user navigates back from it. */ function pageRequiresBackEvent(page) { const isSearchResultsPage = page instanceof models.SearchResults; const isContingentOfferDetailPage = page instanceof models.ContingentOfferDetailPage; const isWinbackOfferDetailPage = page instanceof models.OfferItemDetailPage; return !isSearchResultsPage && !isContingentOfferDetailPage && !isWinbackOfferDetailPage; } export function copyMetricsEventsIntoSidepackedPagewithInformation(objectGraph, page, pageInformation) { if (serverData.isNull(pageInformation)) { return; } page.pageMetrics.addData(backEventFromPageData(objectGraph, pageInformation, undefined), [ PageInvocationPoint.backButton, ]); if (serverData.isNull(page.pageMetrics.pageFields)) { page.pageMetrics.pageFields = {}; } } /** * Constructs metrics page information for a page navigated to from the action links on the product page. * @param productId The adamId of the product for which these action links are displayed. * @param pageType The metrics pageType for the page the links navigates to. */ export function pageInformationForActionLinkPage(objectGraph, productId, pageType) { const base = { pageId: productId || "", pageType: pageType, }; return new metricsModels.MetricsPageInformation(base); } /** * Create metrics page information for rooms that may not have an its id. * @param pageId The identifier to differentiate room */ export function pageInformationForRoom(objectGraph, pageId) { const pageType = "Room"; const page = new metricsModels.MetricsPageInformation({ pageType: pageType, pageId: pageId, page: `${pageType}_${pageId}`, }); return page; } /** * Constructs empty page information for search hints 'page' for given prefix term and optional dataSetId. */ export function pageInformationForSearchHintsPage(objectGraph, prefixTerm, pageUrl, dataSetId) { const base = { pageId: "hints", pageType: "Search", }; if (dataSetId) { base[searchMetricsDataSetID] = dataSetId; } const pageInformation = new metricsModels.MetricsPageInformation(base); pageInformation.pageUrl = pageUrl; return pageInformation; } export function pageInformationForIAPPage(objectGraph, parentId, iapId) { const base = { pageId: iapId || "", pageType: "IAPInstallPage", parentId: metricsUtil.emptyStringIfNullOrUndefined(parentId), }; const pageInformation = new metricsModels.MetricsPageInformation(base); return pageInformation; } /** * Build the pageInformation to use for search pages. */ export function pageInformationForSearchPage(objectGraph, request, response, termContext, searchRequestUrl, pageId, sponsoredSearchRequestData, wasOdmlSuccessful, guidedSearchData) { const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Search", pageId, response); pageInformation.searchTermContext = termContext; pageInformation.pageUrl = searchRequestUrl; // For Search, pageUrl is request url if (guidedSearchData) { pageInformation.guidedSearch = guidedSearch.guidedSearchPageInformationFields(objectGraph, request, guidedSearchData); } if (searchAds.platformSupportsAdverts(objectGraph) && sponsoredSearchRequestData != null) { pageInformation.iAdInfo = new metricsModels.IAdSearchInformation(objectGraph, "searchResults", metricsModels.IAdSearchInformation.createInitialSlotInfos(objectGraph, "searchResults", null, null), sponsoredSearchRequestData.iAdId, sponsoredSearchRequestData.appStoreClientRequestId, wasOdmlSuccessful); } return pageInformation; } export function pageInformationForSegmentedSearchPage(objectGraph, response, termContext, searchRequestUrl, groupId) { const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Search", groupId, response); pageInformation.searchTermContext = termContext; pageInformation.pageUrl = searchRequestUrl; return pageInformation; } export function pageInformationForAppPromotionDetailPage(objectGraph, appPromotionType, appPromotionId, parentAppAdamId, referrerData, recoMetricsData) { let pageId = ""; let pageType = ""; switch (appPromotionType) { case models.AppPromotionType.AppEvent: pageId = `${appPromotionId}_${parentAppAdamId}`; pageType = "EventDetails"; break; case models.AppPromotionType.ContingentOffer: case models.AppPromotionType.OfferItem: pageId = `${appPromotionId}`; pageType = "OfferDetails"; break; default: break; } const base = { pageId: pageId, pageType: pageType, }; if (serverData.isDefinedNonNull(referrerData)) { base["refApp"] = referrerData["app"]; base["extRefUrl"] = referrerData["externalUrl"]; } const pageInformation = new metricsModels.MetricsPageInformation(base); pageInformation.recoMetricsData = recoMetricsData !== null && recoMetricsData !== void 0 ? recoMetricsData : undefined; return pageInformation; } export function addPageEventsToInAppPurchasePage(objectGraph, page, pageInformation) { addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); } export function makePageReferralEligible(objectGraph, page) { if (serverData.isNull(page) || serverData.isNull(page.pageMetrics)) { return; } const pageInstructions = page.pageMetrics.instructions; if (!serverData.isNull(pageInstructions)) { for (const instruction of pageInstructions) { // We shoudl only be requesting crossfire information for page events. // if we dont, and add it to all of the instructions then we end up clearing out // the referral information on the native side during any other event that happens on this page // before a buy can happen, for instance an impressions event that occurs on page exit when // displaying an upsell sheet. if (instruction.data.fields["eventType"] !== "page") { continue; } instruction.data.includingFields.push("crossfireReferral"); } } // Ensure that for referral eligible pages, the attribution is included in the purchase configuration // Crossfire: Metrics: extRefApp2/extRefUrl2 buyparam are being persisted too strongly, being sent on buys of other app pages let productLockup = null; if (page instanceof models.ProductPage) { productLockup = page; } else if (page instanceof models.ShelfBasedProductPage) { productLockup = page.lockup; } if (productLockup) { const eligibleActions = []; if (productLockup.buttonAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction); } else if (productLockup.buttonAction instanceof models.OfferConfirmationAction && productLockup.buttonAction.buyAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.buyAction); } else if (productLockup.buttonAction instanceof models.OfferAlertAction && productLockup.buttonAction.completionAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.completionAction); } else if (productLockup.buttonAction instanceof models.OfferStateAction) { if (productLockup.buttonAction.buyAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.buyAction); } if (productLockup.buttonAction.defaultAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.defaultAction); } if (productLockup.buttonAction.openAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.openAction); } if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && productLockup.buttonAction.subscribePageAction.page === "arcadeSubscribe" && isSome(productLockup.buttonAction.subscribePageAction.pageUrl) && productLockup.buttonAction.subscribePageAction.pageUrl.length > 0) { const pageUrl = urls.URL.from(productLockup.buttonAction.subscribePageAction.pageUrl); pageUrl.param(Parameters.includePostSubscribeAttribution, "true"); productLockup.buttonAction.subscribePageAction.pageUrl = pageUrl.build(); } if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && productLockup.buttonAction.subscribePageAction.pageData instanceof models.MarketingItemRequestInfo && productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction instanceof models.OfferAction) { eligibleActions.push(productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction); } } for (const eligibleAction of eligibleActions) { if (eligibleAction.purchaseConfiguration) { eligibleAction.purchaseConfiguration.excludeAttribution = false; } } } } /** * Add an updated set of page metrics to the shelf that will be used to update the existing page metrics, * as well as be sent as a `pageChange` event. * @param objectGraph The object graph. * @param shelf The shelf to attach the pageChange fields to * @param pageInformation The modified page information for the containing page. * @param fieldsModifier A field modifier, if required. * @returns */ export function addPageChangeFieldsToShelfWithInformation(objectGraph, shelf, pageInformation, fieldsModifier) { if (serverData.isNull(pageInformation)) { return; } let updatedEvents = []; // Page const pageFields = misc.fieldsFromPageInformation(pageInformation); const pageEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); updatedEvents.push(pageEvent); // Impressions const impressionsInstructions = impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier); const impressionsEvents = impressionsInstructions.map((instruction) => instruction.data); updatedEvents = updatedEvents.concat(impressionsEvents); filterPageMetricsFieldsForPageChange(pageFields); updatedEvents.forEach((event) => filterPageMetricsFieldsForPageChange(event.fields)); shelf.pageChangeMetrics = { pageFields: pageFields, updatedEvents: updatedEvents, }; } /// An allow list for the fields that can change in a `pageChange` event. const allowedPageChangeKeys = new Set([ "iAdAppStoreClientRequestId", "iAdId", "iAdSlotInfo", "iAdOdmlSuccess", "iAdEligible", "iAdContainerId", "iAdImpressionId", "iAdMetadata", "adamId", "iAd", "iAdPlacementId", "iAdMissedOpportunityReason", "hasiAdData", "iAdTemplateType", // These are required to merge with existing events "eventType", "impressionQueue", ]); /** * Filter some fields for the allowed content for a pageChange event. * @param metricsFields The MetricsFields to filter. */ function filterPageMetricsFieldsForPageChange(metricsFields) { Object.keys(metricsFields) .filter((key) => !allowedPageChangeKeys.has(key)) .forEach((key) => delete metricsFields[key]); } function pageEventFromPageData(objectGraph, pageInformation, fieldsModifier, isDefaultBrowser) { var _a, _b, _c, _d, _e; const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); if (pageInformation.iAdInfo) { // Always copy this, even if no ad is present. Object.assign(base, pageInformation.iAdInfo.pageFields); } // Include pre-order release date for page events. if (serverData.isDefinedNonNull(pageInformation.offerReleaseDate)) { base["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(pageInformation.offerReleaseDate); } const searchTermContext = pageInformation.searchTermContext; if (searchTermContext) { base["searchTerm"] = searchTermContext.term; if (searchTermContext.suggestedTerm) { base["searchSuggestedTerm"] = searchTermContext.suggestedTerm; } if (searchTermContext.correctedTerm) { base["searchCorrectedTerm"] = searchTermContext.correctedTerm; } if (searchTermContext.originatingTerm) { base["searchOriginatingTerm"] = searchTermContext.originatingTerm; } } if (pageInformation.guidedSearch) { Object.assign(base, pageInformation.guidedSearch); } const pageEvent = metricsBuilder.createMetricsPageData(objectGraph, false, (_a = pageInformation.isCrossfireReferralCandidate) !== null && _a !== void 0 ? _a : false, pageInformation.timingMetrics, base, isDefaultBrowser); const hasAdverts = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.iAdIsPresent) !== null && _c !== void 0 ? _c : false; const shouldIncludeAdRotationFields = (_e = (_d = pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; if (hasAdverts && shouldIncludeAdRotationFields) { pageEvent.includingFields.push("advertRotation"); } return pageEvent; } /** * Creates a pageExitEvent which has all of the same fields as a pageEvent but with an eventType of pageExit. * Noted this event is an event that is primarily meant to be used so the JS can get a hook into when a page is * left so we can clear out associated referralContexts */ function pageExitEventFromPageData(objectGraph, pageInformation, fieldsModifier) { const pageExitEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); pageExitEvent.fields["eventType"] = "pageExit"; return pageExitEvent; } function backEventFromPageData(objectGraph, pageInformation, fieldsModifier) { const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); return metricsBuilder.createMetricsBackClickData(objectGraph, base); } function pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier) { const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); if (pageInformation.searchTermContext) { base["searchTerm"] = pageInformation.searchTermContext.term; } // Splice in page fields until all native clients have fix for if (pageInformation.baseFields) { Object.assign(base, pageInformation.baseFields); } return metricsBuilder.createMetricsPageRenderFields(objectGraph, pageInformation.timingMetrics, base); } function impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier) { var _a, _b, _c; const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); if (pageInformation.searchTermContext) { base["searchTerm"] = pageInformation.searchTermContext.term; } const impressionBase = objects.shallowCopyOf(base); if (pageInformation.iAdInfo) { Object.assign(impressionBase, pageInformation.iAdInfo.impressionsFields); } if (pageInformation.guidedSearch) { Object.assign(impressionBase, pageInformation.guidedSearch); } // Regular Impressions const shouldIncludeAdMetrics = serverData.isDefinedNonNull(pageInformation.iAdInfo); const iAdEligibleForWindowCollection = serverData.isNullOrEmpty((_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.missedOpportunityReason) && objectGraph.client.isPad; const shouldIncludeAdRotationFields = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.shouldIncludeAdRotationFields) !== null && _c !== void 0 ? _c : false; const impressionsData = metricsBuilder.createMetricsImpressionsData(objectGraph, impressionBase, shouldIncludeAdMetrics && iAdEligibleForWindowCollection, shouldIncludeAdRotationFields, true); const instruction = { data: impressionsData, invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit], }; // Fast Impressions const impressionsArray = [instruction]; if (shouldIncludeAdMetrics) { const fastImpressions = metricsBuilder.createMetricsFastImpressionsData(objectGraph, impressionBase, pageInformation); impressionsArray.push({ data: fastImpressions, invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit, PageInvocationPoint.timer], }); } return impressionsArray; } function baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier) { const base = {}; // Add offer type for pre-orders (this should be added to everything, including impressions) if (serverData.isDefinedNonNull(pageInformation.offerType)) { base["offerType"] = pageInformation.offerType; } if (fieldsModifier !== undefined && base) { fieldsModifier(base); } return base; } //# sourceMappingURL=page.js.map