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