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