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