From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../app-store/tmp/src/common/metrics/builder.js | 257 ++++++++ .../tmp/src/common/metrics/event-linter.js | 563 +++++++++++++++++ .../tmp/src/common/metrics/helpers/buy.js | 331 ++++++++++ .../tmp/src/common/metrics/helpers/clicks.js | 458 ++++++++++++++ .../tmp/src/common/metrics/helpers/constants.js | 11 + .../tmp/src/common/metrics/helpers/impressions.js | 419 +++++++++++++ .../legacy-metrics-identifier-fields-opt-out.js | 18 + .../tmp/src/common/metrics/helpers/location.js | 188 ++++++ .../tmp/src/common/metrics/helpers/media.js | 34 ++ .../tmp/src/common/metrics/helpers/misc.js | 46 ++ .../tmp/src/common/metrics/helpers/models.js | 671 +++++++++++++++++++++ .../tmp/src/common/metrics/helpers/page.js | 482 +++++++++++++++ .../metrics/helpers/search-focus-impressions.js | 57 ++ .../metrics/helpers/search-result-impressions.js | 56 ++ .../metrics/helpers/search/search-shelves.js | 98 +++ .../tmp/src/common/metrics/helpers/util.js | 407 +++++++++++++ .../src/common/metrics/metrics-referral-context.js | 370 ++++++++++++ 17 files changed, 4466 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/metrics') diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js new file mode 100644 index 0000000..ffb205f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js @@ -0,0 +1,257 @@ +import { isSome } from "@jet/environment"; +import { isNothing } from "@jet/environment/types/optional"; +import { AppStoreMetricsData, } from "../../api/models/metrics/metrics"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as client from "../../foundation/wrappers/client"; +import { EventLinter } from "./event-linter"; +import { optOutOfLegacyMetricsIdFieldsProvider } from "./helpers/legacy-metrics-identifier-fields-opt-out"; +export function createMetricsClickData(objectGraph, targetId, targetType, eventFields, additionalIncludingFields, isDefaultBrowser) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "click"; + fields["targetType"] = targetType; + fields["targetId"] = targetId; + const include = ["impressionsSnapshot", "pageFields"]; + if (!preprocessor.GAMES_TARGET) { + include.push("contentRestrictionReasons"); + } + if (additionalIncludingFields) { + include.push(...additionalIncludingFields); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, null, isDefaultBrowser))); +} +export function createMetricsBackClickData(objectGraph, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["actionType"] = "back"; + const clickData = createMetricsClickData(objectGraph, "back", "button", fields); + return clickData; +} +export function createMetricsPageData(objectGraph, isReferralEligible, isCrossfireReferralCandidate, timingMetrics, eventFields, isDefaultBrowser) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "page"; + if (timingMetrics) { + fields["clientCorrelationKey"] = timingMetrics.clientCorrelationKey; + fields["requestStartTime"] = timingMetrics.requestStartTime; + fields["responseStartTime"] = timingMetrics.responseStartTime; + fields["responseEndTime"] = timingMetrics.responseEndTime; + } + const include = ["pageFields", "pageReferrer"]; + if (!preprocessor.GAMES_TARGET) { + include.push("userContentRestriction"); + } + if (isReferralEligible) { + include.push("crossfireReferral"); + } + else if (isCrossfireReferralCandidate) { + include.push("crossfireReferralCandidate"); // Only possible when not crossfire eligible. + } + addAltAb2DataToEventFields(objectGraph, fields); + addWebClientEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, null, isDefaultBrowser))); +} +export function createMetricsSearchData(objectGraph, term, target, actionType, actionUrl, eventFields, additionalIncludingFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["term"] = term; + fields["targetType"] = target; + fields["actionType"] = actionType; + if (actionUrl) { + fields["actionUrl"] = actionUrl; // actionUrl is defined for `hints` but not for searches fired from elsewhere. + } + fields["eventType"] = "search"; + const include = ["pageReferrer"]; + if (additionalIncludingFields) { + include.push(...additionalIncludingFields); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +export function createMetricsPageRenderFields(objectGraph, timingMetrics, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "pageRender"; + if (timingMetrics) { + if (!fields["pageUrl"]) { + fields["pageUrl"] = timingMetrics.pageURL; + } + fields["clientCorrelationKey"] = timingMetrics.clientCorrelationKey; + fields["platformRequestStartTime"] = timingMetrics.requestStartTime; + fields["platformResponseStartTime"] = timingMetrics.responseStartTime; + fields["platformResponseEndTime"] = timingMetrics.responseEndTime; + fields["platformResponseWasCached"] = timingMetrics.responseWasCached; + fields["platformJsonParseStartTime"] = timingMetrics.parseStartTime; + fields["platformJsonParseEndTime"] = timingMetrics.parseEndTime; + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return fields; +} +/** + * Create a metrics data for regular impressions. + * @param objectGraph The dependency graph for the App Store. + * @param eventFields Base fields to build off on. + * @param shouldIncludeAdFieldsForPad Whether or not metrics data should include iPad related fields for adverts. + * @param includeAdRotationFields Whether or not metrics data should include advert related fields. + * @param hasImpressionsAppendix Whether condensed format search results should track appendix data. + */ +export function createMetricsImpressionsData(objectGraph, eventFields, shouldIncludeAdFieldsForPad, includeAdRotationFields, hasImpressionsAppendix) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "impressions"; + fields["impressionQueue"] = "data-metrics"; + fields["eventVersion"] = 4; + const include = ["impressions", "pageFields", "pageReferrer"]; + if (!preprocessor.GAMES_TARGET) { + include.push("contentRestrictionReasons"); + } + if (shouldIncludeAdFieldsForPad) { + include.push("advertDeviceWindow"); + } + if (includeAdRotationFields) { + include.push("advertRotation"); + } + if (hasImpressionsAppendix) { + include.push("impressionsAppendix"); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, ["eventVersion"], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +/** + * Create a metrics data for fast impressions, a special type for SearchAds on Search Page. + * Included fields assume fields are for search page w/ Ads. + * @param baseFields Base fields to build off on. + */ +export function createMetricsFastImpressionsData(objectGraph, baseFields, pageInformation) { + var _a, _b; + const shouldIncludeAdFieldsForPad = serverData.isDefinedNonNull(pageInformation.iAdInfo) && + objectGraph.client.isPad && + (isNothing(pageInformation.iAdInfo.missedOpportunityReason) || + pageInformation.iAdInfo.missedOpportunityReason.length === 0); + const shouldIncludeAdRotationFields = (_b = (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.shouldIncludeAdRotationFields) !== null && _b !== void 0 ? _b : false; + const exclude = []; + const fields = createMetricsImpressionsData(objectGraph, baseFields, shouldIncludeAdFieldsForPad, shouldIncludeAdRotationFields, false).fields; + fields["impressionQueue"] = "data-metrics-impressions-low-latency"; + if (pageInformation !== null && serverData.isDefinedNonNull(pageInformation.iAdInfo)) { + const eventVersion = pageInformation.iAdInfo.fastImpressionsEventVersion; + fields["eventVersion"] = eventVersion; + exclude.push("eventVersion"); + // Make some manual adjustments to events for v5. + if (eventVersion === 5) { + // Indicates the viewable area where elements can be impressed excludes the tab bar + fields["viewableArea"] = "excludingTabBar"; + // Remove the `iAdPlacementId` for v5. It's added for compatibility with v4 impressions. + delete fields["iAdPlacementId"]; + } + } + const include = ["fastImpressions", "pageFields", "pageReferrer"]; + if (shouldIncludeAdFieldsForPad) { + include.push("advertDeviceWindow"); + } + if (shouldIncludeAdRotationFields) { + include.push("advertRotation"); + } + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, exclude, topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, pageInformation))); +} +export function createMetricsMediaData(objectGraph, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "media"; + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, [], [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +export function createMetricsMediaClickData(objectGraph, targetId, targetType, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "click"; + fields["targetType"] = targetType; + fields["targetId"] = targetId; + const include = ["pageFields"]; + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +function shouldFlushFromEventFields(objectGraph, eventFields, pageInformation = null, isDefaultBrowser) { + var _a, _b; + const eventType = eventFields["eventType"]; + let shouldFlush = false; + if (!serverData.isDefinedNonNullNonEmpty(eventType)) { + return shouldFlush; + } + const isDefaultBrowserContext = isDefaultBrowser !== null && isDefaultBrowser !== void 0 ? isDefaultBrowser : false; + switch (eventType) { + case "click": + shouldFlush = + serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData) || + isDefaultBrowserContext; + break; + case "exit": + shouldFlush = true; + break; + case "impressions": + shouldFlush = serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData); + // Not my best work here. + // For most ad placements on a page, it's fine to check the `hasIAdData` as an indicator for whether we should flush after a given event. + // The challenge for the product page placements is that they're fetched asynchronously, and we don't create an entirely new impressions + // event when that happens (we just update fields within the event via the `pageChange` mechanism). To ensure that fast impression events on + // the product page are actually fast, we have to force the events to flush if they meet the below conditions that effectively guarantee + // we will be impressing something on the low latency queue. + if (eventFields["impressionQueue"] === "data-metrics-impressions-low-latency" && + (((_a = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.placementType) === "productPageYMAL" || + ((_b = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType) === "productPageYMALDuringDownload")) { + shouldFlush = true; + } + break; + case "page": + // rdar://98487650 (CL seed: Product Page observed lower % of events coming in <2 mins ( page change + page events )) + // In order to resolve issues with flushing timeliness in the PPE: rdar://93127678 ([Metrics] Dismissing store sheet does not fire clickstream events) + // we allow page events with ad data in them to cause a flush on product pages in the PPE. + // We also pre-emptively flush for page events where it's the default browser flow in order to "prewarm" the metrics pipeline, ensuring flushes can occur + // if and when the user taps "Open" on an installed app. + const isProductPageExtension = objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier; + shouldFlush = + isProductPageExtension && + (serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData) || + isDefaultBrowserContext); + break; + default: + break; + } + const isSubscribePageExtensionEnterExitEventsEnabled = objectGraph.host.isiOS; + if (objectGraph.host.clientIdentifier === client.subscribePageExtensionIdentifier && + !isSubscribePageExtensionEnterExitEventsEnabled) { + // Metrics: missing metrics events for arcade upsell from springboard app open on unsubscribed user + // Until a native fix is in to address enter/exit events in SPE (subscribePageExtensionEnterExitEvents), we + // need to force events in the SubscribePageExtension to trigger a flush. + shouldFlush = true; + } + return shouldFlush; +} +function topicFromEventFields(objectGraph, eventFields) { + const topic = eventFields["topic"] || objectGraph.bag.metricsTopic; + return topic; +} +function addAltAb2DataToEventFields(objectGraph, eventFields) { + if (objectGraph.bag.isMetricsAb2DataFallbackEnabled && isSome(objectGraph.experimentCache)) { + eventFields["alt_ab2_data"] = JSON.stringify(objectGraph.experimentCache.createAb2Data()); + } +} +/** + * Attach expected metrics fields specific to the "web" client, if necessary + */ +function addWebClientEventFields(objectGraph, eventFields) { + var _a; + if (!objectGraph.client.isWeb) { + return; + } + // The `platformContext` field is expected to reflect the "platform" that the user is browsing + // at the time of the event + eventFields["platformContext"] = (_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform; +} +//# sourceMappingURL=builder.js.map \ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js new file mode 100644 index 0000000..ad9706c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js @@ -0,0 +1,563 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { LintedMetricsEvent } from "../../api/models/metrics/metrics"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { BuyParameters } from "../../foundation/metrics/buy-parameters"; +import { cookiesOf } from "../../foundation/metrics/cookies"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +import { URL } from "../../foundation/network/urls"; +import { reduceSignificantDigits } from "../../foundation/util/math-util"; +import * as constants from "./helpers/constants"; +import { stripUniqueImpressionIdsIfNecessary } from "./helpers/impressions"; +import { iAdDismissAdActionMetricsParameterStringToken, iAdURLLineItemParameterStringToken, iAdURLParameterStringToken, } from "./helpers/models"; +import * as searchResultImpressions from "./helpers/search-result-impressions"; +import * as searchFocusImpressions from "./helpers/search-focus-impressions"; +import * as util from "./helpers/util"; +import { MetricsReferralContext } from "./metrics-referral-context"; +/** + * A type which applies App Store business rules to metrics fields + * and generates events which are ready for posting to figaro. + */ +export class EventLinter { + /** + * Create an event linter. + * + * @param options The options which specify various behaviors of the new linter. + * This object will be frozen. + */ + constructor(options) { + this._options = Object.freeze(options); + } + // endsection + // section Public Properties + /** + * Topic to use if an event fields blob does not specify one. + */ + get defaultTopic() { + return this._options.defaultTopic; + } + // endsection + // section Utilities + /** + * Reduce the accuracy of fields in a blob according to + * a given array of rules found in a metrics objectGraph.bag. + * + * @param eventFields The fields of an event to reduce the accuracy of. + * @param rules An array of rules from a metrics objectGraph.bag. + */ + _reduceFieldAccuracy(eventFields, rules) { + for (const rule of rules) { + const fieldName = serverData.asString(rule, "fieldName"); + if (serverData.isNull(fieldName)) { + continue; + } + const value = serverData.asNumber(eventFields, fieldName); + if (serverData.isNull(value)) { + continue; + } + let magnitude = serverData.asNumber(rule, "magnitude"); + if (serverData.isNull(magnitude)) { + magnitude = 1024 * 1024; + } + let significantDigits = serverData.asNumber(rule, "significantDigits"); + if (serverData.isNull(significantDigits)) { + significantDigits = 2; + } + if (magnitude <= 0.0 || significantDigits < 0.0) { + // This is the failure mode from MetricsKit. + eventFields[fieldName] = Number.NaN; + continue; + } + const scaledValue = value / magnitude; + eventFields[fieldName] = reduceSignificantDigits(scaledValue, significantDigits); + } + } + /** + * Returns a new URL by scrubbing any ad fields we've inserted into URLs to pass + * ad attribution information between pages. + * @param urlString The original URL to be scrubbed + * @returns A scrubbed URL. + */ + _urlScrubbingAdParameters(urlString) { + const url = new URL(urlString); + url.removeParam(iAdURLParameterStringToken); + url.removeParam(iAdURLLineItemParameterStringToken); + url.removeParam(iAdDismissAdActionMetricsParameterStringToken); + return url.build(); + } + /** + * Returns a new URL by scrubbing everything but the protocol, domain, and port (e.g. host). + * @param urlString The original URL to be scrubbed + * @returns A scrubbed URL. + */ + _urlScrubbingExtRefUrl(urlString) { + const url = new URL(urlString); + url.username = ""; + url.password = ""; + url.pathname = undefined; + url.query = undefined; + url.hash = undefined; + return url.build(); + } + // endsection + // section Fast Impressions Deresolution + _derezFastImpressions(eventFields) { + const impressionQueue = serverData.asString(eventFields, "impressionQueue"); + const eventVersion = serverData.asNumber(eventFields, "eventVersion"); + if (impressionQueue !== "data-metrics-impressions-low-latency") { + return; // Only scrub data going to the AP low latency queue. + } + // Scrub `viewedInfo` of a v4 event. + if (eventVersion === 4) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isNothing(impression)) { + return impression; + } + const viewedInfo = serverData.asArrayOrEmpty(impression, "viewedInfo"); + if (viewedInfo.length === 0) { + return impression; // V3. No modification. + } + /** + * Metrics: iAd: JS must clear impression start times, and derezz duration to 2 sig figs + * - start time to 0 (strange ask - we already send this as part of `impressionTimes`) + * - duration to 2 sig fig. + */ + impression["viewedInfo"] = viewedInfo.map((interval) => { + if (isNothing(interval)) { + return interval; + } + const duration = serverData.asNumber(interval, "d"); + interval["s"] = 0; + if (isSome(duration)) { + interval["d"] = reduceSignificantDigits(duration, 2); + } + return interval; + }); + return impression; + }); + } + // Scrub `viewedInfoDetailed` of a v5 event. + if (eventVersion === 5) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isNothing(impression)) { + return impression; + } + // Impression items in a low latency impressions event shouldn't have a `cardType` field. + delete impression["cardType"]; + if (serverData.isNullOrEmpty(serverData.asString(impression, "iAdMetadata")) || + serverData.isNullOrEmpty(serverData.asString(impression, "iAdImpressionId"))) { + // If the iAdMetadata or iAdImpressionId for this impression on the fast queue is null, + // it's an organic result that has added instrumentation as it's in a defined ad slot. + // For these items, we have to scrub the adamId for privacy reasons. + delete impression["id"]; + } + // Get the dictionary of values, formatted like so: + // {0: [{s: 11636658562756, d: 23281}], 50: [{s: 1636658559350, d: 378}]} + const viewedInfoDetailed = serverData.asDictionary(impression, "viewedInfoDetailed"); + if (isNothing(viewedInfoDetailed) || serverData.isNullOrEmpty(viewedInfoDetailed)) { + return impression; // Nothing to modify. + } + /** + * rdar://89785026 (Chainlink: v5 Impressions ViewedInfoDetailed - Fixes for startTime and duration) + * - match scrubbing "s" value and de-rezing "d" value as per above v4 implementation. + */ + Object.entries(viewedInfoDetailed).forEach(([key, value]) => { + // For each key/value pair, grab the array of traditional "viewedInfo". + const viewedInfo = serverData.asArrayOrEmpty(value); + // Iterate over the array of values, scrubbing the required children. + viewedInfoDetailed[key] = viewedInfo.map((interval) => { + if (isNothing(interval)) { + return interval; + } + const duration = serverData.asNumber(interval, "d"); + interval["s"] = 0; + if (isSome(duration)) { + interval["d"] = reduceSignificantDigits(duration, 2); + } + return interval; + }); + }); + // Re-set the modified dictionary on the `impression` object. + impression["viewedInfoDetailed"] = viewedInfoDetailed; + return impression; + }); + } + } + _stripContentRatingImpressionFields(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isSome(impression)) { + // "contentRating" and "bundleId" are used by native to + // determine the value of "contentRestrictionReasons" (whether a + // lockup's offer is disabled due to content restrictions). + // However we don't want these fields in the final impression. + delete impression["contentRating"]; + delete impression["bundleId"]; + } + return impression; + }); + } + // endsection + // section Search Results Page Impressions + /** + * Decorate `impressions` field for click and impressions events on SRP. + */ + _decorateSearchResultImpressions(eventFields) { + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + /** + * Only run on SRP. + */ + const isSearchResultsPage = pageType === "Search" && pageId !== "hints"; + if (isSearchResultsPage) { + searchResultImpressions.decorateImpressionParentId(eventFields); + } + } + // endsection + // section Search Focus Page Impressions + /** + * Decorate `impressions` field for click and impressions events on SFP. + */ + _decorateSearchFocusImpressions(eventFields) { + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + /** + * Only run on SFP. + */ + if (pageType === "SearchFocus" && pageId === "Focus") { + searchFocusImpressions.decorateImpressionParentId(eventFields); + } + } + // endsection + // section Rules + /** + * Apply the rules which are universal to all metrics events + * to a given metrics fields linter. + * + * @param eventFields The fields which will be used to construct a built event. + * @param topic The topic the built event will be submitted to. + */ + _decorateAll(objectGraph, eventFields, topic) { + var _a, _b, _c; + const getBagValue = this._options.bagProvider; + // - Metrics base fields + const metricsBase = getBagValue("metricsBase", topic); + if (!serverData.isNull(metricsBase) && typeof metricsBase === "object") { + Object.assign(eventFields, metricsBase); + } + // - Universal basic fields + eventFields["clientBuildType"] = this._options.buildType; + eventFields["resourceRevNum"] = this._options.jsVersion; + eventFields["xpSendMethod"] = "jet-js"; + this._options.buyDecorator.useApp(serverData.asString(eventFields, "app")); + // - Universal scrubbing + delete eventFields[constants.contextualAdamIdKey]; + // - Cookie-based fields + const cookies = cookiesOf(serverData.asString(eventFields, "cookie")); + for (const cookie of cookies) { + if (cookie.key === "xp_ci") { + // Always update buy decorator. + this._options.buyDecorator.useClientId(cookie.value); + break; + } + } + delete eventFields["cookie"]; + const clientIdFields = (_b = (_a = objectGraph.metricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsFieldsForTypes([MetricsIdentifierType.client])) !== null && _b !== void 0 ? _b : {}; + Object.assign(eventFields, clientIdFields); + delete eventFields["clientGeneratedId"]; + // - page + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + if (!serverData.isNull(pageType) && !serverData.isNull(pageId)) { + const separator = serverData.asString(getBagValue("compoundSeparator", topic)) || "_"; + eventFields["page"] = `${pageType}${separator}${pageId}`; + } + // - Field value resolution reduction + const rules = serverData.asArrayOrEmpty(getBagValue("deResFields", topic)); + this._reduceFieldAccuracy(eventFields, rules); + // Scrub sensitive urls in event from data that should not leave the device. + // At times we insert ad metadata into URLs in order to pass ad attribution information between pages. + // Additionally, scrub extRefUrl of everything but the origin. + const urlFieldsToScrub = ["pageUrl", "actionUrl", "extRefUrl", "refUrl", "url", "parentPageUrl"]; + for (const urlField of urlFieldsToScrub) { + const urlString = serverData.asString(eventFields, urlField); + if (isSome(urlString) && urlString.length > 0) { + eventFields[urlField] = + urlField === "extRefUrl" + ? this._urlScrubbingExtRefUrl(urlString) + : this._urlScrubbingAdParameters(urlString); + } + } + // Where an `overridePageContext` has been provided, use it as the `pageContext` value. + const overridePageContext = serverData.asString(eventFields, "overridePageContext"); + if (isSome(overridePageContext)) { + delete eventFields["overridePageContext"]; + eventFields["pageContext"] = overridePageContext; + } + if (objectGraph.bag.isMetricsUserIdFallbackEnabled) { + const existingUserId = serverData.asString(eventFields, "userId"); + let metricsUserDSID = null; + if (isNothing(existingUserId) || + existingUserId.length === 0 || + existingUserId.length === EventLinter.clientGeneratedUserIdLength) { + metricsUserDSID = (_c = objectGraph.user.dsid) !== null && _c !== void 0 ? _c : null; + } + if (isSome(metricsUserDSID) && metricsUserDSID.length > 0) { + eventFields["dsId"] = metricsUserDSID; + } + } + } + _decorateClick(eventFields) { + util.adjustGhostHintFieldsForClick(eventFields); + // clicks have snapshot impressions field. + this._decorateSearchResultImpressions(eventFields); + this._decorateSearchFocusImpressions(eventFields); + MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields); + this._filterBuyParams(eventFields); + /// rdar://118948967 (Disable snapshot impressions on iOS 17) + const pageType = serverData.asString(eventFields, "pageType"); + if (isNothing(pageType) || !pageType.toLowerCase().includes("search")) { + delete eventFields["impressions"]; + } + stripUniqueImpressionIdsIfNecessary(eventFields); + } + /** + * Apply the rules specific to the `impression` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + * @returns Whether or not the impressions event is valid and non-empty + */ + _decorateImpressions(objectGraph, eventFields) { + if (serverData.isNullOrEmpty(eventFields["impressions"])) { + return false; + } + this._derezFastImpressions(eventFields); + this._decorateSearchResultImpressions(eventFields); + this._stripContentRatingImpressionFields(eventFields); + const refUrl = serverData.asString(eventFields, "refUrl"); + if (isSome(refUrl) && refUrl.length > 0) { + eventFields["searchTerm"] = util.searchTermFromRefURL(refUrl); + delete eventFields["refUrl"]; + } + if (objectGraph.client.isVision) { + const pageUrl = serverData.asString(eventFields, "pageUrl"); + if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) { + const searchTerm = util.searchTermFromProductURL(pageUrl); + if (isSome(searchTerm)) { + eventFields["searchTerm"] = searchTerm; + } + } + } + stripUniqueImpressionIdsIfNecessary(eventFields); + // We need the impressionQueue for _derezFastImpressions, so we know which events to derez, but then we need to + // remove it from the event + // rdar://144333694 ([Ad Platforms][CrystalE][iOS]Seeing 'Impression Queue' as a top level + // field for impressions which was removed earlier) + delete eventFields["impressionQueue"]; + return true; + } + /** + * Apply the rules specific to the `media` event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decorateMedia(eventFields) { + const position = serverData.asNumber(eventFields, "position"); + if (!serverData.isNull(position)) { + eventFields["position"] = Math.round(position); + } + } + /** + * Apply the rules specific to the `buyparams` . + * + * @param eventFields The fields which will be used to construct a built event. + */ + _filterBuyParams(eventFields) { + const buyParamsString = serverData.asString(eventFields, "actionDetails.buyParams"); + if (isSome(buyParamsString) && buyParamsString.length > 0) { + const buyParams = new BuyParameters(buyParamsString); + const disallowedFields = ["ownerDsid"]; + disallowedFields.forEach((key) => { + buyParams.set(key, null, null); + }); + if (isSome(eventFields["actionDetails"])) { + eventFields["actionDetails"]["buyParams"] = buyParams.toString(); + } + } + } + /** + * Apply the rules specific to the `page` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePage(objectGraph, eventFields) { + const page = serverData.asString(eventFields, "page"); + if (!serverData.isNull(page)) { + eventFields["pageHistory"] = this._options.buyDecorator.getPageHistoryFor(page); + } + MetricsReferralContext.shared.setReferralDataForProductPageExtensionIfNecessary(eventFields); + MetricsReferralContext.shared.beginReferralContextForPageIfNecessary(eventFields); + MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields); + // Make sure to add the referral data before checking for valid refUrl + const refUrl = serverData.asString(eventFields, "refUrl"); + if (!serverData.isNull(refUrl)) { + const refApp = util.extractSiriRefAppFromRefURL(refUrl); + const searchTerm = util.searchTermFromRefURL(refUrl); + if (refApp !== null && refApp.length > 0) { + eventFields["refApp"] = refApp; + } + if (searchTerm !== null && searchTerm.length > 0) { + eventFields["searchTerm"] = searchTerm; + } + } + if (objectGraph.client.isVision) { + const pageUrl = serverData.asString(eventFields, "pageUrl"); + if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) { + const searchTerm = util.searchTermFromProductURL(pageUrl); + if (isSome(searchTerm)) { + eventFields["searchTerm"] = searchTerm; + } + } + } + } + /** + * Apply the rules specific to the `pageChange` event. The pageChange should have the same treatment + * as page event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePageChange(objectGraph, eventFields) { + this._decoratePage(objectGraph, eventFields); + } + /** + * Apply the rules specific to the `search` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + */ + _decorateSearch(eventFields) { + eventFields["eventVersion"] = 3; + util.adjustGhostHintFieldsForSearch(eventFields); + } + /** + * Apply the rules specific to the `pageExit` event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePageExit(eventFields) { + MetricsReferralContext.shared.endReferralContextIfNecessaryForPageEvent(eventFields); + } + // endsection + // section Filter + _filterExtraneous(eventFields) { + util.removeExtraGhostHintFields(eventFields); + MetricsReferralContext.shared.removeReferralContextInfoFromMetricsEvent(eventFields); + } + // endsection + // section Building Events + /** + * Create a metrics event by applying the business rules of App Store to a given fields blob. + * + * @param eventFields The fields to use to construct an event. + * @returns A built event ready for posting. + */ + makeEvent(objectGraph, eventFields) { + var _a, _b; + if (preprocessor.GAMES_TARGET) { + // Until we get further with Privacy/Legal, the decision has been made to disable instrumentation + // for Game Overlay. the pre-consent fields provider is only active for Game Overlay. Other options + // for more fine-tuned control (_gameCenterPreConsent and _crossUsePreConsent) have been added to + // provide JS flexibility. + const isPreConsentFieldsProviderEnabled = eventFields["_isPreConsentFieldsProviderEnabled"]; + const gameCenterPreConsent = eventFields["_gameCenterPreConsent"]; + const crossUsePreConsent = eventFields["_crossUsePreConsent"]; + if (isPreConsentFieldsProviderEnabled === true || + gameCenterPreConsent === true || + crossUsePreConsent === true) { + return new LintedMetricsEvent({}); + } + delete eventFields["_isPreConsentFieldsProviderEnabled"]; + delete eventFields["_gameCenterPreConsent"]; + delete eventFields["_crossUsePreConsent"]; + } + const eventType = serverData.asString(eventFields, "eventType"); + if (this._options.isLoggingEnabled) { + objectGraph.console.log(`Building event for topic: ${eventType}`); + } + // rdar://135738684 (TLF: Off Store Lockups Removal) + // Currently all events from App Store Components are suppressed. + // Delete all fields to leave an empty event. + const app = eventFields["app"]; + if (app === "com.apple.appstorecomponentsd") { + return new LintedMetricsEvent({}); + } + const topic = serverData.asString(eventFields, "topic") || this._options.defaultTopic; + this._decorateAll(objectGraph, eventFields, topic); + // rdar://148554411 (Metrics: Disable PII collection from U13 accounts) + let isUnderThirteenAccount; + if (preprocessor.GAMES_TARGET && objectGraph.props.enabled("157263806-add-playerBridge-askGlobal")) { + isUnderThirteenAccount = (_b = (_a = objectGraph.player) === null || _a === void 0 ? void 0 : _a.isUnderThirteen) !== null && _b !== void 0 ? _b : false; + } + else { + isUnderThirteenAccount = objectGraph.user.isUnderThirteen; + } + if (isUnderThirteenAccount) { + delete eventFields["dsId"]; + delete eventFields["userId"]; + delete eventFields["canonicalAccountIdentifierOverride"]; + } + const extRefUrl = eventFields["extRefUrl"]; + if (extRefUrl && extRefUrl === "") { + delete eventFields["extRefUrl"]; + } + switch (eventType) { + case "click": + this._decorateClick(eventFields); + break; + case "exit": + break; + case "impressions": + const isValidEvent = this._decorateImpressions(objectGraph, eventFields); + if (!isValidEvent) { + // We want to filter out empty impressions events, + // and passing an empty event will contractually drop it from the metrics recorder + return new LintedMetricsEvent({}); + } + break; + case "media": + this._decorateMedia(eventFields); + break; + case "page": + this._decoratePage(objectGraph, eventFields); + break; + case "pageChange": + this._decoratePageChange(objectGraph, eventFields); + break; + case "pageExit": + this._decoratePageExit(eventFields); + break; + case "search": + this._decorateSearch(eventFields); + break; + default: + break; + } + this._filterExtraneous(eventFields); + if (objectGraph.bag.metricsIdMigrationEnabled) { + util.removeDSIDFields(eventFields); + } + return new LintedMetricsEvent(eventFields); + } +} +/** + * The length of a client generated user ID. + */ +EventLinter.clientGeneratedUserIdLength = 24; +/** + * Key whose value indicates if event fields contain iAd data. + */ +EventLinter.hasIAdData = "hasiAdData"; +//# sourceMappingURL=event-linter.js.map \ No newline at end of file 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: 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; + /** 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 + // Crossfire: Metrics: extRefApp2/extRefUrl2 buyparam are being persisted too strongly, being sent on buys of other app pages + let productLockup = null; + if (page instanceof models.ProductPage) { + productLockup = page; + } + else if (page instanceof models.ShelfBasedProductPage) { + productLockup = page.lockup; + } + if (productLockup) { + const eligibleActions = []; + if (productLockup.buttonAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction); + } + else if (productLockup.buttonAction instanceof models.OfferConfirmationAction && + productLockup.buttonAction.buyAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.buyAction); + } + else if (productLockup.buttonAction instanceof models.OfferAlertAction && + productLockup.buttonAction.completionAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.completionAction); + } + else if (productLockup.buttonAction instanceof models.OfferStateAction) { + if (productLockup.buttonAction.buyAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.buyAction); + } + if (productLockup.buttonAction.defaultAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.defaultAction); + } + if (productLockup.buttonAction.openAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.openAction); + } + if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && + productLockup.buttonAction.subscribePageAction.page === "arcadeSubscribe" && + isSome(productLockup.buttonAction.subscribePageAction.pageUrl) && + productLockup.buttonAction.subscribePageAction.pageUrl.length > 0) { + const pageUrl = urls.URL.from(productLockup.buttonAction.subscribePageAction.pageUrl); + pageUrl.param(Parameters.includePostSubscribeAttribution, "true"); + productLockup.buttonAction.subscribePageAction.pageUrl = pageUrl.build(); + } + if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && + productLockup.buttonAction.subscribePageAction.pageData instanceof models.MarketingItemRequestInfo && + productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction instanceof + models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction); + } + } + for (const eligibleAction of eligibleActions) { + if (eligibleAction.purchaseConfiguration) { + eligibleAction.purchaseConfiguration.excludeAttribution = false; + } + } + } +} +/** + * Add an updated set of page metrics to the shelf that will be used to update the existing page metrics, + * as well as be sent as a `pageChange` event. + * @param objectGraph The object graph. + * @param shelf The shelf to attach the pageChange fields to + * @param pageInformation The modified page information for the containing page. + * @param fieldsModifier A field modifier, if required. + * @returns + */ +export function addPageChangeFieldsToShelfWithInformation(objectGraph, shelf, pageInformation, fieldsModifier) { + if (serverData.isNull(pageInformation)) { + return; + } + let updatedEvents = []; + // Page + const pageFields = misc.fieldsFromPageInformation(pageInformation); + const pageEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); + updatedEvents.push(pageEvent); + // Impressions + const impressionsInstructions = impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier); + const impressionsEvents = impressionsInstructions.map((instruction) => instruction.data); + updatedEvents = updatedEvents.concat(impressionsEvents); + filterPageMetricsFieldsForPageChange(pageFields); + updatedEvents.forEach((event) => filterPageMetricsFieldsForPageChange(event.fields)); + shelf.pageChangeMetrics = { + pageFields: pageFields, + updatedEvents: updatedEvents, + }; +} +/// An allow list for the fields that can change in a `pageChange` event. +const allowedPageChangeKeys = new Set([ + "iAdAppStoreClientRequestId", + "iAdId", + "iAdSlotInfo", + "iAdOdmlSuccess", + "iAdEligible", + "iAdContainerId", + "iAdImpressionId", + "iAdMetadata", + "adamId", + "iAd", + "iAdPlacementId", + "iAdMissedOpportunityReason", + "hasiAdData", + "iAdTemplateType", + // These are required to merge with existing events + "eventType", + "impressionQueue", +]); +/** + * Filter some fields for the allowed content for a pageChange event. + * @param metricsFields The MetricsFields to filter. + */ +function filterPageMetricsFieldsForPageChange(metricsFields) { + Object.keys(metricsFields) + .filter((key) => !allowedPageChangeKeys.has(key)) + .forEach((key) => delete metricsFields[key]); +} +function pageEventFromPageData(objectGraph, pageInformation, fieldsModifier, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.iAdInfo) { + // Always copy this, even if no ad is present. + Object.assign(base, pageInformation.iAdInfo.pageFields); + } + // Include pre-order release date for page events. + if (serverData.isDefinedNonNull(pageInformation.offerReleaseDate)) { + base["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(pageInformation.offerReleaseDate); + } + const searchTermContext = pageInformation.searchTermContext; + if (searchTermContext) { + base["searchTerm"] = searchTermContext.term; + if (searchTermContext.suggestedTerm) { + base["searchSuggestedTerm"] = searchTermContext.suggestedTerm; + } + if (searchTermContext.correctedTerm) { + base["searchCorrectedTerm"] = searchTermContext.correctedTerm; + } + if (searchTermContext.originatingTerm) { + base["searchOriginatingTerm"] = searchTermContext.originatingTerm; + } + } + if (pageInformation.guidedSearch) { + Object.assign(base, pageInformation.guidedSearch); + } + const pageEvent = metricsBuilder.createMetricsPageData(objectGraph, false, (_a = pageInformation.isCrossfireReferralCandidate) !== null && _a !== void 0 ? _a : false, pageInformation.timingMetrics, base, isDefaultBrowser); + const hasAdverts = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.iAdIsPresent) !== null && _c !== void 0 ? _c : false; + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (hasAdverts && shouldIncludeAdRotationFields) { + pageEvent.includingFields.push("advertRotation"); + } + return pageEvent; +} +/** + * Creates a pageExitEvent which has all of the same fields as a pageEvent but with an eventType of pageExit. + * Noted this event is an event that is primarily meant to be used so the JS can get a hook into when a page is + * left so we can clear out associated referralContexts + */ +function pageExitEventFromPageData(objectGraph, pageInformation, fieldsModifier) { + const pageExitEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); + pageExitEvent.fields["eventType"] = "pageExit"; + return pageExitEvent; +} +function backEventFromPageData(objectGraph, pageInformation, fieldsModifier) { + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + return metricsBuilder.createMetricsBackClickData(objectGraph, base); +} +function pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier) { + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.searchTermContext) { + base["searchTerm"] = pageInformation.searchTermContext.term; + } + // Splice in page fields until all native clients have fix for + if (pageInformation.baseFields) { + Object.assign(base, pageInformation.baseFields); + } + return metricsBuilder.createMetricsPageRenderFields(objectGraph, pageInformation.timingMetrics, base); +} +function impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier) { + var _a, _b, _c; + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.searchTermContext) { + base["searchTerm"] = pageInformation.searchTermContext.term; + } + const impressionBase = objects.shallowCopyOf(base); + if (pageInformation.iAdInfo) { + Object.assign(impressionBase, pageInformation.iAdInfo.impressionsFields); + } + if (pageInformation.guidedSearch) { + Object.assign(impressionBase, pageInformation.guidedSearch); + } + // Regular Impressions + const shouldIncludeAdMetrics = serverData.isDefinedNonNull(pageInformation.iAdInfo); + const iAdEligibleForWindowCollection = serverData.isNullOrEmpty((_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.missedOpportunityReason) && objectGraph.client.isPad; + const shouldIncludeAdRotationFields = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.shouldIncludeAdRotationFields) !== null && _c !== void 0 ? _c : false; + const impressionsData = metricsBuilder.createMetricsImpressionsData(objectGraph, impressionBase, shouldIncludeAdMetrics && iAdEligibleForWindowCollection, shouldIncludeAdRotationFields, true); + const instruction = { + data: impressionsData, + invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit], + }; + // Fast Impressions + const impressionsArray = [instruction]; + if (shouldIncludeAdMetrics) { + const fastImpressions = metricsBuilder.createMetricsFastImpressionsData(objectGraph, impressionBase, pageInformation); + impressionsArray.push({ + data: fastImpressions, + invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit, PageInvocationPoint.timer], + }); + } + return impressionsArray; +} +function baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier) { + const base = {}; + // Add offer type for pre-orders (this should be added to everything, including impressions) + if (serverData.isDefinedNonNull(pageInformation.offerType)) { + base["offerType"] = pageInformation.offerType; + } + if (fieldsModifier !== undefined && base) { + fieldsModifier(base); + } + return base; +} +//# sourceMappingURL=page.js.map \ 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. + */ +// 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 diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js new file mode 100644 index 0000000..2e0a81f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js @@ -0,0 +1,370 @@ +import { isNothing } from "@jet/environment"; +import { asInterface, asJSONData, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, traverse, } from "../../foundation/json-parsing/server-data"; +/** + * This class is used as a singleton to track whether we should use the crossfire attribution from the JS purchase + * configuration, this is to remedy the following issue: + * + * rdar://96291594 ([Sydney][Regression][App Store][Clickstream][iTMS11] RefApp missing from page event - (iOS) Sydney20A312a) + * + * The flow of how this object is used is as follows: + * + * 1. Deeplink comes in and is routed to the product page controller, containing the crossfire referrer data + * 2. We build the product page model and pass the referrer data along + * 3. As we're building the page model for this product page, we check to see if we have valid referrer data. If we do + * we call into `MetricsReferralContext.beginReferralContextForProduct(appProductId)`, which will create store a unique + * referral context identifier for this product page. This identifier will be used to make sure we're only every dealing + * with the original product page. + * 4. When we create the pageEvent for this product page we modify all the event fields to include unique referral context id, + * using `MetricsReferralContext.addReferralContextToMetricsFieldsIfNecessary(metricsFields)` + * 5. The referrer data is then added to the purchase configuration for the product lockup's buy button, which comes back to us + * at time of purchase decoration + * 6. Later when the user taps ont the product buy button and we enter the purchase decoration flow, we check to see if we should + * skip adding the native referrer data to the purchase buy params, which would clear out the referrer data since the page change, + * event has cleared it out natively. We call `MetricsReferralContext.shouldUseJSReferralData` for this check. + * 7. If any page event comes in to the metrics event linter, we check to see if it has a referral context id that is not the + * current one, if it does we clear the current metrics referrer contents using `MetricsReferralContext.endReferralContextIfNecessaryForPageExitEvent(pageEvent)` + * 8. Additionally in the event linter we always call into `MetricsReferralContext.removeReferralContextInfoFromMetricsEvent(metricsFields)` + * which deletes the added referralContextId field. + */ +export class MetricsReferralContext { + static createSharedMetricsReferralContext(objectGraph) { + if (MetricsReferralContext.shared) { + return; + } + MetricsReferralContext.shared = new MetricsReferralContext(objectGraph); + } + /** + * Initializers + */ + /** + * @param objectGraph The object graph to use to determine if the referral context is needed. + */ + constructor(objectGraph) { + /** + * Properties + */ + /** + * Identifier denoting that we should use the extRefUrl2 and extRefApp2 from the original purchase configuration + * found in the purchase token, rather than using the native metrics data which would clear out the ref data + * due to pageChange events. + */ + this.currentReferral = null; + if (objectGraph.host.isiOS) { + this.isMetricsReferralContextRequired = true; + this.isEventDetailClickEventOverrideNecessary = !objectGraph.host.isOSAtLeast(16, 2, 0); + } + else if (objectGraph.host.isMac) { + this.isMetricsReferralContextRequired = objectGraph.host.isOSAtLeast(13, 0, 0); + this.isEventDetailClickEventOverrideNecessary = false; + } + else { + this.isMetricsReferralContextRequired = false; + this.isEventDetailClickEventOverrideNecessary = false; + } + } + /** + * Returns whether we should use the JS referral data or not. + */ + get shouldUseJSReferralData() { + return this.isMetricsReferralContextRequired && isDefinedNonNull(this.currentReferral); + } + /** + * Returns the current referral data for the active context + */ + get activeReferralData() { + if (!this.shouldUseJSReferralData) { + return null; + } + if (this.currentReferral === null || !this.currentReferral.isActive) { + return null; + } + return this.currentReferral.data; + } + /** + * Setting Referral Data + */ + /** + * Called when we get a deep link into the product page and need to make sure we track + * the referral data for this page. + * + * @param productId The id of the product the referral context is for. + * @param referrerData The referral data for this product page. + */ + setReferralDataForProduct(productId, referrerData) { + var _a, _b, _c; + if (!this.isMetricsReferralContextRequired || isNull(referrerData)) { + return; + } + const extRefApp2 = (_a = asString(referrerData, "app")) !== null && _a !== void 0 ? _a : null; + const extRefUrl2 = (_b = asString(referrerData, "externalUrl")) !== null && _b !== void 0 ? _b : null; + const kind = (_c = asInterface(referrerData, "kind")) !== null && _c !== void 0 ? _c : null; + this.currentReferral = { + id: `${productId}_${Date.now()}`, + data: { + extRefApp2, + extRefUrl2, + kind, + refUrl: null, + }, + isActive: false, + productPageExtensionInfo: null, + }; + } + /** + * Called when we are linting a page event, if this page event is for a product page extension, and + * we don't yet have an active crossfire referral context, we need to make sure we start one, since there + * should always be one in this case. The fact that there is not means that the pageChange event cleared it out + * natively. + * + * Additionally we're going to add the referral context id to the page event so we can track it later, and know + * when to end the referral context. + * + * @param pageEvent The page event that is currently being linted, so we can check to see if we're on a product page, + * in the product page extension. + */ + setReferralDataForProductPageExtensionIfNecessary(pageEvent) { + var _a, _b; + if (!this.isMetricsReferralContextRequired) { + return; + } + const productId = asString(pageEvent, "pageId"); + const refApp = asString(pageEvent, "refApp"); + if (!MetricsReferralContextUtil.isProductPageExtension(pageEvent) || + !MetricsReferralContextUtil.isValidPageEvent(pageEvent) || + isNull(productId) || + isNull(refApp)) { + return; + } + const extRefUrl = (_a = asString(pageEvent, "extRefUrl")) !== null && _a !== void 0 ? _a : null; + const refAppKindName = asString(pageEvent, "refAppType"); + let refAppKindContext; + switch (refAppKindName) { + case "trampoline": + refAppKindContext = asJSONData(traverse(pageEvent, "trampolineContext")); + break; + case "widget": + refAppKindContext = asJSONData(traverse(pageEvent, "widgetContext")); + break; + default: + refAppKindContext = {}; + } + const refUrl = (_b = asString(pageEvent, "refUrl")) !== null && _b !== void 0 ? _b : null; + this.currentReferral = { + id: `${productId}_${Date.now()}`, + data: { + extRefApp2: refApp, + extRefUrl2: extRefUrl, + refUrl: refUrl, + kind: { + name: refAppKindName, + context: refAppKindContext, + }, + }, + isActive: false, + productPageExtensionInfo: { + productId, + }, + }; + this.addReferralContextToMetricsFieldsIfNecessary(pageEvent); + } + /** + * Begin / End Metrics Referral Context + */ + /** + * Called when we get a deep link into the product page and need to make sure we track + * whether the referral data should be used from the js configuration. + * + * @param pageEvent Some page event that may be associated with the current referral context. + */ + beginReferralContextForPageIfNecessary(pageEvent) { + if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageEvent)) { + return; + } + if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageEvent)) { + return; + } + if (this.currentReferral !== null) { + this.currentReferral.isActive = true; + } + } + /** + * Called when we get a pageExit event after the page event for the current deeplinked + * product page, if there is one. This will reset the flag to use the native metrics. This should + * always be the next pageExit event after the page enter event + */ + endReferralContextIfNecessaryForPageEvent(pageExitEvent) { + if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageExitEvent)) { + return; + } + if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageExitEvent)) { + return; + } + this.currentReferral = null; + } + /** + * Setting / Clearing Page Fields + */ + /** + * Called when we're building the metrics events for a product page, this way we can tag the events with the current + * referral context id if there is one. + * + * @param pageMetricsFields The page event fields we can modify to track the current product page. + */ + addReferralContextToMetricsFieldsIfNecessary(pageMetricsFields) { + var _a; + if (!this.isMetricsReferralContextRequired) { + return; + } + pageMetricsFields[MetricsReferralContext.referralContextEventField] = (_a = this.currentReferral) === null || _a === void 0 ? void 0 : _a.id; + } + /** + * Called when linting our metrics events so we can make sure to remove the referral context id, so its not sent to the server + * + * @param metricsEvent The metrics event we're currently linting. + */ + removeReferralContextInfoFromMetricsEvent(metricsEvent) { + if (!this.isMetricsReferralContextRequired) { + return; + } + delete metricsEvent[MetricsReferralContext.referralContextEventField]; + } + /** + * Event Attribution + */ + /** + * If we have an active referral context, we need to make sure we add the referral data to the event. + * + * @param metricsEvent The metrics event we're currently linting. + */ + addReferralDataToEventIfNecessary(metricsEvent) { + if (isNull(this.activeReferralData)) { + return; + } + if (!MetricsReferralContextUtil.shouldAddReferralDataToEvent(metricsEvent)) { + return; + } + if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent) && + !this.isEventDetailClickEventOverrideNecessary) { + return; + } + if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent)) { + // Correct the `pageType` of this event for rdar://101302008 ([Sydney][App Store] [Clickstream][iTMS11] click event on EventDetails page from an app referral has incorrect pageType) + // Then continue on and apply referral data. + metricsEvent["pageType"] = "EventDetails"; + } + metricsEvent["refApp"] = this.activeReferralData.extRefApp2; + metricsEvent["extRefUrl"] = this.activeReferralData.extRefUrl2; + if (isDefinedNonNullNonEmpty(this.activeReferralData.refUrl)) { + metricsEvent["refUrl"] = this.activeReferralData.refUrl; + } + if (this.activeReferralData !== null && this.activeReferralData.kind !== null) { + metricsEvent["refAppType"] = this.activeReferralData.kind.name; + switch (metricsEvent["refAppType"]) { + case "trampoline": + metricsEvent["trampolineContext"] = this.activeReferralData.kind.context; + break; + case "widget": + metricsEvent["widgetContext"] = this.activeReferralData.kind.context; + break; + default: + break; + } + } + } +} +/** + * They event field to use on a page event so we can determine later if this page event belongs to + * the same deeplinked product page. + */ +MetricsReferralContext.referralContextEventField = "referralContextId"; +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class MetricsReferralContextUtil { + /** + * Check to see if the pageEvent is within the ProductPageExtension + * + * @param pageEvent The page event we're checking to see if its in an extension. + * @returns Whether "app" for this event is a valid type + */ + static isProductPageExtension(pageEvent) { + const app = asString(pageEvent, "app"); + return app === MetricsReferralContextUtil.productPageExtensionAppId; + } + /** + * Check to see if the current page event is for a product page. + * + * @param pageEvent The page event we're checking to see if its a product page. + * @returns Whether "pageType" for this event is a valid type + */ + static isValidPageEvent(pageEvent) { + const pageType = asString(pageEvent, "pageType"); + if (isNothing(pageType)) { + return false; + } + return MetricsReferralContextUtil.validPageEventTypes.has(pageType); + } + /** + * This method will check the `referralContextEventField` to see if it matches the current referral. + * + * @param referral The current metrics referral taken from the referral context + * @param event Some event to test whether there is an associated referral context. And if so + * if that referral context matches. + */ + static isReferralForEvent(referral, event) { + var _a; + if (isNull(referral)) { + return false; + } + const referralContextId = event[MetricsReferralContext.referralContextEventField]; + const productId = asString(event, "pageId"); + if (isDefinedNonNull(referralContextId)) { + return referralContextId === referral.id; + } + else if (MetricsReferralContextUtil.isProductPageExtension(event) && isDefinedNonNull(productId)) { + // For product page extensions we do not get a chance to add the referralContextId to the + // pageExit event so we need to check the productId to see if it matches the current referral. + return productId === ((_a = referral === null || referral === void 0 ? void 0 : referral.productPageExtensionInfo) === null || _a === void 0 ? void 0 : _a.productId); + } + else { + return false; + } + } + static shouldAddReferralDataToEvent(event) { + // Generally, we don't want to force referral data onto click events, but this is not true for In-App Events (IAE): + // rdar://101399254 ([Sydney] [App Store][Clickstream][iTMS11] Missing refApp and extRefURL on IAE Click Open events through App/Web Referrals) + // This only applies prior to SydneyC, as this was fixed natively there. + if (event.eventType === "click") { + return this.isEventDetailsClickEvent(event); + } + return true; + } + /** + * Check whether the event is for a click on an In-App Events (IAE) page. + * + * @param event The event we're checking to see if it's on a an IAE page. + */ + static isEventDetailsClickEvent(event) { + if (event.eventType !== "click") { + return false; + } + const location = event.location; + const currentLocation = location === null || location === void 0 ? void 0 : location[0]; + return isDefinedNonNull(currentLocation) && currentLocation.locationType === "EventDetails"; + } +} +/** + * The identifier for the product page extension in a metrics page event + */ +MetricsReferralContextUtil.productPageExtensionAppId = "com.apple.AppStore.ProductPageExtension"; +/** + * The identifier used for the pageType field of an app event page event. + */ +MetricsReferralContextUtil.eventDetailsPageType = "EventDetails"; +/** + * The set of valid page types for a product page, page event + */ +MetricsReferralContextUtil.validPageEventTypes = new Set([ + "Software", + "SoftwareBundle", + MetricsReferralContextUtil.eventDetailsPageType, +]); +//# sourceMappingURL=metrics-referral-context.js.map \ No newline at end of file -- cgit v1.2.3