summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js
blob: c03b7297e49960d20aea59b5e0f17c5cc6c53e94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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