import * as validation from "@jet/environment/json/validation"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaDataFetching from "../../foundation/media/data-fetching"; import * as mediaNetwork from "../../foundation/media/network"; import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common"; import { Parameters } from "../../foundation/network/url-constants"; export class PersonalizedData { } export async function recommendedAppsForUseCase(objectGraph, useCase, displayContext) { const displayContextLogString = displayContext === "shelf" ? "OnDeviceRecommendationsShelfController" : "OnDeviceRecommendationsPageController"; return await new Promise((resolve, reject) => { if (!objectGraph.host.isiOS) { const errorMessage = `${displayContextLogString}: On device personalization is only enabled on iOS devices.`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); return; } if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { const errorMessage = `${displayContextLogString}: User is currently not signed in.`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); return; } if (serverData.isNullOrEmpty(useCase)) { const errorMessage = `${displayContextLogString}: Missing valid useCase for ODP: ${useCase}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); return; } objectGraph.onDeviceRecommendationsManager .performRequest({ type: "fetchRecommendations", dsId: objectGraph.user.dsid, useCase: useCase, }) .then((recoResponse) => { const candidates = serverData.asArrayOrEmpty(recoResponse["candidates"]); const recoMetrics = serverData.asJSONData(recoResponse["metrics"]); if (serverData.isNullOrEmpty(candidates)) { const errorMessage = `${displayContextLogString}: ODP returned no candidate ids for useCase: ${useCase}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new NoODPCandidatesError(errorMessage)); return; } if (serverData.isNullOrEmpty(recoMetrics)) { const errorMessage = `${displayContextLogString}: ODP returned no metrics for useCase: ${useCase}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); return; } const candidatesData = []; for (const candidateId of candidates) { if (serverData.isDefinedNonNullNonEmpty(candidateId)) { candidatesData.push({ id: candidateId, type: "apps", }); } } // MAPI Request const mediaApiRequest = new mediaDataFetching.Request(objectGraph, candidatesData) .withFilter("apps:recommendable", "true") .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase); groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest); mediaNetwork .fetchData(objectGraph, mediaApiRequest) .then((recoDataContainer) => { resolve({ candidates: candidates, recoMetrics: recoMetrics, dataContainer: recoDataContainer, }); }) .catch((error) => { const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for candidates: ${candidates}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); }); }) .catch((error) => { const errorMessage = `${displayContextLogString}: Failed to perform ODP for useCase: ${useCase}, ${error}`; validation.unexpectedType("defaultValue", errorMessage, null); reject(new Error(errorMessage)); }); }); } /*** * Gives the list of app ids in the order in which they appear in the passed response * @param personalizedDataItems The data items to be processed * @return List of ordered app ids */ export function getOrderedAppIds(personalizedDataItems) { const seenAppIds = new Set(); const orderedAppIds = []; personalizedDataItems.forEach((dataItem, index) => { if (!seenAppIds.has(dataItem.appId)) { orderedAppIds.push(dataItem.appId); seenAppIds.add(dataItem.appId); } }); return orderedAppIds; } /*** * This function reranks events based on a calculated on-device score * that can factor in recency, frequency, or usage of an app * @param personalizedDataItems List of personalized data items * @param metricsData Metrics data with hyperparameter values to use * @return Personalized data items after reranking using on-device signals */ export function getUpdatedScoreAfterBoosting(personalizedDataItems, metricsData) { var _a; const weightParam = Number((_a = metricsData["weight_parameter"]) !== null && _a !== void 0 ? _a : 0.0); for (const personalizedDataItem of personalizedDataItems) { const serverScore = personalizedDataItem.score; const onDeviceScore = personalizedDataItem.onDeviceScore; personalizedDataItem.modifiedScore = weightParam * onDeviceScore + (1 - weightParam) * serverScore; } personalizedDataItems.sort((a, b) => { return b.modifiedScore - a.modifiedScore; }); return personalizedDataItems; } /*** * This function will diversify the personalized items on the basis of ordering of apps passed. * In case there are pinned items, we will remove them from diversification bucket and keep them in their position and add diversified items in order * This can lead to duplicates next to the pinned items. But the pinning and duplicate app events case scenario is rare so we avoid complicated logic here. * Explanation -> * If the original data items list contains appId in this order -> [1(a), 1(b), 2(a), 3(a), 2(b), 2(c), 3(b), 4(a), 1(c), 4(b)] * and the ordering of the apps used for relative ranking is -> [1, 2, 3, 4] * Then the updated ordering of the data items should look like -> [1(a), 2(a), 3(a), 4(a), 1(b), 2(b), 3(b), 4(b), 1(c), 2(c)] * @param personalizedDataItems List of personalized data items for diversification * @param orderedAppIds List of ordered app ids used for relative ordering between in-app events * @return The diversified list of data items. */ export function diversifyDataItems(personalizedDataItems, orderedAppIds) { const pinnedItems = personalizedDataItems.filter((item) => serverData.isDefinedNonNull(item.pinnedPosition)); personalizedDataItems = personalizedDataItems.filter((item) => serverData.isNull(item.pinnedPosition)); const appIdGroups = new Map(); // Insert the elements in a map in reverse order of their appearance in dataItems so that we can later use pop() instead of shift() // 1 -> [1(c), 1(b), 1(a)], 2 -> [2(c), 2(b), 2(a)], 3 -> [3(b), 3(a)], 4-> [4(b), 4(a)] personalizedDataItems.reverse(); personalizedDataItems.forEach((dataItem, index) => { if (dataItem.appId in appIdGroups) { appIdGroups[dataItem.appId].push(dataItem); } else { appIdGroups[dataItem.appId] = [dataItem]; } }); const diversifiedDataItems = []; // We find the max number of appItems for our appIds, to determine the iteration count const maxAppEventsForAppId = Math.max(...Object.values(appIdGroups).map((a) => a.length)); for (let index = 0; index < maxAppEventsForAppId; index++) { const reducedServerSideAppIdsOrdering = []; orderedAppIds.forEach((appId) => { if (appId in appIdGroups && appIdGroups[appId].length > 0) { diversifiedDataItems.push(appIdGroups[appId].pop()); if (appIdGroups[appId].length !== 0) { reducedServerSideAppIdsOrdering.push(appId); } } }); orderedAppIds = reducedServerSideAppIdsOrdering; } // Merge the pinned items and diversified items const finalizedResponse = new Array(pinnedItems.length + diversifiedDataItems.length); // Sort pinned items by pinned position to be able to handle the pinned items that exceed the final list length // For all the pinned items that have position greater than input list size, we just add them to end of the list pinnedItems.sort((a, b) => a.pinnedPosition - b.pinnedPosition); for (const dataItem of pinnedItems) { if (dataItem.pinnedPosition < finalizedResponse.length) { // Possible edgecase - Sanity check in case filtering reduces the length of items finalizedResponse[dataItem.pinnedPosition] = dataItem; } else { // Extremely rare case scenario: Push these items to diversifiedDataItemsList diversifiedDataItems.push(dataItem); } } diversifiedDataItems.reverse(); // To allow popping for first element fetch for (const [index, finalizedItem] of finalizedResponse.entries()) { if (serverData.isNull(finalizedItem) && diversifiedDataItems.length) { finalizedResponse[index] = diversifiedDataItems.pop(); } } return finalizedResponse; } export class NoODPCandidatesError extends Error { } //# sourceMappingURL=on-device-recommendations-common.js.map