summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js
diff options
context:
space:
mode:
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.js563
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