diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers')
14 files changed, 3276 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js new file mode 100644 index 0000000..b39f9cf --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js @@ -0,0 +1,331 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { BuyParameters } from "../../../foundation/metrics/buy-parameters"; +import { URL } from "../../../foundation/network/urls"; +import * as productVariants from "../../product-page/product-page-variants"; +import { MetricsReferralContext } from "../metrics-referral-context"; +import * as metricsPosting from "../posting"; +import * as misc from "./misc"; +import { IAdSearchInformation } from "./models"; +import * as metricsUtil from "./util"; +//* ************************* +//* Buy Metrics +//* ************************* +/** + * Adds bag driven metrics fields. + * @param objectGraph + * @returns + */ +function addBagMetricsToBuyParams(objectGraph, baseBuyParams) { + const buyParams = new BuyParameters(baseBuyParams); + const metricsConfiguration = objectGraph.bag.metricsConfiguration; + const language = serverData.asString(metricsConfiguration, "metricsBase.language"); + buyParams.set("languageId", language); + return buyParams.toString(); +} +export function addPageMetricsToBuyParams(objectGraph, baseBuyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2) { + const buyParams = new BuyParameters(baseBuyParams); + addPageMetricsToBuyParamsObject(objectGraph, buyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2); + addExtraInfoToBuyParamsObject(objectGraph, buyParams, pageInformation); + return buyParams.toString(); +} +export function addPageMetricsToBuyParamsObject(objectGraph, buyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2) { + var _a, _b, _c; + const fields = misc.fieldsFromPageInformation(pageInformation); + if (targetType) { + buyParams.set("impressionType", targetType); // impressionType == targetType == locationType. + } + if (kind) { + buyParams.set("kind", kind); + } + const pageId = serverData.asString(serverData.asJSONData(fields), "pageId"); + buyParams.set("pageId", pageId); + const pageType = serverData.asString(serverData.asJSONData(fields), "pageType"); + buyParams.set("pageType", pageType); + // Ad container id + const iAdContainerId = serverData.asString(serverData.asJSONData(fields), "iAdContainerId"); + if (isSome(iAdContainerId) && iAdContainerId.length > 0) { + buyParams.set(adBuyParamKeys.containerId, iAdContainerId, null); + } + // we add search terms to the page if the page is for the item we are buying, or if + // its for a streamlined contingent offer + const pageIds = (_a = pageId === null || pageId === void 0 ? void 0 : pageId.split("_")) !== null && _a !== void 0 ? _a : []; + const pageMatchesProduct = pageIds.includes(adamId); + const isProductPage = pageType === "Software"; + const isStreamlinedContingentOffer = ((_b = buyParams.get("contingentItemId", null)) === null || _b === void 0 ? void 0 : _b.length) > 0; + if (!isProductPage || pageMatchesProduct || isStreamlinedContingentOffer) { + // If there is a native search term, it will overwrite this + // initial value below in `addNativeMetricsToBuyParams`. + let searchTerm = serverData.asString(serverData.asJSONData(pageInformation), "searchTermContext.resultsTerm"); + // A search term may be attached to the product URL as a means of associating buys with searches. + if (serverData.isNullOrEmpty(searchTerm)) { + searchTerm = metricsUtil.searchTermFromProductURL(pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.pageUrl); + } + if (!serverData.isNull(searchTerm)) { + buyParams.set("searchTerm", searchTerm); + } + } + if (isProductPage && isSome(pageInformation) && isSome(pageInformation.pageUrl)) { + const pageUrl = new URL(pageInformation.pageUrl); + if (((_c = pageUrl.query) === null || _c === void 0 ? void 0 : _c["context"]) === "browserChoice") { + buyParams.set("prevPage", "BrowserChoice"); + buyParams.set("browserChoiceScreenBuy", "1", null); + } + } + productVariants.addProductPageVariantMetricsToBuyParams(buyParams, adamId, pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.productVariantData, productVariantData !== null && productVariantData !== void 0 ? productVariantData : undefined); + if (!serverData.isNull(metricsPlatformDisplayStyle) && metricsPlatformDisplayStyle.length > 0) { + buyParams.set("platformDisplayStyle", metricsPlatformDisplayStyle); + } + // App Event ID + buyParams.set("inAppEventId", inAppEventId); + // Referrer + if (!excludeCrossfireAttribution) { + if (serverData.isDefinedNonNull(MetricsReferralContext.shared.activeReferralData)) { + buyParams.set("extRefApp2", MetricsReferralContext.shared.activeReferralData.extRefApp2, null); + buyParams.set("extRefUrl2", MetricsReferralContext.shared.activeReferralData.extRefUrl2, null); + if (isSome(MetricsReferralContext.shared.activeReferralData.kind)) { + const extRefAppKindName = MetricsReferralContext.shared.activeReferralData.kind.name; + if (extRefAppKindName === "clip" || extRefAppKindName === "appClip") { + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + } + } + } + else { + buyParams.set("extRefApp2", extRefApp2, null); + buyParams.set("extRefUrl2", extRefUrl2, null); + } + } +} +function addExtraInfoToBuyParamsObject(objectGraph, buyParams, pageInformation) { + var _a, _b; + if (isNothing(pageInformation)) { + return; + } + const extraInfo = []; + const hasiAdData = isSome((_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.clickFields["hasiAdData"]); + if (hasiAdData) { + const iAdExtraInfoKey = isIAdFromCurrentPage(objectGraph, pageInformation) ? "iAdSponsored" : "iAdOriginated"; + extraInfo.push({ key: iAdExtraInfoKey, value: "true" }); + } + if (serverData.isDefinedNonNullNonEmpty(extraInfo)) { + const extraInfoParamValue = extraInfo + .map((extraInfoValue) => `${extraInfoValue.key}=${extraInfoValue.value}`) + .join(";"); + const encodedValue = (_b = objectGraph.cryptography) === null || _b === void 0 ? void 0 : _b.base64Encode(extraInfoParamValue); + if (isSome(encodedValue)) { + buyParams.set("extraInfo", encodedValue); + } + } +} +/** + * Determines if an iAd is from the current page by comparing the iAd's placement type with the page type. + * + * This function validates whether an advertisement is correctly placed on the appropriate page type. + * Different ad placement types are designed for specific page contexts, and this function ensures + * that the placement matches the current page context. + * + * @param objectGraph - The AppStore object graph providing access to app services and utilities + * @param pageInformation - Information about the current page, including its type and iAd information + * @returns True if the iAd's placement type matches the current page type, false otherwise + * or if pageInformation or iAdInfo is missing + */ +function isIAdFromCurrentPage(objectGraph, pageInformation) { + const iAdInfo = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo; + if (isNothing(pageInformation) || isNothing(iAdInfo)) { + return false; + } + const pageType = pageInformation.baseFields["pageType"]; + const iAdPlacementType = IAdSearchInformation.placementTypeFromPlacementId(objectGraph, iAdInfo.placementId); + switch (iAdPlacementType) { + case "searchLanding": + return pageType === "SearchLanding"; + case "today": + return pageType === "Today"; + case "productPageYMAL": + case "productPageYMALDuringDownload": + return pageType === "Software" || pageType === "SoftwareBundle"; + default: + return pageType === "Search"; + } +} +/** + * Returns a copy of the specified buy parameters enriched with native metrics fields. + * + * @param baseBuyParams The buy parameters of an app which have previously been + * decorated with page metrics. Passing buy parameters that were not decorated with page + * metrics can result in incorrect search terms being reported. + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `baseBuyParams` enriched with native metrics fields. + */ +export function addNativeValuesToBuyParams(objectGraph, baseBuyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics) { + const baseBuyParamsObject = new BuyParameters(baseBuyParams); + addNativeValuesToBuyParamsObject(objectGraph, baseBuyParamsObject, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics); + return baseBuyParamsObject.toString(); +} +/** + * Returns a copy of the specified buy parameters enriched with native metrics fields. + * + * @param baseBuyParams The buy parameters of an app which have previously been + * decorated with page metrics. Passing buy parameters that were not decorated with page + * metrics can result in incorrect search terms being reported. + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `baseBuyParams` enriched with native metrics fields. + */ +export function addNativeValuesToBuyParamsObject(objectGraph, buyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics) { + var _a, _b; + const pageContext = serverData.asString(nativeMetrics, "pageContext"); + buyParams.set("pageContext", pageContext); + const paymentTopic = objectGraph.props.enabled("paymentTopicFromBag") + ? objectGraph.bag.metricsPaymentTopic + : undefined; + buyParams.set("topic", paymentTopic !== null && paymentTopic !== void 0 ? paymentTopic : objectGraph.bag.metricsTopic); + metricsPosting.buyDecorator.useNativeValues(nativeMetrics); + const decoratorParams = metricsPosting.buyDecorator.params; + for (const key of Object.keys(decoratorParams)) { + if (key === "prevPage" && isSome(buyParams.get("prevPage"))) { + // We may have added a `prevPage` param earlier, specifically don't override this value if we have. + continue; + } + const value = serverData.asString(decoratorParams, key); + buyParams.set(key, value); + } + if (!serverData.isNull(osMetrics)) { + for (const key of Object.keys(osMetrics)) { + const value = serverData.asString(osMetrics, key); + buyParams.set(key, value); + buyParams.set(key, value, null); + } + } + if (!nativeMetrics) { + // ^^ Is this actually needed? + buyParams.set("searchTerm", null); + buyParams.set("platformDisplayStyle", null); + return; + } + const hostApp = serverData.asString(nativeMetrics, "hostApp"); + if (isSome(hostApp) && hostApp.length > 0) { + buyParams.set("hostApp", hostApp); + } + const isContingentOffer = ((_a = buyParams.get("contingentItemId", null)) === null || _a === void 0 ? void 0 : _a.length) > 0; + const app = serverData.asString(nativeMetrics, "app"); + if (isContingentOffer) { + // Contingent Offer Streamline buy app install flag + buyParams.set("app", objectGraph.host.clientIdentifier); + } + else if (isSome(app) && app.length > 0) { + buyParams.set("app", app); + } + if (!excludeCrossfireAttribution && !MetricsReferralContext.shared.shouldUseJSReferralData) { + const extRefUrl = serverData.asString(nativeMetrics, "extRefUrl2"); + const extractedSiriRefApp = metricsUtil.extractSiriRefAppFromRefURL(extRefUrl); + if (extRefUrl && extractedSiriRefApp) { + nativeMetrics["refApp"] = extractedSiriRefApp; + } + // ^^ Is this actually needed? + const usageContext = serverData.asString(nativeMetrics, "usageContext"); + if (isSome(usageContext)) { + switch (usageContext) { + case "overlay": + buyParams.set("hostApp", "com.apple.AppStore.overlay"); + break; + case "overlayClip": + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + break; + default: + break; + } + buyParams.set("extRefApp2", hostApp, null); + } + else { + const extRefApp2 = serverData.asString(nativeMetrics, "extRefApp2"); + buyParams.set("extRefApp2", extRefApp2, null); + const extRefUrl2 = serverData.asString(nativeMetrics, "extRefUrl2"); + buyParams.set("extRefUrl2", extRefUrl2, null); + const extRefAppType = serverData.asString(nativeMetrics, "extRefAppType"); + if (extRefAppType === "clip") { + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + } + } + } + // we add search terms to the page if the page is for the item we are buying + const pageId = buyParams.get("pageId"); + const pageType = buyParams.get("pageType"); + const pageIds = (_b = pageId === null || pageId === void 0 ? void 0 : pageId.split("_")) !== null && _b !== void 0 ? _b : []; + const pageMatchesProduct = pageIds.includes(adamId); + const isProductPage = pageType === "Software"; + if (!isProductPage || pageMatchesProduct) { + const searchTerm = metricsUtil.searchTermFromRefURL(serverData.asString(nativeMetrics, "refUrl")); + if (serverData.isDefinedNonNull(searchTerm)) { + buyParams.set("searchTerm", searchTerm); + } + } + // Remove ownerDsid from buyParams + buyParams.set("ownerDsid", null, null); +} +/** + * + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param buyParams The buy parameters of an app from a Media API response. + * @param pageInformation The purchase configuration page metrics configuration + * from an offer action. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param targetType The target type that buy occured on + * @param kind The Kind that buy occured on, if any. + * @param metricsPlatformDisplayStyle The platform display style from an offer action. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `buyParams` decorated with all metrics. + */ +export function addMetricsToBuyParams(objectGraph, adamId, buyParams, pageInformation, excludeCrossfireAttribution, isAppInstalled, targetType, kind, metricsPlatformDisplayStyle, nativeMetrics, osMetrics, productVariantData, inAppEventId, extRefApp2, extRefUrl2) { + // Future: Build BuyParameters once, instead of creating it per modification. + const bagMetricsBuyParams = addBagMetricsToBuyParams(objectGraph, buyParams); + const pageMetricsBuyParams = addPageMetricsToBuyParams(objectGraph, bagMetricsBuyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2); + const nativeMetricsBuyParams = addNativeValuesToBuyParams(objectGraph, pageMetricsBuyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics); + return nativeMetricsBuyParams; +} +/** + * Keys used for ad-related buy params. + */ +export const adBuyParamKeys = { + containerId: "mtContainerId", + placementId: "mtIadPlacementId", + templateType: "mtIadTemplateType", +}; +/** + * Copy ad-related buy params to the override buy params. + * Override buy params are used for updated and redownloads, and in those cases we lose iAd download attribution. + * If the original buy params had ad fields, we should copy them over here. + * @param originalBuyParamsString The original buy params attached to the offerAction. + * @param overrideBuyParamsString The override buy params from the purchase. + * @returns An updated buy params string with any ad-related fields copied over. + */ +export function copyAdBuyParamsToOverrideBuyParams(originalBuyParamsString, overrideBuyParamsString) { + const originalBuyParams = new BuyParameters(originalBuyParamsString); + const overrideBuyParams = new BuyParameters(overrideBuyParamsString); + const originalPlacementId = originalBuyParams.get(adBuyParamKeys.placementId, null); + if ((originalPlacementId === null || originalPlacementId === void 0 ? void 0 : originalPlacementId.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.placementId, null))) { + overrideBuyParams.set(adBuyParamKeys.placementId, originalPlacementId, null); + } + const originalContainerId = originalBuyParams.get(adBuyParamKeys.containerId, null); + if ((originalContainerId === null || originalContainerId === void 0 ? void 0 : originalContainerId.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.containerId, null))) { + overrideBuyParams.set(adBuyParamKeys.containerId, originalContainerId, null); + } + const originalTemplateType = originalBuyParams.get(adBuyParamKeys.templateType, null); + if ((originalTemplateType === null || originalTemplateType === void 0 ? void 0 : originalTemplateType.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.templateType, null))) { + overrideBuyParams.set(adBuyParamKeys.templateType, originalTemplateType, null); + } + return overrideBuyParams.toString(); +} +//# sourceMappingURL=buy.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js new file mode 100644 index 0000000..cc7b6f2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js @@ -0,0 +1,458 @@ +import * as metricsBuilder from "../builder"; +import * as metricsConstants from "./constants"; +import * as metricsLocation from "./location"; +import * as metricsModels from "./models"; +import * as metricsUtil from "./util"; +import * as metricsMisc from "./misc"; +import * as validation from "@jet/environment/json/validation"; +import { isNothing, 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 dateUtil from "../../../foundation/util/date-util"; +//* ************************* +//* Click Metrics +//* ************************* +export function clickOptionsForLockup(objectGraph, data, baseOptions, clickOptions) { + return validation.context("clickOptionsForLockup", () => { + const contextualAdamId = data.id.slice(); + let id = data.id; + if (baseOptions.anonymizationOptions !== undefined && + baseOptions.anonymizationOptions.anonymizationString.length > 0) { + id = baseOptions.anonymizationOptions.anonymizationString; + } + const metricsOptions = { + ...baseOptions, + ...clickOptions, + id: id, + contextualAdamId: contextualAdamId, + softwareType: metricsUtil.softwareTypeForData(objectGraph, data), + }; + if (serverData.isNullOrEmpty(metricsOptions.targetType)) { + metricsOptions.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + // There's a delicate false split from `baseOptions` and click options interface. Some paths pre-populate an unexpected `kind`, but we don't want to change that suddenly. + // Derive `kind` from data if missing. + if (serverData.isNull(metricsOptions.kind)) { + metricsOptions.kind = metricsUtil.metricsKindFromData(objectGraph, data); + } + // Include offerType for pre-order impressions + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + if (isPreorder) { + metricsOptions.offerType = "preorder"; + } + return metricsOptions; + }); +} +export function addBuyEventToOfferActionOnPage(objectGraph, action, options, isPreorder, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const pageInformation = options.pageInformation; + const buttonMetricsOptions = { + ...options, + targetType: "button", + }; + const metricsLocations = metricsLocation.createContentLocation(objectGraph, buttonMetricsOptions, (_a = action.title) !== null && _a !== void 0 ? _a : ""); + let targetId = (_c = (_b = options.anonymizationOptions) === null || _b === void 0 ? void 0 : _b.anonymizationString) !== null && _c !== void 0 ? _c : action.adamId; + if (isSome(options.targetId) && ((_d = options.targetId) === null || _d === void 0 ? void 0 : _d.length) > 0) { + targetId = options.targetId; + } + addBuyEventToOfferAction(objectGraph, action, targetId, isPreorder, pageInformation, metricsLocations, (_e = options.isAdvert) !== null && _e !== void 0 ? _e : false, options.recoMetricsData, isDefaultBrowser); +} +export function addBuyEventToOfferActionInheritingMetrics(objectGraph, newAction, originalAction, isPreorder) { + const originalPageInformation = originalAction.purchaseConfiguration.pageInformation; + // Find some event to inherit location event field from. + let metricsLocations; + if (isSome(originalAction.actionMetrics)) { + for (const event of originalAction.actionMetrics.data) { + metricsLocations = serverData.asArrayOrEmpty(serverData.asJSONValue(event.fields), "location"); + if (metricsLocations) { + break; + } + } + } + // Fine for now - CMC doesn't use anonymization. + addBuyEventToOfferAction(objectGraph, newAction, newAction.adamId, isPreorder, originalPageInformation, metricsLocations, false); +} +function addBuyEventToOfferAction(objectGraph, action, targetId, isPreorder, pageInformation, metricsLocations, isAdvert, recoMetricsData, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const eventFields = {}; + if (pageInformation) { + // We can't always check that `pageInformation` is an `instanceof MetricsPageInformation` here, because sometimes + // `pageInformation` is reconstituted from JSON which means it doesn't get the underlying type information. + // Instead, cast it and check for the existence of individual properties on the cast object. + const metricsPageInformation = pageInformation; + if (isAdvert && + ((_c = (_b = (_a = metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.iAdAdamId) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0 && + isSome(metricsPageInformation.iAdInfo) && + metricsPageInformation.iAdInfo.iAdAdamId === action.adamId) { + Object.assign(eventFields, metricsPageInformation.iAdInfo.clickFields); + } + if (serverData.isDefinedNonNullNonEmpty(metricsPageInformation.searchTermContext)) { + eventFields["searchTerm"] = metricsPageInformation.searchTermContext.term; + } + } + // Add the reco metrics data if available + if (isSome(recoMetricsData)) { + Object.assign(eventFields, recoMetricsData); + } + eventFields["actionDetails"] = { buyParams: action.purchaseConfiguration.buyParams }; + if (metricsLocations !== undefined) { + eventFields["location"] = metricsLocations; + } + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + eventFields[metricsConstants.contextualAdamIdKey] = action.adamId; + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + // Add Pre-order fields + if (isPreorder) { + eventFields["offerType"] = "preorder"; + if (serverData.isDefinedNonNull(action.expectedReleaseDate)) { + eventFields["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(action.expectedReleaseDate); + } + } + const clickEvent = metricsBuilder.createMetricsClickData(objectGraph, targetId, "button", eventFields, undefined, isDefaultBrowser); + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + eventFields[metricsConstants.contextualAdamIdKey] = action.adamId; // needed for `appState` + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + clickEvent.includingFields.push("appState"); + if (action.purchaseConfiguration.isArcadeApp) { + // Include button names in arcade buy action metrics + clickEvent.includingFields.push("buttonName"); + } + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (isAdvert && shouldIncludeAdRotationFields) { + clickEvent.includingFields.push("advertRotation"); + } + const shouldIncludeAdWindowFields = isAdvert && objectGraph.client.isPad; + if (shouldIncludeAdWindowFields) { + clickEvent.includingFields.push("advertDeviceWindow"); + } + action.actionMetrics.addMetricsData(clickEvent); +} +export function addClickEventToArcadeBuyInitiateAction(objectGraph, action, options) { + var _a; + addClickEventToAction(objectGraph, action, { + ...options, + actionType: "buyInitiate", + subscriptionSKU: (_a = objectGraph.bag.arcadeProductId) !== null && _a !== void 0 ? _a : undefined, + actionContext: "Arcade", + targetType: "button", + }); +} +export function addClickEventToAction(objectGraph, action, options, addIAdFields = false, targetType) { + var _a, _b, _c, _d, _e, _f, _g; + let actionType = options.actionType; + if (!actionType) { + actionType = "navigate"; + } + const eventFields = { + actionType: actionType, + }; + // Ad click events will be wrapped in a CompoundAction. We still want to handle attaching `actionUrl` properly + // below if a `FlowAction` or `ExternalUrlAction` are inside the `CompoundAction`, so we create an array of actions + // to check. + let actions; + if (action instanceof models.CompoundAction) { + actions = action.actions; + } + else { + actions = [action]; + } + actions.forEach((subAction) => { + // Set the action URL if appropriate + if (subAction instanceof models.FlowAction) { + const flowAction = subAction; + eventFields["actionUrl"] = flowAction.pageUrl; + } + else if (subAction instanceof models.ExternalUrlAction) { + const flowAction = subAction; + eventFields["actionUrl"] = flowAction.url; + } + }); + if (options.actionDetails) { + eventFields["actionDetails"] = options.actionDetails; + } + if (options.actionContext) { + eventFields["actionContext"] = options.actionContext; + } + // Add offer type for pre-orders + if (serverData.isDefinedNonNull(options.offerType)) { + eventFields["offerType"] = options.offerType; + } + // Add release date for pre-orders + if (serverData.isDefinedNonNull(options.offerReleaseDate)) { + eventFields["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(options.offerReleaseDate); + } + const title = (_c = (_b = (_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) !== null && _b !== void 0 ? _b : action === null || action === void 0 ? void 0 : action.title) !== null && _c !== void 0 ? _c : ""; + eventFields["location"] = metricsLocation.createContentLocation(objectGraph, options, title); + // Search Term + if (options.pageInformation && options.pageInformation.searchTermContext) { + eventFields["searchTerm"] = options.pageInformation.searchTermContext.term; + } + if (serverData.isDefinedNonNull(options.softwareType)) { + eventFields["softwareType"] = options.softwareType; + } + // Advert Metrics + let additionalIncludingFields; + // Do we need both `options.isAdvert` and `addIAdFields`? + if ((options.isAdvert || options.isAdEligible) && addIAdFields && ((_d = options.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo)) { + Object.assign(eventFields, options.pageInformation.iAdInfo.clickFields); + if (objectGraph.client.isPad) { + additionalIncludingFields = ["advertDeviceWindow"]; + } + } + if (options.mercuryMetricsData) { + Object.assign(eventFields, options.mercuryMetricsData); + } + if (isSome(options.subjectIds)) { + eventFields["subjectIds"] = options.subjectIds; + } + const event = metricsBuilder.createMetricsClickData(objectGraph, options.id, targetType !== null && targetType !== void 0 ? targetType : metricsUtil.targetTypeForMetricsOptions(objectGraph, options), eventFields, additionalIncludingFields); + // Include button names in arcade buy action metrics + const isArcadeBuyAction = options.actionContext === "Arcade" && (options.actionType === "buy" || options.actionType === "buyInitiate"); + if (isArcadeBuyAction) { + event.includingFields.push("buttonName"); + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + event.fields[metricsConstants.contextualAdamIdKey] = options.contextualAdamId; + if (isSome(action.adamId)) { + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + } + } + // Include ad rotation metrics and click events if enabled for placement. + const shouldIncludeAdRotationFields = (_g = (_f = (_e = options.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.shouldIncludeAdRotationFields) !== null && _g !== void 0 ? _g : false; + if (options.isAdvert && shouldIncludeAdRotationFields) { + event.includingFields.push("advertRotation"); + } + action.actionMetrics.addMetricsData(event); +} +/** + * + * @param objectGraph The App Store Object Graph + * @param action The search cancel or dismiss action + * @param options The metrics click options + * @param targetType The metrics click target type + * @param searchTerm The current search term + */ +export function addClickEventToSearchCancelOrDismissAction(objectGraph, action, options, targetType, searchTerm) { + const eventFields = { + searchTerm: searchTerm, + actionType: options.actionType, + }; + if (options.actionDetails) { + eventFields["actionDetails"] = options.actionDetails; + } + if (options.actionContext) { + eventFields["actionContext"] = options.actionContext; + } + // Search Term + if (searchTerm) { + eventFields["searchTerm"] = searchTerm; + } + if (serverData.isDefinedNonNull(options.softwareType)) { + eventFields["softwareType"] = options.softwareType; + } + if (options.mercuryMetricsData) { + Object.assign(eventFields, options.mercuryMetricsData); + } + const event = metricsBuilder.createMetricsClickData(objectGraph, options.id, targetType !== null && targetType !== void 0 ? targetType : metricsUtil.targetTypeForMetricsOptions(objectGraph, options), eventFields); + action.actionMetrics.addMetricsData(event); +} +export function addClickEventsToAdLockup(objectGraph, lockup, clickOptions) { + var _a, _b, _c, _d, _e; + const searchAd = (_b = (_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd) !== null && _b !== void 0 ? _b : lockup.searchAd; + if (serverData.isNull(searchAd)) { + return; + } + // Theoretically, we would probably be fine to just overwrite the click event action metrics below in `addClickEventToAction`. + // But we'll keep the call to clear the metrics just to provide continuity with older code that ran this full path. + (_c = lockup.clickAction) === null || _c === void 0 ? void 0 : _c.actionMetrics.clearAll(); + if (lockup.clickAction) { + addClickEventToAction(objectGraph, lockup.clickAction, clickOptions, true); + } + const pageInformation = clickOptions.pageInformation; + const eventFields = { + actionType: "ad_transparency", + }; + if (pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) { + Object.assign(eventFields, pageInformation.iAdInfo.clickFields); + } + const figaroEvent = metricsBuilder.createMetricsClickData(objectGraph, lockup.adamId, "button", eventFields); + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (shouldIncludeAdRotationFields) { + figaroEvent.includingFields.push("advertRotation"); + } + searchAd.transparencyAction.actionMetrics.addMetricsData(figaroEvent); +} +export function addClickEventToSeeAllAction(objectGraph, action, url, options) { + const eventFields = {}; + if (serverData.isDefinedNonNull(url)) { + eventFields["actionUrl"] = url; + } + if (!options.targetType) { + options.targetType = "button"; + } + eventFields["location"] = metricsLocation.createBasicLocation(objectGraph, options, action.title); + eventFields["actionType"] = "navigate"; + eventFields["target"] = "button_See All"; + const event = metricsBuilder.createMetricsClickData(objectGraph, "See All", "button", eventFields); + action.actionMetrics.addMetricsData(event); +} +export function addClickEventToClearSearchHistoryAction(objectGraph, clearAction) { + // MAINTAINER'S NOTE: + // We intentionally use an unlocalized targetId, instead of localized `clearAction.title`, + // so metrics are consistent regardless of language. + const clickMetrics = metricsBuilder.createMetricsClickData(objectGraph, "Clear Searches", "button", { + actionType: "confirm", + }); + clearAction.actionMetrics.addMetricsData(clickMetrics); +} +export function addClickEventToActivityFeedMetrics(objectGraph, actionMetrics, title, targetId, options) { + const eventFields = { + actionType: "navigate", + id: targetId, + idType: "static", + location: metricsLocation.createBasicLocation(objectGraph, options, title), + }; + const clickEvent = metricsBuilder.createMetricsClickData(objectGraph, targetId, "link", eventFields); + actionMetrics.addMetricsData(clickEvent); +} +export function addClickEventToPageFacetsChangeAction(objectGraph, action, filterParameter) { + const eventFields = {}; + eventFields["actionType"] = "filter"; + const event = metricsBuilder.createMetricsClickData(objectGraph, `filter_${filterParameter}`, "button", eventFields); + event.includingFields.push("selectedPageFacets"); + action.actionMetrics.addMetricsData(event); +} +//* ************************* +//* Search Metrics +//* ************************* +/** + * Adds a given `SearchAction` with the metrics data based on the Search it will fire, i.e data for: + * - Click Event + * - Search Event + * + * @param action Action to add metrics to + * @param target Target of this action. This should correspond to the UI this action is attached to. + * @param locationTracker Location tracker. + */ +export function addEventsToSearchAction(objectGraph, action, target, locationTracker, pageInformation) { + var _a, _b, _c, _d; + const actionType = metricsActionTypeForSearchOrigin(action.origin); + if (isNothing(pageInformation)) { + pageInformation = new metricsModels.MetricsPageInformation({ + page: "Search", + pageType: "Search", + pageId: "Search", + pageDetails: "Apps", // Legacy. Note sure why this is Apps. + }); + } + const options = { + pageInformation: pageInformation, + locationTracker: locationTracker, + targetType: target, + }; + // Click-Only Fields + const clickFields = { + ...metricsMisc.fieldsFromPageInformation(pageInformation), + actionType: actionType, + actionUrl: metricsUtil.emptyStringIfNullOrUndefined(action.url), + location: metricsLocation.createBasicLocation(objectGraph, options, action.term), + searchTerm: action.term, + }; + // Search-Only Fields + const searchFields = { + targetId: action.term, + }; + const searchActionDetails = {}; + if ((_a = action.prefixTerm) === null || _a === void 0 ? void 0 : _a.length) { + // Keep `searchPrefix` in `actionDetails` for search events + searchActionDetails["searchPrefix"] = action.prefixTerm; + } + if ((_b = action.entity) === null || _b === void 0 ? void 0 : _b.length) { + searchActionDetails["hintsEntity"] = action.entity; + } + if (serverData.isDefinedNonNullNonEmpty(searchActionDetails)) { + searchFields["actionDetails"] = searchActionDetails; + } + // Shared Fields + if ((_c = action.originatingTerm) === null || _c === void 0 ? void 0 : _c.length) { + clickFields["searchOriginatingTerm"] = action.originatingTerm; + searchFields["searchOriginatingTerm"] = action.originatingTerm; + } + // SSS: Clicks must be before Search + const clickData = metricsBuilder.createMetricsClickData(objectGraph, action.term, target, clickFields, [ + "searchGhostHint", + ]); + action.actionMetrics.addMetricsData(clickData); + const searchData = metricsBuilder.createMetricsSearchData(objectGraph, action.term, target, actionType, (_d = action.url) !== null && _d !== void 0 ? _d : null, searchFields, ["searchGhostHint"]); + action.actionMetrics.addMetricsData(searchData); +} +/** + * Returns the mapped action type for given search origin. + * @param origin Origin to resolve action type for + */ +function metricsActionTypeForSearchOrigin(origin) { + // actionType based on search origin. + switch (origin) { + case "trending": + return "trending"; + case "suggested": + return "suggested"; + case "recents": + return "recentQuery"; + case "hints": + return "hint"; + case "undoSpellCorrection": + return "searchInsteadFor"; + case "applySpellCorrection": + return "didYouMean"; + case "userTypedHint": + return "userTypedHint"; + // It is unexpected to see other search origins here. These are built in native: + default: + return "submit"; + } +} +// region Segmented Search +/** + * Add click metrics to segment change action + * @param action Action to add click event to. + * @param locationTracker Location tracker + */ +export function addEventsToSegmentChangeAction(objectGraph, action, targetId, locationTracker) { + // Click-Only Fields + const targetType = "link"; + const clickFields = { + actionType: "navigate", + location: metricsLocation.createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: locationTracker, + targetType: targetType, + }, action.title), + }; + const clickData = metricsBuilder.createMetricsClickData(objectGraph, targetId, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); +} +/** + * Add click metrics to segment change action + * @param action Action to add click event to. + * @param locationTracker Location tracker + */ +export function addClickEventToSearchPageSegmentChangeAction(objectGraph, action, targetId, locationTracker) { + // Click-Only Fields + const targetType = "SearchResults"; + const clickFields = { + actionType: "navigate", + location: metricsLocation.createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: locationTracker, + targetType: targetType, + }, "searchPageSegmentChange"), + }; + const clickData = metricsBuilder.createMetricsClickData(objectGraph, targetId, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); +} +// endregion +//# sourceMappingURL=clicks.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js new file mode 100644 index 0000000..c23084b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js @@ -0,0 +1,11 @@ +/** + * HACK: <rdar://problem/65687333> Tech Debt: Metrics: Adam ID attribution should be explicit + * This is an incremental step out of a hack that used `targetId` as `adamId` for decorating adamId-related fields for a given `MetricsData`. + * It should be leveraged for paths that use field providers that have an adam id dependency. + * + * As of 2022E we are using this key in the `custom` field of `actionMetrics`, which is a recommended method of sending down + * additional data to augment metrics events but shouldn't be included in the event itself. `custom` fields are automatically + * added to the `MetricsFieldsContext` by `ActionDispatcher`, allowing `MetricsFieldsProvider`s to access and use this value. + */ +export const contextualAdamIdKey = "jet_adamId"; +//# sourceMappingURL=constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js new file mode 100644 index 0000000..edc1d96 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js @@ -0,0 +1,419 @@ +import * as metricsLocation from "./location"; +import * as metricsUtil from "./util"; +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as productVariants from "../../product-page/product-page-variants"; +import * as lockups from "../../../common/lockups/lockups"; +//* ************************* +//* Impression Metrics +//* ************************* +function generateImpressionFields(objectGraph, options) { + /// please note this is not the metrics event itself but the fields for the items that impress + /// there is a very strict spec for these items so please do not modify this unless you know + /// exactly what you are doing. + var _a, _b, _c, _d; + let id = options.id; + let title = options.title; + if (serverData.isDefinedNonNullNonEmpty(options.anonymizationOptions)) { + const anonymizationString = (_b = (_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) !== null && _b !== void 0 ? _b : "ANONYMOUS"; + id = anonymizationString; + title = anonymizationString; + } + else if (id && options.createUniqueImpressionId && !objectGraph.client.isWatch) { + id = createUniqueImpressionId(objectGraph, id); + } + const impressionData = { + id: metricsUtil.emptyStringIfNullOrUndefined(id), + name: metricsUtil.emptyStringIfNullOrUndefined(title), + impressionType: metricsUtil.targetTypeForMetricsOptions(objectGraph, options), + }; + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + impressionData["idType"] = idType; + } + if (options && options.kind) { + impressionData["kind"] = options.kind; + } + if (options && options.softwareType) { + impressionData["softwareType"] = options.softwareType; + } + if (options && options.recoMetricsData) { + Object.assign(impressionData, options.recoMetricsData); + } + if (options && options.mercuryMetricsData) { + Object.assign(impressionData, options.mercuryMetricsData); + } + if (options && options.lockupDisplayStyle) { + impressionData["platformDisplayStyle"] = options.lockupDisplayStyle; + } + const shouldOmitImpressionIndex = (_c = options.shouldOmitImpressionIndex) !== null && _c !== void 0 ? _c : false; + if (options && options.locationTracker && !shouldOmitImpressionIndex) { + const currentPosition = metricsLocation.currentPosition(options.locationTracker); + impressionData["impressionIndex"] = currentPosition; + if (impressionData.id === "") { + impressionData.id = currentPosition.toString(); + impressionData["idType"] = "sequential"; + } + } + if (options && options.modelSource) { + impressionData["modelSource"] = options.modelSource; + } + // Add offerType if available + if (serverData.isDefinedNonNull(options.offerType)) { + impressionData["offerType"] = options.offerType; + } + // Arcade Upsell Tracking + if (options && serverData.isDefinedNonNull(options.displaysArcadeUpsell)) { + impressionData["displaysArcadeUpsell"] = options.displaysArcadeUpsell; + } + // Preorder Tracking + if (options && serverData.isDefinedNonNull(options.isPreorder)) { + impressionData["isPreorder"] = options.isPreorder; + } + // Add adamId if available + if (serverData.isDefinedNonNull(options.adamId) && serverData.isNullOrEmpty(options.anonymizationOptions)) { + impressionData["adamId"] = options.adamId; + } + // Badges + if (options && serverData.isDefinedNonNull(options.badges)) { + impressionData["badges"] = options.badges; + } + // In App Event ID + if (options && serverData.isDefinedNonNull(options.inAppEventId)) { + impressionData["inAppEventId"] = options.inAppEventId; + } + // Related subject IDs + if (options && serverData.isDefinedNonNull(options.relatedSubjectIds)) { + impressionData["relatedSubjectIds"] = options.relatedSubjectIds; + } + /// Hints entity + if ((_d = options === null || options === void 0 ? void 0 : options.hintsEntity) === null || _d === void 0 ? void 0 : _d.length) { + impressionData["hintsEntity"] = options.hintsEntity; + } + // autoAdvanceInterval for auto scrolling Views + if (options && serverData.isDefinedNonNull(options.autoAdvanceInterval)) { + impressionData["autoAdvanceInterval"] = options.autoAdvanceInterval; + } + // Add fcKind if available + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.fcKind)) { + impressionData["fcKind"] = options.fcKind; + } + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.canonicalId)) { + impressionData["canonicalId"] = options.canonicalId; + } + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.displayStyle)) { + impressionData["displayStyle"] = options.displayStyle; + } + // Add product variant fields if available + if (serverData.isDefinedNonNull(options.productVariantData)) { + Object.assign(impressionData, productVariants.contentFieldsForProductVariantData(options.productVariantData)); + } + if (isSome(options.contentRating)) { + impressionData["contentRating"] = options.contentRating; + } + if (isSome(options.bundleId)) { + impressionData["bundleId"] = options.bundleId; + } + if (impressionData.id === "") { + objectGraph.console.log(`impressionId missing. Tracking broken for ${impressionData.name} of ${impressionData.impressionType}`); + } + return impressionData; +} +export function addImpressionFields(objectGraph, impressionable, options) { + if (!impressionable) { + return; + } + impressionable.impressionMetrics = new models.ImpressionMetrics(generateImpressionFields(objectGraph, options)); +} +export function addImpressionFieldsToTagRoomHeader(objectGraph, impressionable, options) { + if (!impressionable) { + return; + } + const impressionMetrics = new models.ImpressionMetrics(generateImpressionFields(objectGraph, options)); + delete impressionMetrics.fields.impressionIndex; + impressionable.impressionMetrics = impressionMetrics; +} +/** + * Adds impression fields to the search metadata ribbon item. + * + * @param objectGraph - The object graph. + * @param metadataRibbonItem - The metadata ribbon item. + * @param options - The metrics impression options. + */ +export function addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, metadataRibbonItem, options) { + var _a; + if (!metadataRibbonItem) { + return; + } + addImpressionFields(objectGraph, metadataRibbonItem, options); + if ((options.isAdvert || options.isAdEligible) && + ((_a = options.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) && + metadataRibbonItem.impressionMetrics) { + metadataRibbonItem.impressionMetrics = new models.FastImpressionMetrics(metadataRibbonItem.impressionMetrics, true); + } +} +/** + * Add impressions fields to a Today card. + * @param objectGraph The object graph. + * @param card The Today card to apply the impressions fields to. + * @param options A set of metrics options to gather data from. + * @param franchise The card franchise. + * @param cardType The card type. + * @param isOnboardingCard Whether the card is an onboarding card. + * @param coerceNullToEmptyStrings Whether `null` values for `franchise` and `cardType` should be coerced into empty strings. This + * was a behaviour that was always enabled by default, but as of the implementation of Chainlink ads, we don't necessarily want empty strings. + * Defaults to true to maintain legacy behaviour for other cards that don't want to opt out. + * @returns + */ +export function addImpressionsFieldsToTodayCard(objectGraph, card, options, franchise, cardType, isOnboardingCard, coerceNullToEmptyStrings = true) { + var _a, _b, _c, _d, _e, _f; + if (!card) { + return; + } + const impressionData = generateImpressionFields(objectGraph, options); + if (coerceNullToEmptyStrings) { + impressionData["franchise"] = metricsUtil.emptyStringIfNullOrUndefined(franchise); + impressionData["cardType"] = metricsUtil.emptyStringIfNullOrUndefined(cardType); + } + else { + if (franchise) { + impressionData["franchise"] = franchise; + } + if (cardType) { + impressionData["cardType"] = cardType; + } + } + if (isOnboardingCard) { + impressionData["isOnboardingCard"] = isOnboardingCard; + } + if (((_b = (_a = options === null || options === void 0 ? void 0 : options.optimizationEntityId) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) { + impressionData["optimizationEntityId"] = options.optimizationEntityId; + } + if (((_d = (_c = options === null || options === void 0 ? void 0 : options.optimizationId) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0) { + impressionData["optimizationId"] = options.optimizationId; + } + if (isSome(options === null || options === void 0 ? void 0 : options.rowIndex)) { + impressionData["rowIndex"] = options.rowIndex; + } + card.impressionMetrics = new models.ImpressionMetrics(impressionData); + if ((options.isAdvert || options.isAdEligible) && ((_e = options.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo)) { + const sanitizedIAdData = metricsUtil.sanitizedMetricsDictionary(options.pageInformation.iAdInfo.fastImpressionsFieldsForCurrentItem(options.locationTracker, options.adSlotOverride)); + Object.assign(card.impressionMetrics.fields, sanitizedIAdData); + card.impressionMetrics = new models.FastImpressionMetrics(card.impressionMetrics, true); + if (options.isAdvert) { + (_f = card.impressionMetrics) === null || _f === void 0 ? true : delete _f.fields["cardType"]; + } + } +} +export function addImpressionsFieldsToAd(objectGraph, impressionable, options, iAdData) { + if (!impressionable || !iAdData) { + return; + } + addImpressionFields(objectGraph, impressionable, options); + const sanitizedIAdData = metricsUtil.sanitizedMetricsDictionary(iAdData.fastImpressionsFieldsForCurrentItem(options.locationTracker, options.adSlotOverride)); + if (isSome(impressionable.impressionMetrics)) { + Object.assign(impressionable.impressionMetrics.fields, sanitizedIAdData); + const disableFastImpressions = serverData.asBooleanOrFalse(options.disableFastImpressionsForAds); + impressionable.impressionMetrics = new models.FastImpressionMetrics(impressionable.impressionMetrics, !disableFastImpressions); + } + /** + * This is an longstanding hack to prevent ad and organic impression showing same content (i.e. AdamId) + * to properly have nonclashing identifiers, and have separate impression objects. + * Even with impression item ids' hasing on `impressionIndex` - this is still needed in case the Nth rotated ad is the same as Nth organic. + * This is orthogonal to "ad_container" in location stack when building ads for ad-rotation, which is a separate metrics trick. + */ + if (isSome(impressionable.impressionMetrics)) { + impressionable.impressionMetrics.fields["parentId"] = "ad_container"; + } +} +export function addImpressionFieldsToInAppPurchaseLockup(objectGraph, lockup, options) { + if (!lockup) { + return; + } + // The code that calls this method currently does this indirectly, + // this is just to guard against that code changing in the future. + if (!lockup.impressionMetrics) { + addImpressionFields(objectGraph, lockup, options); + } + if (lockup.parent && lockup.parent.adamId && isSome(lockup.impressionMetrics)) { + lockup.impressionMetrics.fields["parentAdamId"] = metricsUtil.emptyStringIfNullOrUndefined(lockup.parent.adamId); + } +} +export function impressionOptionsForLockup(objectGraph, data, lockup, displayStyle, baseOptions, canDisplayArcadeOfferButton, attributePlatformOverride = undefined) { + var _a; + const options = impressionOptions(objectGraph, data, lockup.title, baseOptions); + options.lockupDisplayStyle = displayStyle; + options.contentRating = (_a = lockup.offerDisplayProperties) === null || _a === void 0 ? void 0 : _a.contentRating; + options.bundleId = lockup.bundleId; + // If no targetType is provided, set the correct value for the platform. + if (serverData.isNullOrEmpty(options.targetType)) { + options.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + if (canDisplayArcadeOfferButton && content.isArcadeSupported(objectGraph, data)) { + options.displaysArcadeUpsell = true; + } + // If it has a discounted offer then use the options already set + const parentID = baseOptions["id"]; + if (serverData.isDefinedNonNullNonEmpty(lockups.discountedOfferFromData(data)) && + serverData.isDefinedNonNull(parentID) && + parentID.length > 0) { + options.id = parentID; + } + return options; +} +export function impressionOptions(objectGraph, data, title, baseOptions, attributePlatformOverride = undefined) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + }; + // Include offerType for pre-order impressions + // NOTE: Even though metricsOptions.isPreorder may be true, we don't key off that here because + // offerType implies an offer exists, which is not always true (e.g. an Arcade Coming Soon breakout). + const containsPreorderOffer = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder", attributePlatformOverride); + if (containsPreorderOffer) { + metricsOptions.offerType = "preorder"; + } + return metricsOptions; + }); +} +/// Returns impression options for Arcade See All Games ribbon item. +export function impressionOptionsForArcadeSeeAllGamesRibbonItem(baseOptions) { + return validation.context("impressionOptionsForArcadeSeeAllGamesRibbonItem", () => { + const metricsOptions = { + ...baseOptions, + id: "AllGames", + idType: "none", + kind: null, + softwareType: null, + title: "All Games", + }; + return metricsOptions; + }); +} +/// Returns impression options for tag ribbon item on product page. +export function impressionOptionsForTagRibbonItem(objectGraph, data, title, baseOptions) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + idType: "its_id", + displayStyle: "textOnly", + }; + return metricsOptions; + }); +} +/// Returns impression options for tag ribbon item on product page. +export function impressionOptionsForTagHeader(objectGraph, data, title, baseOptions) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + idType: "its_contentId", + targetType: "tagHeader", + }; + return metricsOptions; + }); +} +/// Returns impression options for Arcade Choose Your Favorites brick. +export function impressionOptionsForArcadeChooseYourFavoritesBrick(baseOptions) { + return validation.context("impressionOptionsForArcadeChooseYourFavoritesBrick", () => { + const metricsOptions = { + ...baseOptions, + id: "", + kind: null, + softwareType: null, + title: "choose_your_games_brick", + }; + return metricsOptions; + }); +} +export function impressionOptionsForMetadataRibbonItem(baseOptions, id, name, idType) { + return validation.context("impressionOptionsForMetadataRibbonItem", () => { + const metricsOptions = { + ...baseOptions, + id: id, + kind: null, + softwareType: null, + title: name, + idType: idType, + targetType: "tag", + }; + return metricsOptions; + }); +} +// region Hints Impressions +export function impressionOptionsForSearchHint(objectGraph, hintTerm, baseOptions, searchEntity, hintSource) { + return validation.context("impressionOptionsForSearchHint", () => { + const metricsOptions = { + ...baseOptions, + id: "", + kind: null, + softwareType: null, + title: hintTerm, + hintsEntity: searchEntity, + modelSource: hintSource, + }; + return metricsOptions; + }); +} +export function addImpressionMetricsToHintsSearchAction(objectGraph, searchAction, metricsOptions) { + const options = impressionOptionsForSearchHint(objectGraph, searchAction.term, metricsOptions, searchAction.entity, searchAction.source); + const impressionFields = generateImpressionFields(objectGraph, options); + searchAction.impressionMetrics = new models.ImpressionMetrics(impressionFields); +} +const uniqueIdDelimiter = "::"; +function createUniqueImpressionId(objectGraph, baseId) { + return `${baseId}${uniqueIdDelimiter}${objectGraph.random.nextUUID()}`; +} +/** + * Strips the unique impression ID from the event fields if necessary. + * + * @param eventFields - The event fields containing impressions. + */ +export function stripUniqueImpressionIdsIfNecessary(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + for (const impression of impressions) { + const impressionId = serverData.asString(impression, "id"); + if (isNothing(impressionId)) { + continue; + } + impression["id"] = stripUniqueImpressionIdIfNecessary(impressionId); + } +} +/** + * Strips the unique impression ID from the given impression ID if necessary. + * If the impression ID contains a unique ID delimiter, it splits the string + * and returns the part before the delimiter. Otherwise, it returns the original + * impression ID. + * + * @param impressionId - The impression ID to process. + * @returns The processed impression ID. + */ +function stripUniqueImpressionIdIfNecessary(impressionId) { + if (impressionId.includes(uniqueIdDelimiter)) { + return impressionId.split(uniqueIdDelimiter)[0]; + } + return impressionId; +} +// endregion +//# sourceMappingURL=impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js new file mode 100644 index 0000000..4232a33 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js @@ -0,0 +1,18 @@ +/** + * Hack for injecting clientId and metrics data if needed. + */ +import { AppStoreMetricsData } from "../../../api/models"; +/** + * Opt out of legacy metrics id fields provider, the `AMSMetricsIdentifierFieldsProvider` instead + * we'll rely solely on `MetricsIdFieldsProvider` added for Katana + * @param objectGraph - The object graph. + * @param metricsData - The metrics data. + * @returns The metrics data, with the `amsMetricsID` field excluded. + */ +export function optOutOfLegacyMetricsIdFieldsProvider(objectGraph, metricsData) { + var _a; + const excludingFields = (_a = metricsData.excludingFields) !== null && _a !== void 0 ? _a : []; + excludingFields.push("amsMetricsID"); + return new AppStoreMetricsData(metricsData.fields, metricsData.includingFields, excludingFields, metricsData.topic, metricsData.shouldFlush); +} +//# sourceMappingURL=legacy-metrics-identifier-fields-opt-out.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js new file mode 100644 index 0000000..f7ffd4e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js @@ -0,0 +1,188 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { asJSONData } from "../../../foundation/json-parsing/server-data"; +import * as productVariant from "../../product-page/product-page-variants"; +import * as metricsUtil from "./util"; +class MetricsLocationStackItem { + constructor() { + this.position = 0; + } +} +export function newLocationTracker() { + return { + rootPosition: 0, + locationStack: [], + }; +} +/** + * @param locationTracker The location tracker to copy + * @returns A copy of the location tracker + */ +export function createLocationTrackerCopy(locationTracker) { + const locationStackCopy = []; + for (const locationStackEntry of locationTracker.locationStack) { + locationStackCopy.push({ + ...locationStackEntry, + }); + } + return { + rootPosition: locationTracker.rootPosition, + locationStack: locationStackCopy, + }; +} +export function createContentLocation(objectGraph, options, title) { + const locations = stackItemsToLocationStack(options.locationTracker); + const contentLocation = newContentLocation(objectGraph, options, title); + return [contentLocation, ...locations]; +} +export function createBasicLocation(objectGraph, options, title) { + const locations = stackItemsToLocationStack(options.locationTracker); + const basicLocation = newBasicLocation(objectGraph, options, title); + return [basicLocation, ...locations]; +} +export function pushContentLocation(objectGraph, options, title) { + const stackItem = new MetricsLocationStackItem(); + stackItem.location = newContentLocation(objectGraph, options, title); + options.locationTracker.locationStack.unshift(stackItem); +} +export function pushBasicLocation(objectGraph, options, title) { + const stackItem = new MetricsLocationStackItem(); + stackItem.location = newBasicLocation(objectGraph, options, title); + options.locationTracker.locationStack.unshift(stackItem); +} +export function popLocation(tracker) { + if (tracker.locationStack.length === 0) { + validation.unexpectedType("ignoredValue", "non-empty location stack", "empty location stack"); + return; + } + tracker.locationStack.shift(); +} +export function currentPosition(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + return stackItem.position; + } + else { + return tracker.rootPosition; + } +} +export function previousPosition(tracker) { + if (tracker.locationStack.length < 2) { + return null; + } + return tracker.locationStack[1].position; +} +export function currentLocation(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + return stackItem.location; + } + else { + return null; + } +} +/** + * Set current position of tracker. This is necessary when large today modules are broken apart into multipart shelves. + * We need to preserve the position of content within server-response, not our logical shelves. + * @param tracker Tracker update position of. + * @param position Position to set to. + */ +export function setCurrentPosition(tracker, position) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + stackItem.position = position; + } + else { + tracker.rootPosition = position; + } +} +export function nextPosition(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + stackItem.position++; + } + else { + tracker.rootPosition++; + } +} +function newContentLocation(objectGraph, options, title) { + var _a; + const base = newBasicLocation(objectGraph, options, title); + // Use the location tracker if there is no id override + if (!options.id && options.locationTracker) { + base.idType = "sequential"; + base.id = currentPosition(options.locationTracker).toString(); + } + else { + // If there is a id specified, use that + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + base.idType = idType; + } + let id = options.id; + if ((_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) { + id = options.anonymizationOptions.anonymizationString; + } + base.id = isNothing(id) ? "" : id; + } + if (options.fcKind) { + base.fcKind = options.fcKind; + } + if (options.displayStyle) { + base.displayStyle = options.displayStyle; + } + if (options.inAppEventId) { + base.inAppEventId = options.inAppEventId; + } + if (options.relatedSubjectIds) { + base.relatedSubjectIds = options.relatedSubjectIds; + } + if (options.canonicalId) { + base.canonicalId = options.canonicalId; + } + if (options.optimizationEntityId) { + base.optimizationEntityId = options.optimizationEntityId; + } + if (options.optimizationId) { + base.optimizationId = options.optimizationId; + } + if (isSome(options.rowIndex)) { + base.rowIndex = options.rowIndex; + } + if (options.productVariantData) { + Object.assign(base, productVariant.contentFieldsForProductVariantData(options.productVariantData)); + } + return base; +} +function newBasicLocation(objectGraph, options, title) { + var _a, _b; + let name = title; + if ((_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) { + name = options.anonymizationOptions.anonymizationString; + } + const location = { + locationPosition: currentPosition(options.locationTracker), + locationType: metricsUtil.targetTypeForMetricsOptions(objectGraph, options), + name: isNothing(name) ? "" : name, + }; + if (isSome(options.badges)) { + location.badges = (_b = asJSONData(options.badges)) !== null && _b !== void 0 ? _b : undefined; + } + if (options.recoMetricsData) { + Object.assign(location, options.recoMetricsData); + } + return location; +} +function stackItemsToLocationStack(tracker) { + return tracker.locationStack.map((stackItem) => { + return stackItem.location; + }); +} +function lastStackItemAdded(tracker) { + const length = tracker.locationStack.length; + if (length === 0) { + return null; + } + return tracker.locationStack[0]; +} +//# sourceMappingURL=location.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js new file mode 100644 index 0000000..0b12cf9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js @@ -0,0 +1,34 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as objects from "../../../foundation/util/objects"; +import * as metricsBuilder from "../builder"; +import * as metricsLocation from "./location"; +import * as misc from "./misc"; +import * as metricsUtil from "./util"; +//* ************************* +//* Media Metrics +//* ************************* +export function addMetricsEventsToVideo(objectGraph, video, options) { + if (isNothing(video)) { + return; + } + const mediaEventFields = misc.fieldsFromPageInformation(options.pageInformation); + if (mediaEventFields === null) { + return; + } + mediaEventFields["id"] = metricsUtil.emptyStringIfNullOrUndefined(options.id); + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + mediaEventFields["idType"] = idType; + } + mediaEventFields["type"] = "video"; + mediaEventFields["typeDetails"] = "iTunesStoreContent"; + mediaEventFields["location"] = metricsLocation.createContentLocation(objectGraph, options, ""); + if (options.actionDetails) { + mediaEventFields["actionDetails"] = options.actionDetails; + } + video.templateMediaEvent = metricsBuilder.createMetricsMediaData(objectGraph, mediaEventFields); + const clickEventFields = objects.shallowCopyOf(mediaEventFields); + clickEventFields["actionUrl"] = video.videoUrl; + video.templateClickEvent = metricsBuilder.createMetricsMediaClickData(objectGraph, null, "button", clickEventFields); +} +//# sourceMappingURL=media.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js new file mode 100644 index 0000000..ffb0b77 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js @@ -0,0 +1,46 @@ +import { isDefinedNonNull } from "../../../foundation/json-parsing/server-data"; +import { pageFieldsForPageInfoProductVariantData } from "../../product-page/product-page-variants"; +import { EventLinter } from "../event-linter"; +import * as metricsUtil from "./util"; +// region Page Information +/** + * Returns a set of pageFields from a given `pageInformation` - i.e. fields that are included on metrics data with `pageField` IncludingFields, and buyParams. + * It is not recommended to use this function for instrumentation that involves field providers, as these page fields will overwrite them. + * @param pageInformation Page information to create page fields for. + */ +export function fieldsFromPageInformation(pageInformation) { + var _a; + const fields = {}; + if (!pageInformation) { + return fields; + } + Object.assign(fields, pageInformation.baseFields); + if (pageInformation.pageUrl) { + fields["pageUrl"] = pageInformation.pageUrl; + } + else if (pageInformation.timingMetrics && pageInformation.timingMetrics.pageURL) { + fields["pageUrl"] = pageInformation.timingMetrics.pageURL; + } + if (pageInformation.recoMetricsData) { + Object.assign(fields, pageInformation.recoMetricsData); + } + if (pageInformation.mercuryMetricsData) { + Object.assign(fields, pageInformation.mercuryMetricsData); + } + if (pageInformation.productVariantData) { + Object.assign(fields, pageFieldsForPageInfoProductVariantData(pageInformation.productVariantData)); + } + if (pageInformation.iAdInfo && isDefinedNonNull(pageInformation.iAdInfo.pageFields[EventLinter.hasIAdData])) { + fields[EventLinter.hasIAdData] = pageInformation.iAdInfo.pageFields[EventLinter.hasIAdData]; + } + const iAdId = (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.pageFields["iAdId"]; + if (isDefinedNonNull(iAdId)) { + fields["iAdId"] = iAdId; + } + return metricsUtil.sanitizedMetricsDictionary(fields); +} +// endregion +// region Network Performance +// @see `JSNetworkPerformanceMetrics.metrics(fromResult:)` in native. +// endregion +//# sourceMappingURL=misc.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js new file mode 100644 index 0000000..3456e9e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js @@ -0,0 +1,671 @@ +import * as metricsLocation from "./location"; +import * as metricsUtil from "./util"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../../foundation/media/attributes"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import { productVariantDataForData, productVariantDataHasVariant } from "../../product-page/product-page-variants"; +import { EventLinter } from "../event-linter"; +import { eligibleSlotPositionsForAdPlacement } from "../../ads/ad-common"; +import { FlattenedTodayItemType } from "../../today/today-parse-util"; +import { getSelectedCustomCreativeId } from "../../search/custom-creative"; +export const iAdURLParameterStringToken = "X-AppStore-iAdClickToken"; +export const iAdURLLineItemParameterStringToken = "X-AppStore-iAdLineItem"; +export const iAdDismissAdActionMetricsParameterStringToken = "X-AppStore-iAdDismissAdActionMetrics"; +// User defined type guard for determining if an object conforms to ContentMetricsOptions interface. +export function isContentMetricsOptions(object) { + return object && Object.prototype.hasOwnProperty.call(object, "id"); +} +export class IAdSearchInformation { + /** + * Initialise a new `IAdSearchInformation` + * @param objectGraph The Object Graph. + * @param placementType The placement type for the ad this object is tracking. + * @param baseSlotInformation The initial list of slotInfos for this ad placement + * @param iAdId The unique id for the ad instance. + * @param appStoreClientRequestId The unique id for the client requesting the ad. + * @param wasOdmlSuccessful Whether native ODML processing was successful. + * @param positionInfo The position info data describing the requested position of this ad. + */ + constructor(objectGraph, placementType, baseSlotInformation, iAdId, appStoreClientRequestId, wasOdmlSuccessful, positionInfo) { + this.placementType = placementType; + this.placementId = placementType === null ? null : this.placementIdFromType(placementType); + this.pageFields = {}; + this.clickFields = {}; + this.impressionsFields = {}; + this.fastImpressionFields = {}; + this.iAdClickEventFields = {}; + this._iAdApplied = false; + this._iAdAdamId = undefined; + this.positionInfo = positionInfo; + this.slotInfo = baseSlotInformation; + this.setInitialAdData(objectGraph, iAdId, appStoreClientRequestId); + if (serverData.isDefinedNonNull(wasOdmlSuccessful)) { + this.pageFields["iAdOdmlSuccess"] = wasOdmlSuccessful; + } + this.fastImpressionFields["iAdEligible"] = true; + } + /** + * Construct an IAdSearchInformation from the given JSON representation. + * This is necessary over and above standard JSON parsing to preserve our ability to call functions on this object. + * @param objectGraph The Object Graph. + * @param json The JSON representation of the object. + * @returns A constructed IAdSearchInformation from the JSON. + */ + static from(objectGraph, json) { + var _a, _b, _c, _d; + const iAdInfo = new IAdSearchInformation(objectGraph, serverData.asString(json.placementType), serverData.asArrayOrEmpty(json.slotInfo), (_a = serverData.asString(json.iAdId)) !== null && _a !== void 0 ? _a : undefined, (_b = serverData.asString(json.appStoreClientRequestId)) !== null && _b !== void 0 ? _b : undefined, (_c = serverData.asBoolean(json.wasOdmlSuccessful)) !== null && _c !== void 0 ? _c : undefined, serverData.asInterface(json.positionInfo)); + iAdInfo._iAdApplied = serverData.asBooleanOrFalse(json._iAdApplied); + iAdInfo._iAdAdamId = (_d = serverData.asString(json._iAdAdamId)) !== null && _d !== void 0 ? _d : undefined; + Object.assign(iAdInfo.pageFields, json.pageFields); + Object.assign(iAdInfo.clickFields, json.clickFields); + Object.assign(iAdInfo.impressionsFields, json.impressionsFields); + Object.assign(iAdInfo.fastImpressionFields, json.fastImpressionFields); + Object.assign(iAdInfo.iAdClickEventFields, json.iAdClickEventFields); + iAdInfo.updateContainerId(serverData.asString(json.containerId)); + return iAdInfo; + } + /** + * Create an array of `IAdSlotInformation` objects based on the current ad information available. + * This isn't ideal, but we need to understand the available ad slots so we can report + * on all slots, whether they have ads in them or not. + */ + static createInitialSlotInfos(objectGraph, placementType, positionInfo, flattenedTodayFeed) { + var _a; + switch (placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + const productPageContainerId = IAdSearchInformation.containerIdFromType(placementType); + const slotIndex = (_a = positionInfo === null || positionInfo === void 0 ? void 0 : positionInfo.slot) !== null && _a !== void 0 ? _a : 0; + const productPageSlot = { + slotId: `${productPageContainerId}_${slotIndex}`, + slotIndex: slotIndex, + hasAdData: false, + }; + return [ + { + containerId: productPageContainerId, + slots: [productPageSlot], + }, + ]; + break; + case "today": + const placementEligibleSlotPositions = eligibleSlotPositionsForAdPlacement(objectGraph, placementType); + const eligibleAdSlots = isNothing(placementEligibleSlotPositions) + ? [] + : placementEligibleSlotPositions.map((slotPosition) => slotPosition.slot); + const containerSlotInfoMapping = {}; + /// iAd provides slots that are not zero based, adjust the slot + const adjustedAdSlot = isSome(positionInfo) ? positionInfo.slot - 1 : null; + let adPositionEncountered = false; + eligibleAdSlots.forEach((eligibleIndex) => { + var _a; + // If we've reached the ad we need to subtract 1 from the index, to act as if the ad ad was + // actually part of the feed. + const augmentedFeedIndexIndex = adPositionEncountered ? eligibleIndex - 1 : eligibleIndex; + const todayItem = flattenedTodayFeed === null || flattenedTodayFeed === void 0 ? void 0 : flattenedTodayFeed.find((item) => item.containedAdSlots.includes(augmentedFeedIndexIndex)); + const isAdSlotIndex = adjustedAdSlot === eligibleIndex; + const todayContainerId = iAdContainerIdForSlotInTodayItem(augmentedFeedIndexIndex, isAdSlotIndex, todayItem); + const containerSlotInformation = (_a = containerSlotInfoMapping[todayContainerId]) !== null && _a !== void 0 ? _a : { + containerId: todayContainerId, + slots: [], + }; + containerSlotInfoMapping[todayContainerId] = containerSlotInformation; + const todaySlot = { + slotId: `${todayContainerId}_${eligibleIndex}`, + slotIndex: eligibleIndex, + hasAdData: false, + }; + containerSlotInformation.slots.push(todaySlot); + adPositionEncountered = adPositionEncountered || isAdSlotIndex; + }); + return Object.values(containerSlotInfoMapping); + default: + return null; + } + } + get iAdIsPresent() { + return this._iAdApplied; + } + get iAdAdamId() { + return this._iAdAdamId; + } + /** + * Update our information with a new ad response. + * This is primarily used for asynchronous ad requests, where we need to update the metrics info subsequent to the initial page load. + * @param objectGraph The Object Graph. + * @param adResponse An ad response to use to update some fields. + */ + updateForAdResponse(objectGraph, adResponse) { + var _a; + if (serverData.isNull(adResponse)) { + return; + } + this.placementType = adResponse.placementType; + this.placementId = this.placementIdFromType(this.placementType); + this.positionInfo = (_a = adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo; + this.setInitialAdData(objectGraph, adResponse.iAdId, adResponse.clientRequestId); + } + /** + * Set the initial ad data, if available. + * This function requires that an ad fetch has been made, and can be called from two paths: + * 1. Via the constructor, where the ad fetch is made at page load time, or + * 2. Via `updateForAdResponse`, where the ad fetch was made asynchronously after the page load. + * @param objectGraph The Object Graph. + * @param iAdId The unique id provided for the ad instance. + * @param appStoreClientRequestId The unique id representing the App Store ad request client. + */ + setInitialAdData(objectGraph, iAdId, appStoreClientRequestId) { + if (isNothing(appStoreClientRequestId)) { + return; + } + // ToroID Suppression expects this to be replaced with -1 for figaro events + const iAdIdValue = isNothing(iAdId) ? "-1" : iAdId; + this.pageFields[EventLinter.hasIAdData] = true; + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + // Only set `hasIAdData` on impressions fields initially if it's a Chainlink placement. + // This covers the "ad eligible" cases where we still want to check ad metrics when ads are not present. + this.impressionsFields[EventLinter.hasIAdData] = true; + break; + default: + break; + } + this.pageFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + this.clickFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + this.impressionsFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + break; + default: + break; + } + this.pageFields["iAdId"] = iAdIdValue; + this.impressionsFields["iAdId"] = iAdIdValue; + this.clickFields["iAdId"] = iAdIdValue; + this.updateContainerId(null); + // Update slot info with data + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + if (this.placementId !== null && this.placementId.length > 0) { + this.pageFields["iAdPlacementId"] = this.placementId; + this.clickFields["iAdPlacementId"] = this.placementId; + // Attach the `iAdPlacementId` to top-level impressions for v4 impressions. + this.impressionsFields["iAdPlacementId"] = this.placementId; + // For Chainlink placements, the `iAdPlacementId` sits within the impressions array. + // We manually remove `iAdPlacementId` from the top-level fields for v5 impressions in `createMetricsFastImpressionsData`. + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + this.fastImpressionFields["iAdPlacementId"] = this.placementId; + break; + default: + break; + } + } + } + /** + * Update the containerId based on the current placementType. + */ + updateContainerId(containerId) { + if (this.placementType === "today") { + this.containerId = containerId !== null && containerId !== void 0 ? containerId : null; + if (serverData.isDefinedNonNull(this.containerId)) { + this.clickFields["iAdContainerId"] = this.containerId; + this.fastImpressionFields["iAdContainerId"] = this.containerId; + } + } + else { + this.containerId = + this.placementType === null ? null : IAdSearchInformation.containerIdFromType(this.placementType); + if (serverData.isDefinedNonNull(this.containerId)) { + this.pageFields["iAdContainerId"] = this.containerId; + this.clickFields["iAdContainerId"] = this.containerId; + this.fastImpressionFields["iAdContainerId"] = this.containerId; + } + } + } + /** + * @param slotIndex The slot index to look for a container Id for + * @returns The container Id for the given slot index, based off the slot infos + */ + containerIdForSlotIndex(slotIndex) { + if (isNothing(slotIndex) || isNothing(this.slotInfo)) { + return null; + } + for (const slotInfo of this.slotInfo) { + for (const slot of slotInfo.slots) { + if (slot.slotIndex === slotIndex) { + return slotInfo.containerId; + } + } + } + return this.containerId; + } + apply(objectGraph, adData) { + if (isNothing(adData) || serverData.isNullOrEmpty(adData)) { + return; + } + const iAdAdamId = adData.id; + const iAdConfigurationDictionary = attributeAsDictionary(adData, "iad"); + this._iAdAdamId = iAdAdamId; + if (iAdConfigurationDictionary) { + this.impressionsFields[EventLinter.hasIAdData] = true; + this.clickFields[EventLinter.hasIAdData] = true; + const impressionId = metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["impressionId"]); + this.fastImpressionFields["iAdImpressionId"] = impressionId; + this.clickFields["iAdImpressionId"] = impressionId; + const metadata = metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["metadata"]); + this.clickFields["iAdMetadata"] = metadata; + this.fastImpressionFields["iAdMetadata"] = metadata; + // Ads boldly populate `adamId` on pages. This is correct. + this.pageFields["adamId"] = iAdAdamId; + this.pageFields["iAd"] = { + iAdFormat: metricsUtil.sanitizedMetricsDictionary(serverData.asInterface(serverData.asJSONValue(iAdConfigurationDictionary), "format")), + iAdAlgoId: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["algoId"]), + iAdImpressionId: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["impressionId"]), + iAdMetadata: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["metadata"]), + }; + const productVariantData = productVariantDataForData(objectGraph, adData); + this.updateIAdMetricsFieldsForProductVariantData(productVariantData, this.clickFields); + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(adData); + if (this.placementType === "today") { + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.fastImpressionFields); + } + else { + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.impressionsFields); + } + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.clickFields); + } + } + Object.assign(this.iAdClickEventFields, iAdConfigurationDictionary); + this._iAdApplied = true; + // Clear out any prior missed opportunity reason if we have an ad. + this.setMissedOpportunity(objectGraph, undefined, this.placementType); + } + // Update slot info after ad is applied. + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + } + /** + * Apply the click fields that were attached to the page request we're following. + * This will generally be following on from click on a lockup with ad data attached, where we want to pull + * some of the click fields from ad to be applied to this new page data. + * + * The goal here is to attach enough metadata into the incoming page that we can attribute any offer action + * to the ad that was tapped on to reach this page. Whilst doing this we don't want to attach too much + * ad data to the page, as this can start to interfere with metrics for other ad placements. This results in + * us manually inserting and removing fields to achieve the right balance. + * @param iAdAdamId The adamId for the ad. + * @param fields The set of fields to apply to our clickFields. + */ + applyClickFieldsFromPageRequest(iAdAdamId, fields) { + this._iAdApplied = true; + this._iAdAdamId = iAdAdamId; + Object.assign(this.clickFields, fields); + // We don't want to assign any of the fields to the page event, as this looks like an ad is being shown + // on the page we're navigating to. + Object.keys(this.pageFields).forEach((field) => delete this.pageFields[field]); + } + setSpecifiedAlignedRegionUsed(didUseSpecifiedCreative) { + this.fastImpressionFields["iAdIsSpecifiedCreativeUsed"] = didUseSpecifiedCreative; + this.clickFields["iAdIsSpecifiedCreativeUsed"] = didUseSpecifiedCreative; + } + /** + * Set the template type for the page. + */ + setTemplateType(templateType) { + this.pageFields["iAdTemplateType"] = templateType; + this.impressionsFields["iAdTemplateType"] = templateType; + this.clickFields["iAdTemplateType"] = templateType; + } + setMissedOpportunity(objectGraph, reason, placementType) { + this.missedOpportunityReason = reason; + // Only set in page and impressions fields if reason is non-null. + if (serverData.isDefinedNonNull(reason)) { + this.clickFields["iAdMissedOpportunityReason"] = reason; + // Chainlink placements don't expect "iAdMissedOpportunityReason" at the top level of impressions or page events. + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + break; + default: + this.pageFields["iAdMissedOpportunityReason"] = reason; + this.impressionsFields["iAdMissedOpportunityReason"] = reason; + break; + } + } + else { + delete this.clickFields["iAdMissedOpportunityReason"]; + // Generally, we just want to remove the missed opportunity reason if one isn't set. + // The 'productPageYMALDuringDownload' placement is a special case where it's placed on the page + // subsequent to a previous placement, and metrics is updated via the pageChange metrics. + // This means that we need to send `null` as the value for this placement, in order to clear + // any possible missed opportunity value from the placement it's replacing. + switch (placementType) { + case "productPageYMALDuringDownload": + this.pageFields["iAdMissedOpportunityReason"] = null; + this.impressionsFields["iAdMissedOpportunityReason"] = null; + break; + default: + delete this.pageFields["iAdMissedOpportunityReason"]; + delete this.impressionsFields["iAdMissedOpportunityReason"]; + break; + } + } + // Only set in page and impressions fields if reason is non-null. + if (serverData.isDefinedNonNull(reason)) { + this.pageFields["iAdMissedOpportunityReason"] = reason; + this.impressionsFields["iAdMissedOpportunityReason"] = reason; + } + else { + delete this.pageFields["iAdMissedOpportunityReason"]; + delete this.impressionsFields["iAdMissedOpportunityReason"]; + } + // Update slotInfo with missed opportunity reason. + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + } + placementIdFromType(type) { + switch (type) { + case "searchLanding": + return "APPSTORE_SEARCH_LANDING_PAGE"; + case "searchResults": + return "APPSTORE_SEARCH_RESULT_PAGE"; + case "today": + return "APPSTORE_TODAY_TAB"; + case "productPageYMAL": + return "APPSTORE_PRODUCT_PAGE"; + case "productPageYMALDuringDownload": + return "APPSTORE_PRODUCT_PAGE_DOWNLOAD"; + default: + throw new Error(`This method should never be called with value: ${type}`); + } + } + static placementTypeFromPlacementId(objectGraph, id) { + switch (id) { + case "APPSTORE_SEARCH_LANDING_PAGE": + return "searchLanding"; + case "APPSTORE_SEARCH_RESULT_PAGE": + return "searchResults"; + case "APPSTORE_TODAY_TAB": + return "today"; + case "APPSTORE_PRODUCT_PAGE": + return "productPageYMAL"; + case "APPSTORE_PRODUCT_PAGE_DOWNLOAD": + return "productPageYMALDuringDownload"; + default: + objectGraph.console.log(`Failed to get placementType from placementId ${id}. Defaulting to searchResults`); + // For legacy reasons we fall back to `seachResults` when nothing is provided. + // Being the first ad slot, in the past this has been assumed as the "default". + return "searchResults"; + } + } + /** + * Get a containerId value for a given placement type to attach to the ad's metrics. + * @param type The AdPlacementType for the current ad. + * @returns A string value for containerId, if the placement has one. Otherwise null. + */ + static containerIdFromType(type) { + switch (type) { + case "productPageYMAL": + return "customers-also-bought-apps"; + case "productPageYMALDuringDownload": + return "customers-also-bought-apps-download"; + case "today": + return null; // Today page has variable containerIds that are updated programatically + default: + return null; + } + } + /** + * Return the fast impressions metrics fields for an item using the location tracker. + * + * `fastImpressionFields` are the items inside the impressions array of metrics events. Although some fields are stable, + * these are unique to a given position, so we must use that position information to generate the right fields. + * + * @param locationTracker A LocationTracker used to identify the relevant information to attach to the metrics fields. + * @param adSlotOverride The ad slot to use for the metrics fields. If not provided, the ad slot will be inferred from + * the location tracker. This is needed for today where the ad can span different shelves so the position is going to be 0 for multiple ads + * @returns Relevant metrics fields for the item in an impressions array. + */ + fastImpressionsFieldsForCurrentItem(locationTracker, adSlotOverride) { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + let position; + if (isSome(adSlotOverride)) { + position = adSlotOverride; + } + else { + const location = metricsLocation.currentLocation(locationTracker); + // If the current location is within a todayCard, we want to extract the location of the card, not the location + // of this lockup within the card. + if (location !== null && location.locationType === "todayCard") { + position = metricsLocation.previousPosition(locationTracker); + } + else { + position = metricsLocation.currentPosition(locationTracker); + } + } + const sharedFields = shallowCopyOf(this.fastImpressionFields); + sharedFields["iAdSlotId"] = `${this.containerIdForSlotIndex(position)}_${position}`; + if (position !== this.adjustedSlotIndex) { + // If the current position is not where the ad is, we only want a subset of the tracked fields. + const allowedFields = ["iAdEligible", "iAdPlacementId", "iAdContainerId", "iAdSlotId"]; + Object.keys(sharedFields).forEach((key) => { + if (!allowedFields.includes(key)) { + delete sharedFields[key]; + } + }); + } + return sharedFields; + default: + return this.fastImpressionFields; + } + } + /** + * Get the adjusted slot index of the ad we're tracking. + * This is zero-based - Ad Platforms returns slot info to us one-based but we always adjust it before using it anywhere. + */ + get adjustedSlotIndex() { + var _a; + const positionInfoSlotIndex = (_a = this.positionInfo) === null || _a === void 0 ? void 0 : _a.slot; + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + if (serverData.isDefinedNonNull(positionInfoSlotIndex)) { + return positionInfoSlotIndex - 1; + } + return null; + } + /** + * Update the existing slot information now that new data has been applied. + */ + updateSlotInfo() { + if (isNothing(this.slotInfo)) { + return; + } + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + for (const containerSlotInfo of this.slotInfo) { + for (const slot of containerSlotInfo.slots) { + slot.hasAdData = this.iAdIsPresent; + if (serverData.isDefinedNonNull(this.missedOpportunityReason)) { + slot.missedOpportunityReason = this.missedOpportunityReason; + } + } + } + break; + case "today": + const adjustedSlotIndex = this.adjustedSlotIndex; + for (const containerSlotInfo of this.slotInfo) { + for (const slot of containerSlotInfo.slots) { + // Check whether the current index matches our slot information. + // If so, an ad is intended to be placed here. + const isAdInCurrentIndex = adjustedSlotIndex === slot.slotIndex; + // In order to understand whether a given slot has ad data, we ensure that a usable ad is present, + // and the current slot is the one an ad is placed in. + const hasAdData = this.iAdIsPresent && isAdInCurrentIndex; + // By default, use the missed opportunity reason, if any, stored here. + let missedOpportunityReason = this.missedOpportunityReason; + // If: + // - the ad is not in the current index we're building slot info for, and + // - there is a valid slot index + // we override the reason to 'NOAD', as it means another eligible slot was filled. + if (!isAdInCurrentIndex && serverData.isDefinedNonNull(adjustedSlotIndex)) { + missedOpportunityReason = "NOAD"; + } + slot.hasAdData = hasAdData; + if (serverData.isDefinedNonNull(missedOpportunityReason)) { + slot.missedOpportunityReason = missedOpportunityReason; + } + } + } + break; + default: + break; + } + } + /** + * Modifies the provided MetricsFields for the ProductVariantData specific to fields required for iAds. + * @param productVariantData The variant data of impressionable data to apply. + * @param metricsFields An existing set of MetricsFields to modify. + */ + updateIAdMetricsFieldsForProductVariantData(productVariantData, metricsFields) { + if (isSome(productVariantData) && productVariantDataHasVariant(productVariantData, "customProductPage")) { + metricsFields["iAdPageCustomId"] = productVariantData.productPageId; + } + else { + // If a custom product page doesn't exist, we remove the fields. + delete metricsFields["iAdPageCustomId"]; + } + } + /** + * Modifies the provided MetricsFields to include the custom id for iAds. + * @param selectedCustomCreativeId The id for the custom creative that we want to set as custom id. + * @param metricsFields An existing set of MetricsFields to modify. + */ + updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, metricsFields) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (isSome(selectedCustomCreativeId)) { + metricsFields["iAdCustomId"] = selectedCustomCreativeId; + } + else { + delete metricsFields["iAdCustomId"]; + } + } + } + /** + * Return the event version for fast impressions based on the placement of the ad. + * Impressions v5 is being rolled out first for Chainlink. Once all impressions have adopted it, this can be removed. + */ + get fastImpressionsEventVersion() { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + return 5; + default: + return 4; + } + } + /** + * Return whether ad rotation fields should be included on metrics events, based on the current placement type. + */ + get shouldIncludeAdRotationFields() { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + return false; + case "searchLanding": + case "searchResults": + return true; + default: + return true; + } + } +} +/** + * Return the container id to use for a today feed item + * @param slotIndex The slot index that we found a TodayItem for + * @param isAdSlot Whether this slotIndex is the slot the ad is in + * @param todayItem The today item that this card is contained in, this could be a single item or a story group item + * @returns The configuration to use when parsing a today card + */ +export function iAdContainerIdForSlotInTodayItem(slotIndex, isAdSlot, todayItem) { + if (isNothing(todayItem)) { + return "0"; + } + switch (todayItem.type) { + case FlattenedTodayItemType.EditorialItemGroup: + const storyGroupHasMultipleItems = todayItem.containedAdSlots.length > 1; + const slotIndexIsFirstOrLastInStoryGroup = !storyGroupHasMultipleItems || + slotIndex === todayItem.containedAdSlots[0] || + slotIndex === todayItem.containedAdSlots[todayItem.containedAdSlots.length - 1]; + if (isAdSlot && slotIndexIsFirstOrLastInStoryGroup) { + // If the story group has fewer than 2 items, that means theres not valid slot **in** the actual story group + // to place the ad, so its going to be at the top level. Or if there are multiple items, but the slot is the + // first or last item there is no reason to pull the ad into the story group, it should be top level. + return "0"; + } + else { + // Using the `data` from the todayItem here and not the todayCardData, because + // this will be the data for the containing story gorup + return "0"; + } + default: + return "0"; + } +} +/** + * Create the iAdInformation for a given page if necessary + * @param objectGraph The dependency graph for the app store + * @param pageId The id of the page this iAd info is being used for + * @param response The MAPI response for this page + * @param positionInfo The position information on where the ad will attempted to be inserted + * @param flattenedTodayFeed The flattened to today feed if this is for a today page + * @returns The iAdInformation to be used in a metrics page information object + */ +export function iAdInformationFromMediaApiResponse(objectGraph, pageId, response, positionInfo = null, flattenedTodayFeed = null) { + var _a; + /** <rdar://problem/33764430> Toro: iAd is missing AD_OPEN figaro event when tapping on ad and pressing OPEN through the product page */ + const iAdClickInfoString = serverData.asString(response, iAdURLParameterStringToken); + if (isNothing(iAdClickInfoString)) { + return null; + } + const iAdClickInfo = JSON.parse(iAdClickInfoString); + // rdar://72607206 (Gibraltar: Tech Debt: Population of iAd fields in Product Page) + const placementType = IAdSearchInformation.placementTypeFromPlacementId(objectGraph, serverData.asString(iAdClickInfo, "iAdPlacementId")); + const iAdInfo = new IAdSearchInformation(objectGraph, placementType, IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, positionInfo, flattenedTodayFeed), (_a = serverData.asString(iAdClickInfo, "iAdId")) !== null && _a !== void 0 ? _a : undefined, undefined, undefined, positionInfo); + iAdInfo.applyClickFieldsFromPageRequest(pageId !== null && pageId !== void 0 ? pageId : undefined, iAdClickInfo); + return iAdInfo; +} +/** @public */ +export class MetricsPageInformation { + constructor(base = {}) { + this.baseFields = base; + } +} +//# sourceMappingURL=models.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js new file mode 100644 index 0000000..0c088b1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js @@ -0,0 +1,482 @@ +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 + // <rdar://problem/45420867> 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 <rdar://problem/68879825> + 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
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js new file mode 100644 index 0000000..0bd030a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js @@ -0,0 +1,57 @@ +import * as serverData from "../../../foundation/json-parsing/server-data"; +/** + * This is a workaround for Search Focus Page based on existing metrics tech debt in Search Results Page. + * + * # Context + * - In CrystalB, impressions for SFP now includes a recent searches shelf which needs to update dynamically as searches are performed. + * + * # What is the Workaround? + * The JetEngine Metrics API is designed to work s.t.: + * 1. JS populates `parentId`, which JetEngine Metric APIs uses **internally** to refer to between `ImpressionMetrics`. + * 2. Native generates `impressionParentId`, which is refers `impressionId` of parent assigned during serialization - + * + * Here, we instead: + * 1. JS populates `parentId` as normal + * 2. `event-linter` iterates through the impressions, finds the parent containers, and adds parent impression ids. + * 3. Native generates `impressionParentId` as normal where possible. + * + * # Why Workaround? + * 1. There are existing hacks for impression parents, e.g. fake `ad_container` and native child trackers w/ parent ID attribution. + * 2. AppStore is stuck between old ASK metrics and new JE metrics APIs. Our JS builders and existing native metrics are intertwined in a way that makes using JS defined `parentId` attribution nontrivial. + * 3. We need to dynamically update the recent searches shelf every time a search is performed. + * + * See `search-results-impressions` for more background. + */ +/** + * Update the `impression` field, attributing impressionParentIds per workaround above. + * @param eventFields Event fields to modify **in place**. + */ +export function decorateImpressionParentId(eventFields) { + var _a; + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + // Find result parent id. + let recentsParentImpressionId; + for (const impression of impressions) { + const canonicalId = serverData.asString(impression, "canonicalId"); + if (canonicalId === "R8804") { + recentsParentImpressionId = (_a = serverData.asString(impression, "impressionId")) !== null && _a !== void 0 ? _a : undefined; // *NOT* id. + break; + } + } + if (!recentsParentImpressionId) { + return; + } + // Update impressions for search results + eventFields["impressions"] = impressions.map((impression) => { + const canonicalId = serverData.asString(impression, "canonicalId"); + const impressionType = serverData.asString(impression, "impressionType"); + if (serverData.isNullOrEmpty(canonicalId) && + impressionType === "link" && + impression != null && + serverData.asString(impression, "impressionParentId") == null) { + impression["impressionParentId"] = recentsParentImpressionId; + } + return impression; + }); +} +//# sourceMappingURL=search-focus-impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js new file mode 100644 index 0000000..6874724 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js @@ -0,0 +1,56 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +/** + * This is a workaround for existing metrics tech debt in Search Results Page. + * + * # Context + * - In AzulC, impressions for SRP now includes 'containers' for search results and guided search tokens. + * - The containers were added so tokens don't affect impression index, which search uses as a signal for ranking. + * + * # What is the Workaround? + * The JetEngine Metrics API is designed to work s.t.: + * 1. JS populates `parentId`, which JetEngine Metric APIs uses **internally** to refer to between `ImpressionMetrics`. + * 2. Native generates `impressionParentId`, which is refers `impressionId` of parent assigned during serialization - + * + * Here, we instead: + * 1. JS *doesn't* populate `parentId` + * 2. `event-linter` iterates through the impressions, finds the parent containers, and adds parent impression ids. + * + * # Why Workaround? + * 1. There are existing hacks for impression parents, e.g. fake `ad_container` and native child trackers w/ parent ID attribution. + * 2. AppStore is stuck between old ASK metrics and new JE metrics APIs. Our JS builders and existing native metrics are intertwined in a way that makes using JS defined `parentId` attribution nontrivial. + * + * Note that we **don't** use this for attributing parent id workaround for `search-revisions`, i.e. guided search tokens. + * This is because that codepath began from a clean slate, and uses JE metrics as-designed + */ +/** + * Update the `impression` field, attributing impressionParentIds per workaround above. + * @param eventFields Event fields to modify **in place**. + */ +export function decorateImpressionParentId(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + // Find result parent id. + let resultsParentImpressionId; + for (const impression of impressions) { + const impressionType = serverData.asString(impression, "impressionType"); + if (isSome(impression) && impressionType === "SearchResults") { + resultsParentImpressionId = impression["impressionId"]; // *NOT* id. + break; + } + } + if (!resultsParentImpressionId) { + return; + } + // Update impressions for search results + eventFields["impressions"] = impressions.map((impression) => { + const impressionType = serverData.asString(impression, "impressionType"); + const isCardResult = impressionType === "card"; + const isEventModuleResult = impressionType === "eventModule"; + const isSearchResult = isCardResult || isEventModuleResult; // search results are cards and app events. + if (isSome(impression) && isSearchResult) { + impression["impressionParentId"] = resultsParentImpressionId; + } + return impression; + }); +} +//# sourceMappingURL=search-result-impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js new file mode 100644 index 0000000..486fc9d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js @@ -0,0 +1,98 @@ +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as models from "../../../../api/models"; +import * as searchShelves from "../../../search/content/search-shelves"; +import { impressionOptions } from "../impressions"; +import { asString } from "../../../../foundation/json-parsing/server-data"; +import { relationship } from "../../../../foundation/media/relationships"; +import { isNothing } from "@jet/environment/types/optional"; +/** + * Generates the metrics impressions options for the search shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page containing the shelf + * @returns The metrics options for the shelf + */ +export function createMetricsOptionsForGenericSearchPageShelf(objectGraph, data, shelfAttributes, searchPageContext) { + var _a, _b, _c; + /// On shelves, the actual reco metrics are on the content and not the data blob itself + const recoMetricsDataContainer = relationship(data, "contents"); + const recoMetricsData = recoMetricsDataContainer === null + ? undefined + : (_a = mediaDataStructure.metricsFromMediaApiObject(recoMetricsDataContainer)) !== null && _a !== void 0 ? _a : undefined; + let impressionsIdType = "its_contentId"; + if (searchPageContext.pageType === searchShelves.SearchPageType.ChartsAndCategories) { + impressionsIdType = "static"; + } + const shelfMetricsOptions = { + id: shelfAttributes.id, + kind: null, + softwareType: null, + targetType: "swoosh", + title: (_b = shelfAttributes.title) !== null && _b !== void 0 ? _b : "", + pageInformation: searchPageContext.metricsPageInformation, + locationTracker: searchPageContext.metricsLocationTracker, + idType: impressionsIdType, + fcKind: undefined, + canonicalId: (_c = asString(data.meta, "canonicalId")) !== null && _c !== void 0 ? _c : undefined, + recoMetricsData: recoMetricsData, + }; + return shelfMetricsOptions; +} +/** + * Generates the impressions metrics options for the search chart or category + * @param objectGraph The App Store Object Graph + * @param model The chart or category model + * @param modelData The chart or category model data object + * @param searchShelfContext The context for the shelf containing the chart or category + * @returns The metrics options for the chart or category model + */ +export function createMetricsOptionsForChartOrCategory(objectGraph, model, modelData, searchShelfContext) { + var _a; + const chartModelMetricsOptions = { + ...searchShelfContext.metricsOptions, + ...impressionOptions(objectGraph, modelData, model.title, searchShelfContext.metricsOptions), + recoMetricsData: (_a = mediaDataStructure.metricsFromMediaApiObject(modelData)) !== null && _a !== void 0 ? _a : undefined, + targetType: metricsTargetTypeForChartOrCategory(model.density), + idType: "its_id", + }; + return chartModelMetricsOptions; +} +/** + * Generates the click metrics options for the search chart or category + * @param objectGraph The App Store Object Graph + * @param model The chart or category model + * @param modelData The chart or category model data object + * @param searchShelfContext The context for the shelf containing the chart or category + * @returns The metrics options for the chart or category model + */ +export function createClickMetricsOptionsForChartOrCategory(objectGraph, modelDensity, modelData, searchShelfContext) { + var _a; + const chartModelMetricsOptions = { + pageInformation: searchShelfContext.metricsOptions.pageInformation, + locationTracker: searchShelfContext.metricsOptions.locationTracker, + recoMetricsData: (_a = mediaDataStructure.metricsFromMediaApiObject(modelData)) !== null && _a !== void 0 ? _a : undefined, + targetType: metricsTargetTypeForChartOrCategory(modelDensity), + id: modelData.id, + }; + return chartModelMetricsOptions; +} +/** + * Gets the matching metrics target type for the chart or category model + * @param model The model we want the target type for + * @returns The target type for the model + */ +function metricsTargetTypeForChartOrCategory(modelDensity) { + if (isNothing(modelDensity)) { + return "tile"; + } + switch (modelDensity) { + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return "tile"; + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return "pill"; + default: + return "tile"; + } +} +//# sourceMappingURL=search-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js new file mode 100644 index 0000000..8be872f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js @@ -0,0 +1,407 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as urls from "../../../foundation/network/urls"; +import * as errors from "../../../foundation/util/errors"; +import * as objects from "../../../foundation/util/objects"; +import * as content from "../../content/content"; +import * as deviceFamily from "../../content/device-family"; +export function targetTypeForMetricsOptions(objectGraph, options) { + let type = options.targetType; + if (!type) { + type = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + return type; +} +export function idTypeForMetricsOptions(options) { + let type = options.idType; + if (type === "none") { + type = null; + } + else if (!type) { + type = "its_id"; + } + return type; +} +export function softwareTypeForData(objectGraph, data) { + return content.isArcadeSupported(objectGraph, data) ? "Arcade" : null; +} +export function metricsKindFromData(objectGraph, data, attributePlatformOverride = undefined) { + const type = serverData.asString(data, "type"); + const isMacType = deviceFamily.dataHasDeviceFamily(objectGraph, data, "mac", true); + const isOnlyMacType = deviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac", true); + const isIOSType = deviceFamily.dataHasAnyDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod", "tvos", "watch"], true); + const isOnlyIOSType = deviceFamily.dataOnlyHasDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod", "tvos", "watch"], true); + const isIOSDeviceType = objectGraph.client.isiOS || objectGraph.client.isTV || objectGraph.client.isWatch; + const isAppleSiliconDeviceType = objectGraph.client.isMac && objectGraph.appleSilicon.isSupportEnabled; + const isVisionDeviceType = objectGraph.client.isVision; + const isVisionType = deviceFamily.dataHasDeviceFamily(objectGraph, data, "realityDevice", true); + const isOnlyVisionType = deviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice", true); + // If: + // - this is a mac only app, or + // - it has multiple types but we're currently on the Mac, or + // - it has multiple types and the plaform override is for Mac, + // use the 'macSoftware' or 'macSoftwareBundle' kinds + if (isOnlyMacType || + (isMacType && objectGraph.client.isMac) || + (isMacType && attributePlatformOverride === "osx")) { + switch (type) { + case "apps": + return "macSoftware"; + case "app-bundles": + return "macSoftwareBundle"; + default: + break; + } + } + // If: + // - this is a Vision only app, or + // - it has multiple types but we're currently on a Vision device, or + // - it has multiple types and the platform override is for Vision, + // use the 'visionSoftware' kind. + if (isOnlyVisionType || + (isVisionType && objectGraph.client.isVision) || + (isVisionType && attributePlatformOverride === "xros")) { + switch (type) { + case "apps": + return "visionSoftware"; + case "app-bundles": + // To add if/when vision supports app bundles. + break; + default: + break; + } + } + // If this is an iOS only app or it has multiple types but we're currently: + // - on an iOS device, or + // - on an Apple Silicon Mac, or + // - on a Vision device, or + // - the platform override is for an iOS-like device + // use the 'iosSoftware' or 'mobileSoftwareBundle' kinds + if (isOnlyIOSType || + (isIOSType && isIOSDeviceType) || + (isIOSType && isAppleSiliconDeviceType) || + (isIOSType && isVisionDeviceType) || + (isIOSType && attributePlatformOverride === "ios") || + (isIOSType && attributePlatformOverride === "watch") || + (isIOSType && attributePlatformOverride === "appletvos")) { + switch (type) { + case "apps": + return "iosSoftware"; + case "app-bundles": + return "mobileSoftwareBundle"; + default: + break; + } + } + switch (type) { + case "in-apps": + return "softwareAddOn"; + case "groupings": + return "grouping"; + case "editorial-elements": + case "editorial-items": + return "editorialItem"; + case "developers": + return "artist"; + default: + return null; + } +} +export function emptyStringIfNullOrUndefined(object) { + if (object === null || object === undefined) { + return ""; + } + return object; +} +export function extractSiriRefAppFromRefURL(urlString) { + if (!urlString) { + return null; + } + const refUrl = new urls.URL(urlString); + let extracteRefApp = null; + const query = refUrl.query; + if (isSome(query)) { + for (const key of Object.keys(query)) { + if (key === "referrer") { + if (query[key] === "siri") { + extracteRefApp = "com.apple.siri"; + } + break; + } + } + } + return extracteRefApp; +} +export function sanitizedMetricsDictionary(dict) { + var _a; + if (isNothing(dict)) { + return {}; + } + return (_a = serverData.asInterface(sanitizeJson(serverData.asJSONData(dict)))) !== null && _a !== void 0 ? _a : {}; +} +function sanitizeJson(json) { + if (serverData.isNull(json)) { + return null; + } + else if (json instanceof Array) { + const arrayCopy = []; + for (const value of json) { + const sanitizedValue = sanitizeJson(value); + if (serverData.isDefinedNonNull(sanitizedValue)) { + arrayCopy.push(sanitizedValue); + } + } + return arrayCopy; + } + else if (json instanceof Object) { + const objectCopy = {}; + Object.keys(json).forEach((key, index, array) => { + const value = json[key]; + const sanitizedValue = sanitizeJson(value); + if (serverData.isDefinedNonNull(sanitizedValue)) { + objectCopy[key] = sanitizedValue; + } + }); + return objectCopy; + } + return json; +} +export function searchTermFromRefURL(refUrlString) { + if (!refUrlString) { + return null; + } + const refUrl = new urls.URL(refUrlString); + const queryItems = refUrl.query; + const searchTerm = queryItems === null || queryItems === void 0 ? void 0 : queryItems["term"]; + const path = refUrl.pathname; + if (serverData.isNull(searchTerm) || serverData.isNull(path)) { + return null; + } + if (!path.endsWith("/search")) { + return null; + } + // the url object has already urldecoded this query parameter + const plainTerm = searchTerm; + return plainTerm; +} +/** + * Get a search term from a product URL, if one has been added. + * @param productUrlString The URL of a product + * @returns A string, if a search term exists. + */ +export function searchTermFromProductURL(productUrlString) { + if (isNothing(productUrlString)) { + return null; + } + const productUrl = new urls.URL(productUrlString); + const queryItems = productUrl.query; + const searchTerm = queryItems === null || queryItems === void 0 ? void 0 : queryItems["searchTerm"]; + const path = productUrl.pathname; + if (isNothing(searchTerm) || isNothing(path)) { + return null; + } + if (!path.includes("/app")) { + return null; + } + const plainTerm = searchTerm; + return plainTerm; +} +/** + * Convert a product's data `type` and top lockup icon into a `MetricsPlatformDisplayStyle` object. + * This is used to determine how an app icon is presented (i.e. as watch, as atv) for metrics. + * @param objectGraph Current object graph + * @param data Server data for the app + * @param artwork The product's top lockup icon. + * @param clientIdentifierOverride The preferred client identifier, if any. + * @returns a MetricsPlatformDisplayStyle object. + */ +// <rdar://problem/47715014> Metrics: Send editorial intent buy param to finance for watch apps +export function metricsPlatformDisplayStyleFromData(objectGraph, data, artwork, clientIdentifierOverride) { + if (!data || !artwork) { + return "unknown"; + } + if (data.type === "app-bundles") { + return "bundle"; + } + const artworkStyle = artwork.style; + if (isNothing(artworkStyle)) { + return "unknown"; + } + switch (artworkStyle) { + case "roundedRect": + case "roundedRectPrerendered": { + return "ios"; + } + case "unadorned": { + return "mac"; + } + case "tvRect": { + return "tv"; + } + case "round": + case "roundPrerendered": { + const attributePlatform = content.iconAttributePlatform(objectGraph, data, clientIdentifierOverride !== null && clientIdentifierOverride !== void 0 ? clientIdentifierOverride : undefined); + if (attributePlatform === "xros") { + return "vision"; + } + else { + return "watch"; + } + } + case "pill": { + return "messages"; + } + case "iap": { + return "iap"; + } + default: { + errors.unreachable(artworkStyle); + return "unknown"; + } + } +} +// region Search GhostHint +/** + * Move ghostHint fields for click events + * @param eventFields Fields of event to modify in place. + */ +export function adjustGhostHintFieldsForClick(eventFields) { + /** + * Copy `searchGhostHintPrefix` to `searchPrefix` if no prefix is present. + * - JS-built search actions specify searchPrefix (matches prefixTerm of hint request). + * - Native-built search actions don't specify searchPrefix (dynamic). + */ + const existingSearchPrefix = serverData.asString(eventFields, "searchPrefix"); + const ghostHintPrefix = serverData.asString(eventFields, "searchGhostHintPrefix"); + if (serverData.isNull(existingSearchPrefix) && isSome(ghostHintPrefix) && (ghostHintPrefix === null || ghostHintPrefix === void 0 ? void 0 : ghostHintPrefix.length) > 0) { + eventFields["searchPrefix"] = ghostHintPrefix; + } + /** + * Delete `searchGhostHintTerm` if phase is pending (i.e. only send if displayed or rejected) per POR. + */ + const ghostHintTermPhase = serverData.asString(eventFields, "searchGhostHintTermPhase"); + if (ghostHintTermPhase === "pending") { + delete eventFields["searchGhostHintTerm"]; + } +} +/** + * Move ghostHint fields for seach events + * @param eventFields Fields of event to modify in place. + */ +export function adjustGhostHintFieldsForSearch(eventFields) { + var _a; + /** + * Copy `searchGhostHintPrefix` to `searchPrefix` if no prefix is present. + * - JS-built search actions specify actionDetails.searchPrefix (matches prefixTerm of hint request). + * - Native-built search actions don't specify actionDetails.searchPrefix (dynamic). + */ + const actionDetails = (_a = eventFields["actionDetails"]) !== null && _a !== void 0 ? _a : {}; + const existingSearchPrefix = actionDetails["searchPrefix"]; + const ghostHintPrefix = serverData.asString(eventFields, "searchGhostHintPrefix"); + if (serverData.isNull(existingSearchPrefix) && isSome(ghostHintPrefix) && (ghostHintPrefix === null || ghostHintPrefix === void 0 ? void 0 : ghostHintPrefix.length) > 0) { + actionDetails["searchPrefix"] = ghostHintPrefix; + eventFields["actionDetails"] = actionDetails; + } + /** + * Delete `searchGhostHintTerm` if phase is pending (i.e. only send if displayed or rejected) per POR. + */ + const ghostHintTermPhase = serverData.asString(eventFields, "searchGhostHintTermPhase"); + if (ghostHintTermPhase === "pending") { + delete eventFields["searchGhostHintTerm"]; + } + /** + * Prune `searchGhostHintTerm` if `actionType` is `input`, i.e. is from hints page loading. + * This is a side-effect of when the event is fired, and otherwise doesn't belong there. + */ + if (eventFields["actionType"] === "input") { + delete eventFields["searchGhostHintTerm"]; + } +} +/** + * Clean up extraneous generated fields. These extra fields are speculative + * to allow some additional JS customization if needed for SSS. + * @param eventFields Event fields to modify in place. + */ +export function removeExtraGhostHintFields(eventFields) { + // Prune prefix annotation. + delete eventFields["searchGhostHintPrefix"]; + // Prune phase annotation. + delete eventFields["searchGhostHintTermPhase"]; + // Prune historical annotation. + delete eventFields["searchGhostHintTermLastDisplayed"]; +} +// endregion +// region Arcade Upsell Marketing Items +/** + * Returns a dictionary of fields pulled out from the meta.metrics dictionary associated with a marketing item response. + * This data comes from Mercury, and we simply pull out relevant fields to be hoisted into the top-level base field on + * metrics events (page/impression/click). + * @param marketingItemData The marketing item response data. + */ +export function marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData) { + if (!serverData.isDefinedNonNull(marketingItemData)) { + return null; + } + const fieldsData = {}; + const marketingDictionary = serverData.asDictionary(marketingItemData, "meta.metrics"); + if (!serverData.isDefinedNonNullNonEmpty(marketingDictionary)) { + return null; + } + const channelPartner = serverData.asString(marketingDictionary, "channelPartner"); + if (isSome(channelPartner) && (channelPartner === null || channelPartner === void 0 ? void 0 : channelPartner.length) > 0) { + fieldsData["channelPartner"] = channelPartner; + } + const eligibilityType = serverData.asString(marketingDictionary, "eligibilityType"); + if (isSome(eligibilityType) && (eligibilityType === null || eligibilityType === void 0 ? void 0 : eligibilityType.length) > 0) { + fieldsData["eligibilityType"] = eligibilityType; + } + const upsellScenario = serverData.asString(marketingDictionary, "upsellScenario"); + if (isSome(upsellScenario) && (upsellScenario === null || upsellScenario === void 0 ? void 0 : upsellScenario.length) > 0) { + fieldsData["upsellScenario"] = upsellScenario; + } + fieldsData["marketing"] = { + marketingItemId: marketingItemData.id, + }; + return fieldsData; +} +// endregion +// region On Device Personalization +/** + * Merges the provided reco metrics data with the on device personalization metrics data. + * @param metricsData The input reco metrics data + * @param onDevicePersonalizationProcessingType The type of processing that occurred on the data + * @param onDevicePersonalizationMetricsData The metrics data provided by the on device personalization framework + * @returns Reco metrics data, or null + */ +export function combinedRecoMetricsDataFromMetricsData(metricsData, onDevicePersonalizationProcessingType, onDevicePersonalizationMetricsData) { + let combinedMetricsData = null; + if (serverData.isDefinedNonNull(metricsData)) { + combinedMetricsData = objects.shallowCopyOf(metricsData); + } + if (serverData.isDefinedNonNull(onDevicePersonalizationProcessingType)) { + if (serverData.isNull(combinedMetricsData)) { + combinedMetricsData = {}; + } + combinedMetricsData["odpModuleUpdate"] = onDevicePersonalizationProcessingType.toString(); + } + if (serverData.isDefinedNonNullNonEmpty(onDevicePersonalizationMetricsData)) { + if (serverData.isNull(combinedMetricsData)) { + combinedMetricsData = {}; + } + combinedMetricsData["userSegment"] = onDevicePersonalizationMetricsData; + } + return combinedMetricsData; +} +// endregion +// region Metrics ID +/** + * Clean up dsid fields. We want this as a last line of defense for removing dsid. + * @param eventFields Event fields to modify in place. + */ +export function removeDSIDFields(eventFields) { + // Prune dsid + delete eventFields["dsid"]; + delete eventFields["DSID"]; +} +// endregion +//# sourceMappingURL=util.js.map
\ No newline at end of file |
