diff options
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js | 563 |
1 files changed, 563 insertions, 0 deletions
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. + } + /** + * <rdar://problem/64497066> 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 |
