|
(function() { |
|
const effectiveOrganicRequest = organicRequest || source === 'suggested apps'; |
|
const searchReenAdCap = 1; |
|
const displayReenAdCap = 4; |
|
const searchInstAdCap = 4; |
|
const d1FrequencyCap = 12; |
|
const d7FrequencyCap = 60; |
|
const clickRecencyPriorityWindowSeconds = 60; |
|
const clickRecencyPriorityWindowMs = clickRecencyPriorityWindowSeconds * 1000; |
|
const clickRecencyPriorityEnabled = source === 'suggested apps'; |
|
const installRecencyPriorityWindowSeconds = 300; |
|
const installRecencyPriorityWindowMs = installRecencyPriorityWindowSeconds * 1000; |
|
const installRecencyPriorityEnabled = source === 'suggested apps'; |
|
const softBoostEligibleRate = 0.25; |
|
const softBoostTargetSlotIndex = 3; |
|
const softBoostCandidateStartIndex = 4; |
|
const softBoostCandidateEndIndex = 14; |
|
const softBoostMinUsage7DSeconds = 30; |
|
const softBoostDisplacementRecencyMs = 1000 * 60 * 60 * 2; |
|
const d1delta = 1000 * 60 * 60 * 24; |
|
const d3delta = 1000 * 60 * 60 * 24 * 3; |
|
const d5delta = 1000 * 60 * 60 * 24 * 5; |
|
const d7delta = 1000 * 60 * 60 * 24 * 7; |
|
const d14delta = 1000 * 60 * 60 * 24 * 14; |
|
const d30delta = 1000 * 60 * 60 * 24 * 30; |
|
const y3delta = 1000 * 60 * 60 * 24 * 365 * 3; |
|
let importantAppScores = { |
|
"com.android.dialer": 0.3, |
|
"com.samsung.android.dialer": 0.3, |
|
"com.google.android.dialer": 0.3, |
|
"com.android.messaging": 0.2, |
|
"com.samsung.android.messaging": 0.2, |
|
"com.google.android.messaging": 0.2, |
|
"com.android.chrome": 0.4, |
|
"com.google.android.youtube": 0.2, |
|
"com.sec.android.app.camera": 0.3, |
|
"com.android.settings": 0.4, |
|
"com.android.vending": 0.4, |
|
"com.google.android.apps.maps": 0.2 |
|
}; |
|
let penalizedPackages = ["com.motorola.mobiledesktop"] |
|
let blockedDualAppPackages = ["com.google.android.googlequicksearchbox"]; |
|
let loggingObject = { |
|
ad: ads.length, |
|
ap: Object.keys(apps).length, |
|
t: currentTime, |
|
u: usageStatsEnabled, |
|
s: Object.keys(shortcuts).length |
|
}; |
|
let selectedAdIds = []; |
|
let selectedPackages = []; |
|
|
|
const hasBeenSelected = (ad) => { |
|
return selectedPackages.includes(ad.packageName) || selectedAdIds.includes(ad.id); |
|
}; |
|
|
|
const getAppVariantKey = (app) => { |
|
if (!app || !app.packageName) { |
|
return null; |
|
} |
|
return `${app.packageName}|${app.component || ''}|${app.userId || ''}`; |
|
}; |
|
|
|
const getUserHandleNumber = (handle) => { |
|
if (typeof handle !== 'string') { |
|
return Number.NEGATIVE_INFINITY; |
|
} |
|
const match = handle.match(/\{(-?\d+)\}/); |
|
if (!match) { |
|
return Number.NEGATIVE_INFINITY; |
|
} |
|
const parsed = parseInt(match[1], 10); |
|
return isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed; |
|
}; |
|
|
|
const isInstalledAppVariant = (app) => { |
|
if (!app) { |
|
return false; |
|
} |
|
return !(app.uninstalledAt > app.installedAt); |
|
}; |
|
|
|
const isCurrentUserAppVariant = (app) => { |
|
return !!app && (!app.userId || app.userId === userId); |
|
}; |
|
|
|
const pickPreferredAppVariant = (candidateApps) => { |
|
if (!candidateApps || candidateApps.length === 0) { |
|
return null; |
|
} |
|
|
|
let bestApp = null; |
|
for (let i = 0; i < candidateApps.length; i += 1) { |
|
const candidate = candidateApps[i]; |
|
if (!candidate) { |
|
continue; |
|
} |
|
if (!bestApp) { |
|
bestApp = candidate; |
|
continue; |
|
} |
|
|
|
const candidateInstalled = isInstalledAppVariant(candidate); |
|
const bestInstalled = isInstalledAppVariant(bestApp); |
|
if (candidateInstalled !== bestInstalled) { |
|
if (candidateInstalled) { |
|
bestApp = candidate; |
|
} |
|
continue; |
|
} |
|
|
|
const candidateCurrent = isCurrentUserAppVariant(candidate); |
|
const bestCurrent = isCurrentUserAppVariant(bestApp); |
|
if (candidateCurrent !== bestCurrent) { |
|
if (candidateCurrent) { |
|
bestApp = candidate; |
|
} |
|
continue; |
|
} |
|
|
|
const candidateUpdatedAt = typeof candidate.updatedAt === 'number' ? candidate.updatedAt : 0; |
|
const bestUpdatedAt = typeof bestApp.updatedAt === 'number' ? bestApp.updatedAt : 0; |
|
if (candidateUpdatedAt !== bestUpdatedAt) { |
|
if (candidateUpdatedAt > bestUpdatedAt) { |
|
bestApp = candidate; |
|
} |
|
continue; |
|
} |
|
|
|
const candidateInstalledAt = typeof candidate.installedAt === 'number' ? candidate.installedAt : 0; |
|
const bestInstalledAt = typeof bestApp.installedAt === 'number' ? bestApp.installedAt : 0; |
|
if (candidateInstalledAt !== bestInstalledAt) { |
|
if (candidateInstalledAt > bestInstalledAt) { |
|
bestApp = candidate; |
|
} |
|
continue; |
|
} |
|
|
|
const candidateHandleNumber = getUserHandleNumber(candidate.userId); |
|
const bestHandleNumber = getUserHandleNumber(bestApp.userId); |
|
if (candidateHandleNumber !== bestHandleNumber) { |
|
if (candidateHandleNumber > bestHandleNumber) { |
|
bestApp = candidate; |
|
} |
|
} |
|
} |
|
|
|
return bestApp; |
|
}; |
|
|
|
const parseStringList = (value) => { |
|
if (typeof value !== 'string') { |
|
return []; |
|
} |
|
const trimmed = value.trim(); |
|
if (!trimmed) { |
|
return []; |
|
} |
|
if (trimmed[0] === '[') { |
|
try { |
|
const parsed = JSON.parse(trimmed); |
|
if (Array.isArray(parsed)) { |
|
return parsed.map((item) => String(item).trim()).filter(Boolean); |
|
} |
|
} catch (error) { |
|
// fall through |
|
} |
|
} |
|
return trimmed |
|
.replace(/[\[\]]/g, '') |
|
.split(',') |
|
.map((item) => item.trim().replace(/^['"]|['"]$/g, '')) |
|
.filter(Boolean); |
|
}; |
|
|
|
const deStringifyTargetingMetadata = (ad) => { |
|
if (!ad) { |
|
return; |
|
} |
|
if (ad.targetingMetadata && typeof ad.targetingMetadata === 'string') { |
|
try { |
|
ad.targetingMetadata = JSON.parse(ad.targetingMetadata); |
|
} catch (error) { |
|
ad.targetingMetadata = {}; |
|
return; |
|
} |
|
} |
|
if (!ad.targetingMetadata || typeof ad.targetingMetadata !== 'object' || Array.isArray(ad.targetingMetadata)) { |
|
ad.targetingMetadata = {}; |
|
return; |
|
} |
|
if (ad.targetingMetadata.aliases && typeof ad.targetingMetadata.aliases === 'string') { |
|
ad.targetingMetadata.aliases = parseStringList(ad.targetingMetadata.aliases); |
|
} |
|
if (ad.targetingMetadata.competitors && typeof ad.targetingMetadata.competitors === 'string') { |
|
ad.targetingMetadata.competitors = parseStringList(ad.targetingMetadata.competitors); |
|
} |
|
}; |
|
|
|
const getReenBlackoutWindowMs = (ad) => { |
|
if (!ad || ad.type !== 'reen') { |
|
return 0; |
|
} |
|
deStringifyTargetingMetadata(ad); |
|
let blackoutHours = ad.targetingMetadata ? ad.targetingMetadata.reenBlackoutHours : null; |
|
if (blackoutHours === undefined || blackoutHours === null || blackoutHours === '') { |
|
return 0; |
|
} |
|
if (typeof blackoutHours === 'string') { |
|
blackoutHours = parseFloat(blackoutHours); |
|
} |
|
if (typeof blackoutHours !== 'number' || !isFinite(blackoutHours) || blackoutHours <= 0) { |
|
return 0; |
|
} |
|
return blackoutHours * 60 * 60 * 1000; |
|
}; |
|
|
|
const isFrequencyCapped = (ad) => { |
|
if (ad.type === 'reen') { |
|
return false; |
|
} |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (currEvents) { |
|
let d1Impressions = 0; |
|
let d7Impressions = 0; |
|
|
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
const event = currEvents[i]; |
|
if (event.event === 'view' && event.completedAt > (currentTime - d7delta)) { |
|
if (event.placement && event.placement.indexOf('search') > -1) { |
|
continue; |
|
} |
|
|
|
d7Impressions += 1; |
|
|
|
if (event.completedAt > (currentTime - d1delta)) { |
|
d1Impressions += 1; |
|
} |
|
|
|
if (d1Impressions > d1FrequencyCap || d7Impressions > d7FrequencyCap) { |
|
return true; |
|
} |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
|
|
const hasRecentVTA = (ad) => { |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (currEvents) { |
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
if (currEvents[i].event === 'vta' && currEvents[i].completedAt > (currentTime - d7delta)) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
|
|
const isConvertedAd = (ad, app) => { |
|
if (!ad) { |
|
return false; |
|
} |
|
if (ad.type === 'reen') { |
|
const blackoutWindowMs = getReenBlackoutWindowMs(ad); |
|
if (!blackoutWindowMs) { |
|
return false; |
|
} |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (currEvents) { |
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
if (currEvents[i].event === 'click' && |
|
currEvents[i].completedAt > (currentTime - blackoutWindowMs)) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (currEvents) { |
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
if ((currEvents[i].event === 'click' || currEvents[i].event === 'vta') && currEvents[i].completedAt > (currentTime - d3delta)) { |
|
if ((ad.type === 'inst' || ad.type === 'acti') && |
|
app && app.timeUsedInLast3Y > 0 && |
|
(app.lastTimeInForeground - currEvents[i].completedAt) > 0) { |
|
return true; |
|
// } else if ((ad.type === 'web' || ad.type === 'reen') && |
|
// (currentTime - currEvents[i].completedAt) < d3delta) { |
|
// return true; |
|
} |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
|
|
const isSoftBoostConvertedAd = (ad) => { |
|
if (!ad || ad.type !== 'reen') { |
|
return false; |
|
} |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (currEvents) { |
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
if (currEvents[i].event === 'click' && |
|
currEvents[i].completedAt > (currentTime - d1delta)) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
|
|
const hasAnyUsage = (app) => { |
|
if (!app) { |
|
return false; |
|
} |
|
return (app.timeUsedInLast1D || 0) > 0 || |
|
(app.timeUsedInLast3D || 0) > 0 || |
|
(app.timeUsedInLast7D || 0) > 0 || |
|
(app.timeUsedInLast3Y || 0) > 0; |
|
}; |
|
|
|
const isInstEligibleApp = (app) => { |
|
if (!app) { |
|
return false; |
|
} |
|
return !hasAnyUsage(app) && |
|
(app.lastTimeInForeground || 0) <= 0 && |
|
(app.lastUsedAt || 0) <= 0; |
|
}; |
|
|
|
const getInstalledAdPriority = (ad, app, options) => { |
|
if (!ad || !app) { |
|
return Number.POSITIVE_INFINITY; |
|
} |
|
|
|
const allowedTypes = options && options.allowedTypes ? options.allowedTypes : null; |
|
if (allowedTypes && !allowedTypes.includes(ad.type)) { |
|
return Number.POSITIVE_INFINITY; |
|
} |
|
|
|
const isConverted = options && options.useSoftBoostConversion ? |
|
isSoftBoostConvertedAd(ad) : |
|
isConvertedAd(ad, app); |
|
if (isConverted) { |
|
return Number.POSITIVE_INFINITY; |
|
} |
|
|
|
const appHasUsage = hasAnyUsage(app); |
|
const hasNoUsage = !appHasUsage; |
|
const recentInstallWithUsage = (currentTime - app.installedAt) < d1delta && appHasUsage; |
|
if (ad.type === 'inst') { |
|
return isInstEligibleApp(app) ? 0 : Number.POSITIVE_INFINITY; |
|
} |
|
|
|
if (ad.type === 'acti') { |
|
const distantInstallWithUsage = (currentTime - app.installedAt) < d30delta && appHasUsage; |
|
return hasNoUsage || recentInstallWithUsage || distantInstallWithUsage ? 1 : Number.POSITIVE_INFINITY; |
|
} |
|
|
|
if (ad.type === 'reen') { |
|
return 2; |
|
} |
|
|
|
if (ad.type === 'web' && options && options.includeWeb && webAdsEnabled) { |
|
return 3; |
|
} |
|
|
|
return Number.POSITIVE_INFINITY; |
|
}; |
|
|
|
const getBestInstalledAdForApp = (app, adPool, options) => { |
|
if (!app) { |
|
return null; |
|
} |
|
|
|
const candidateAds = adPool || ads; |
|
let bestAd = null; |
|
let bestPriority = Number.POSITIVE_INFINITY; |
|
for (let i = 0; i < candidateAds.length; i += 1) { |
|
const candidateAd = candidateAds[i]; |
|
if (!candidateAd || candidateAd.packageName !== app.packageName) { |
|
continue; |
|
} |
|
const priority = getInstalledAdPriority(candidateAd, app, options); |
|
if (priority < bestPriority) { |
|
bestAd = candidateAd; |
|
bestPriority = priority; |
|
if (priority === 0) { |
|
break; |
|
} |
|
} |
|
} |
|
|
|
return bestAd; |
|
}; |
|
|
|
const compareInstalledAdsWithTypePriority = (a, b, metricDiff) => { |
|
if (metricDiff !== 0) { |
|
return metricDiff; |
|
} |
|
|
|
if (a && b && a.packageName && a.packageName === b.packageName) { |
|
const app = apps[a.packageName]; |
|
const aPriority = getInstalledAdPriority(a, app); |
|
const bPriority = getInstalledAdPriority(b, app); |
|
if (aPriority !== bPriority) { |
|
return aPriority - bPriority; |
|
} |
|
} |
|
|
|
return 0; |
|
}; |
|
|
|
const hasSoftBoostRecentUsage = (app) => { |
|
if (!app) { |
|
return false; |
|
} |
|
return app.timeUsedInLast7D >= softBoostMinUsage7DSeconds && |
|
app.lastTimeInForeground > (currentTime - d7delta); |
|
}; |
|
|
|
const isVeryRecentlyUsedApp = (app) => { |
|
if (!app) { |
|
return false; |
|
} |
|
return app.lastTimeInForeground > (currentTime - softBoostDisplacementRecencyMs); |
|
}; |
|
|
|
const getSuggestedAppsSoftBoostState = (visibleApps, organicCount) => { |
|
if (source !== 'suggested apps' || |
|
!organicAdsEnabled || |
|
organicAdsBoostEnabled || |
|
organicCount < 5 || |
|
visibleApps.length <= softBoostTargetSlotIndex) { |
|
return null; |
|
} |
|
|
|
const displacedApp = visibleApps[softBoostTargetSlotIndex]; |
|
if (!displacedApp || |
|
importantAppScores[displacedApp.packageName] || |
|
isVeryRecentlyUsedApp(displacedApp)) { |
|
return null; |
|
} |
|
|
|
let visiblePackages = []; |
|
for (let i = 0; i < Math.min(organicCount, visibleApps.length); i += 1) { |
|
visiblePackages.push(visibleApps[i].packageName); |
|
} |
|
|
|
for (let i = softBoostCandidateStartIndex; i <= softBoostCandidateEndIndex && i < visibleApps.length; i += 1) { |
|
let candidateApp = visibleApps[i]; |
|
if (!candidateApp || |
|
visiblePackages.includes(candidateApp.packageName) || |
|
!hasSoftBoostRecentUsage(candidateApp)) { |
|
continue; |
|
} |
|
|
|
let candidateAd = getBestInstalledAdForApp(candidateApp, ads, { |
|
allowedTypes: ['reen'], |
|
useSoftBoostConversion: true |
|
}); |
|
if (!candidateAd || |
|
candidateAd.type !== 'reen' || |
|
isSoftBoostConvertedAd(candidateAd)) { |
|
continue; |
|
} |
|
|
|
return { |
|
candidateIndex: i, |
|
candidatePackage: candidateApp.packageName, |
|
displacedPackage: displacedApp.packageName, |
|
scoreGap: (displacedApp.rankingScore || 0) - (candidateApp.rankingScore || 0) |
|
}; |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
const recentInstallNoUsage = (ad, app) => { |
|
if ((currentTime - app.installedAt) < d7delta && |
|
app.timeUsedInLast7D === 0 && |
|
((ad.type === 'inst' && isInstEligibleApp(app)) || ad.type === 'acti' || ad.type === 'reen')) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const recentInstallUsage = (ad, app) => { |
|
if ((currentTime - app.installedAt) < d14delta && |
|
(ad.type === 'acti' || ad.type === 'reen')) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const recentUsage = (ad, app) => { |
|
if (((currentTime - app.installedAt) < d14delta || app.timeUsedInLast7D > 0) && |
|
ad.type === 'reen') { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const distantInstallNoUsage = (ad, app) => { |
|
if ((currentTime - app.installedAt) < d30delta && |
|
app.timeUsedInLast3Y === 0 && |
|
((ad.type === 'inst' && isInstEligibleApp(app)) || ad.type === 'acti' || ad.type === 'reen')) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const distantInstallUsage = (ad, app) => { |
|
if ((currentTime - app.installedAt) < d30delta && |
|
(ad.type === 'acti' || ad.type === 'reen')) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const competitorInstall = (ad) => { |
|
if (ad.targetingMetadata && |
|
ad.targetingMetadata.competitors && |
|
ad.targetingMetadata.competitors.length > 0) { |
|
for (let i = 0; i < ad.targetingMetadata.competitors.length; i += 1) { |
|
const competitorPackage = ad.targetingMetadata.competitors[i]; |
|
const app = apps[competitorPackage]; |
|
if (app && (currentTime - app.installedAt) < d30delta) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
const uninstalledApp = (app) => { |
|
if (app.uninstalledAt > app.installedAt) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
|
|
const parseMotoBoostRule = (ad) => { |
|
if (!ad || ad.type !== 'reen') { |
|
return null; |
|
} |
|
deStringifyTargetingMetadata(ad); |
|
const rawRule = ad.targetingMetadata ? ad.targetingMetadata.mb : null; |
|
if (typeof rawRule !== 'string' || !rawRule.trim()) { |
|
return null; |
|
} |
|
const parts = rawRule.split('|').map((part) => parseInt(part.trim(), 10)); |
|
if (parts.length !== 3 || parts.some((part) => isNaN(part))) { |
|
return null; |
|
} |
|
const slot = Math.max(1, parts[0]); |
|
const interval = Math.max(1, parts[1]); |
|
const weeklyCap = Math.max(0, parts[2]); |
|
return { slot, interval, weeklyCap }; |
|
}; |
|
|
|
const getMotoBoostWeeklyViews = (ad) => { |
|
const currEvents = events ? events[ad.packageName] : null; |
|
if (!currEvents) { |
|
return 0; |
|
} |
|
let views = 0; |
|
for (let i = 0; i < currEvents.length; i += 1) { |
|
const event = currEvents[i]; |
|
if (event.event === 'view' && |
|
event.completedAt > (currentTime - d7delta) && |
|
(!event.resId || event.resId === ad.id)) { |
|
views += 1; |
|
} |
|
} |
|
return views; |
|
}; |
|
|
|
const passesMotoBoostInterval = (ad, rule) => { |
|
return rule.interval <= 1 || Math.random() < (1 / rule.interval); |
|
}; |
|
|
|
const isMotoBoostEligible = (ad, rule, allowAlreadySelected) => { |
|
if (source !== 'suggested apps' || !effectiveOrganicRequest) { |
|
return false; |
|
} |
|
if (!ad || !rule) { |
|
return false; |
|
} |
|
if (!allowAlreadySelected && hasBeenSelected(ad)) { |
|
return false; |
|
} |
|
const app = apps[ad.packageName]; |
|
if (!app || uninstalledApp(app)) { |
|
return false; |
|
} |
|
if (blacklist && blacklist.includes(ad.packageName)) { |
|
return false; |
|
} |
|
if (!passesMotoBoostInterval(ad, rule)) { |
|
return false; |
|
} |
|
if (rule.weeklyCap > 0 && getMotoBoostWeeklyViews(ad) >= rule.weeklyCap) { |
|
return false; |
|
} |
|
return !isConvertedAd(ad, app); |
|
}; |
|
|
|
const tagMotoBoostAd = (ad, rule, mode) => { |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
ad.log = { |
|
sel: mode, |
|
mb: '1', |
|
mbS: rule.slot, |
|
mbI: rule.interval, |
|
mbW: rule.weeklyCap |
|
}; |
|
}; |
|
|
|
const rememberSelectedAd = (ad) => { |
|
if (!selectedAdIds.includes(ad.id)) { |
|
selectedAdIds.push(ad.id); |
|
} |
|
if (!selectedPackages.includes(ad.packageName)) { |
|
selectedPackages.push(ad.packageName); |
|
} |
|
}; |
|
|
|
const applyMotoCampaignBoost = (results, resultCount) => { |
|
if (source !== 'suggested apps') { |
|
return results; |
|
} |
|
|
|
for (let i = 0; i < ads.length; i += 1) { |
|
const ad = ads[i]; |
|
const rule = parseMotoBoostRule(ad); |
|
let matchingResult = null; |
|
for (let j = 0; j < results.length; j += 1) { |
|
const result = results[j]; |
|
if (!result || !result.app || result.app.packageName !== ad.packageName) { |
|
continue; |
|
} |
|
matchingResult = result; |
|
break; |
|
} |
|
|
|
const allowAlreadySelected = !!(matchingResult && matchingResult.ad && matchingResult.ad.id === ad.id); |
|
if (!isMotoBoostEligible(ad, rule, allowAlreadySelected)) { |
|
continue; |
|
} |
|
|
|
if (matchingResult) { |
|
if (matchingResult.ad && matchingResult.ad.id !== ad.id) { |
|
return results; |
|
} |
|
tagMotoBoostAd(ad, rule, 'moto-boost-organic'); |
|
matchingResult.ad = ad; |
|
rememberSelectedAd(ad); |
|
return results; |
|
} |
|
|
|
const slotIndex = rule.slot - 1; |
|
if (slotIndex < 0 || slotIndex >= resultCount) { |
|
return results; |
|
} |
|
if (results[slotIndex] && results[slotIndex].ad) { |
|
return results; |
|
} |
|
|
|
tagMotoBoostAd(ad, rule, 'moto-boost-slot'); |
|
results.splice(slotIndex, 0, { |
|
app: apps[ad.packageName], |
|
ad |
|
}); |
|
while (results.length > resultCount) { |
|
results.pop(); |
|
} |
|
rememberSelectedAd(ad); |
|
return results; |
|
} |
|
|
|
return results; |
|
}; |
|
|
|
const distantUsage = (ad, app) => { |
|
if (app.timeUsedInLast3Y > 0 && |
|
ad.type === 'reen') { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
const competitiveUsage = (ad) => { |
|
if (ad.targetingMetadata && |
|
ad.targetingMetadata.competitors && |
|
ad.targetingMetadata.competitors.length > 0) { |
|
for (let i = 0; i < ad.targetingMetadata.competitors.length; i += 1) { |
|
let competitorPackage = ad.targetingMetadata.competitors[i]; |
|
let app = apps[competitorPackage]; |
|
if (app && app.timeUsedInLast3Y > 0) { |
|
return true; |
|
} |
|
} |
|
} |
|
return false; |
|
}; |
|
const appInstalled = (ad, app) => { |
|
if (app.installedAt > app.uninstalledAt && |
|
ad.type === 'reen') { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
|
|
const rankInstalledAds = (adsToRank) => { |
|
let appList = []; |
|
let seenPackages = []; |
|
for (let i = 0; i < adsToRank.length; i += 1) { |
|
let ad = adsToRank[i]; |
|
if (!ad || seenPackages.includes(ad.packageName)) { |
|
continue; |
|
} |
|
let app = apps[ad.packageName]; |
|
if (!app) { |
|
continue; |
|
} |
|
seenPackages.push(ad.packageName); |
|
appList.push(app); |
|
} |
|
let rankedAds = []; |
|
let rankedApps = rankInstalledApps(appList); |
|
for (let i = 0; i < rankedApps.length; i += 1) { |
|
let app = rankedApps[i]; |
|
let ad = getBestInstalledAdForApp(app, adsToRank); |
|
if (ad) { |
|
rankedAds.push(ad); |
|
} |
|
} |
|
return rankedAds; |
|
}; |
|
|
|
const rankInstalledApps = (appsToRank) => { |
|
let rankedApps = []; |
|
let max1DLog = Math.log(1 + d1delta); |
|
let max3DLog = Math.log(1 + d3delta); |
|
let max7DLog = Math.log(1 + d7delta); |
|
let max3YLog = Math.log(1 + y3delta); |
|
const timeOfDayLookbackMs = d7delta; |
|
const timeOfDayWindowMinutes = 90; |
|
const timeOfDayMinScore = 0.25; |
|
const timeOfDayMaxScore = 3.0; |
|
const timeOfDayWeight = 0.6; |
|
const timeOfDayDate = new Date(currentTime); |
|
const timeOfDayNowMinutes = (timeOfDayDate.getHours() * 60) + timeOfDayDate.getMinutes(); |
|
const timeOfDayInvWindow = 1 / timeOfDayWindowMinutes; |
|
const timeOfDayInvLookback = 1 / timeOfDayLookbackMs; |
|
const timeOfDayCutoffMs = currentTime - timeOfDayLookbackMs; |
|
for (let i = 0; i < appsToRank.length; i += 1) { |
|
let app = appsToRank[i]; |
|
if (app && !uninstalledApp(app)) { |
|
let pkg = app.packageName; |
|
if (pkg === currentPackage) { |
|
continue; |
|
} |
|
|
|
let usedVeryRecentlyScore = 0; |
|
if (app.lastTimeInForeground > 0) { |
|
let timeSinceLastUseSeconds = (currentTime - app.lastTimeInForeground) / 1000; |
|
|
|
if (timeSinceLastUseSeconds <= 1800) { |
|
usedVeryRecentlyScore = 4.0 - (1.5 * timeSinceLastUseSeconds / 1800); |
|
} else if (timeSinceLastUseSeconds <= 7200) { |
|
usedVeryRecentlyScore = 2.5 - (1.5 * (timeSinceLastUseSeconds - 1800) / 5400); |
|
} else if (timeSinceLastUseSeconds <= 21600) { |
|
usedVeryRecentlyScore = 1.0 - (0.5 * (timeSinceLastUseSeconds - 7200) / 14400); |
|
} else { |
|
usedVeryRecentlyScore = Math.max(0.5 - (timeSinceLastUseSeconds - 21600) / 72000, 0); |
|
} |
|
} |
|
|
|
let usageScore = 0.4 * (Math.log(1 + 1000 * app.timeUsedInLast1D) / max1DLog) + |
|
0.3 * (Math.log(1 + 1000 * app.timeUsedInLast3D) / max3DLog) + |
|
0.2 * (Math.log(1 + 1000 * app.timeUsedInLast7D) / max7DLog) + |
|
0.1 * (Math.log(1 + 1000 * app.timeUsedInLast3Y) / max3YLog); |
|
|
|
if (penalizedPackages.includes(pkg)) { |
|
usageScore = usageScore * 0.05; |
|
} |
|
|
|
let engagementScore = 0; |
|
let mostRecentClickAt = 0; |
|
let timeOfDayScore = 0; |
|
if (events && events[pkg]) { |
|
const eventsForPackage = events[pkg]; |
|
const len = eventsForPackage.length; |
|
if (len > 0) { |
|
const first = eventsForPackage[0]; |
|
const last = eventsForPackage[len - 1]; |
|
const firstTs = first && typeof first.completedAt === 'number' ? first.completedAt : 0; |
|
const lastTs = last && typeof last.completedAt === 'number' ? last.completedAt : 0; |
|
const ascending = firstTs <= lastTs; |
|
const appId = app.id; |
|
let hasEngagement = false; |
|
const start = ascending ? len - 1 : 0; |
|
const end = ascending ? -1 : len; |
|
const step = ascending ? -1 : 1; |
|
for (let j = start; j !== end; j += step) { |
|
const event = eventsForPackage[j]; |
|
if (!event || event.event !== 'click' || typeof event.completedAt !== 'number') { |
|
continue; |
|
} |
|
if (event.resId === undefined || appId === undefined || event.resId !== appId) { |
|
continue; |
|
} |
|
const ts = event.completedAt; |
|
const ageMs = currentTime - ts; |
|
if (ageMs < 0) { |
|
continue; |
|
} |
|
if (!hasEngagement) { |
|
const timeSinceEventSeconds = ageMs / 1000; |
|
mostRecentClickAt = ts; |
|
if (timeSinceEventSeconds <= 300) { |
|
engagementScore = 10.0 - (2.0 * timeSinceEventSeconds / 300); |
|
} else if (timeSinceEventSeconds <= 1800) { |
|
engagementScore = 8.0 - (3.0 * (timeSinceEventSeconds - 300) / 1500); |
|
} else if (timeSinceEventSeconds <= 3600) { |
|
engagementScore = 5.0 - (2.0 * (timeSinceEventSeconds - 1800) / 1800); |
|
} else if (timeSinceEventSeconds <= 21600) { |
|
engagementScore = 3.0 - (1.0 * (timeSinceEventSeconds - 3600) / 18000); |
|
} else if (timeSinceEventSeconds <= 86400) { |
|
engagementScore = 2.0 - (1.0 * (timeSinceEventSeconds - 21600) / 64800); |
|
} else { |
|
const timeSinceEventDays = timeSinceEventSeconds / 86400; |
|
engagementScore = Math.exp(-1.5 * timeSinceEventDays); |
|
} |
|
hasEngagement = true; |
|
} |
|
if (ts < timeOfDayCutoffMs) { |
|
continue; |
|
} |
|
timeOfDayDate.setTime(ts); |
|
const eventMinutes = (timeOfDayDate.getHours() * 60) + timeOfDayDate.getMinutes(); |
|
let diff = eventMinutes - timeOfDayNowMinutes; |
|
if (diff < 0) { |
|
diff = -diff; |
|
} |
|
if (diff > 720) { |
|
diff = 1440 - diff; |
|
} |
|
if (diff > timeOfDayWindowMinutes) { |
|
continue; |
|
} |
|
const timeWeight = 1 - (diff * timeOfDayInvWindow); |
|
const recencyWeight = 1 - (ageMs * timeOfDayInvLookback); |
|
timeOfDayScore += timeWeight * recencyWeight; |
|
} |
|
} |
|
} |
|
|
|
let timeSinceInstallSeconds = (currentTime - app.installedAt) / 1000; |
|
let timeSinceInstallDays = timeSinceInstallSeconds / 86400; |
|
|
|
let usageEngagementFactor = 1.0; |
|
if (timeSinceInstallSeconds > 21600) { |
|
let totalRecentUsage = app.timeUsedInLast1D + app.timeUsedInLast3D + app.timeUsedInLast7D; |
|
let expectedUsageThreshold = Math.max(300, timeSinceInstallDays * 600); |
|
|
|
if (totalRecentUsage === 0) { |
|
usageEngagementFactor = Math.max(0.1, Math.exp(-0.5 * timeSinceInstallDays)); |
|
} else if (totalRecentUsage < expectedUsageThreshold * 0.3) { |
|
usageEngagementFactor = Math.max(0.3, Math.exp(-0.2 * timeSinceInstallDays)); |
|
} else if (totalRecentUsage < expectedUsageThreshold) { |
|
usageEngagementFactor = Math.max(0.6, Math.exp(-0.1 * timeSinceInstallDays)); |
|
} |
|
} |
|
|
|
let baseInstallScore = 0; |
|
if (timeSinceInstallSeconds <= 3600) { |
|
baseInstallScore = 12.0 - (2.0 * timeSinceInstallSeconds / 3600); |
|
} else if (timeSinceInstallSeconds <= 21600) { |
|
baseInstallScore = 2.0 - (1.0 * (timeSinceInstallSeconds - 3600) / 18000); |
|
} else if (timeSinceInstallSeconds <= 86400) { |
|
baseInstallScore = 1.0 - (0.5 * (timeSinceInstallSeconds - 21600) / 64800); |
|
} else if (timeSinceInstallDays <= 3) { |
|
baseInstallScore = 0.5 - (0.2 * (timeSinceInstallDays - 1) / 2); |
|
} else if (timeSinceInstallDays <= 7) { |
|
baseInstallScore = 0.3 - (0.1 * (timeSinceInstallDays - 3) / 4); |
|
} else if (timeSinceInstallDays <= 14) { |
|
baseInstallScore = 0.2 - (0.1 * (timeSinceInstallDays - 7) / 7); |
|
} else { |
|
baseInstallScore = 0.1 * Math.exp(-0.05 * (timeSinceInstallDays - 14)); |
|
} |
|
|
|
let installScore = baseInstallScore * usageEngagementFactor; |
|
|
|
let importantPackageScore = 0; |
|
if (importantAppScores[pkg]) { |
|
importantPackageScore = importantAppScores[pkg]; |
|
} |
|
|
|
let engagementMultiplier = 1.0; |
|
if (engagementScore >= 8.0) { |
|
engagementMultiplier = 1.5; |
|
} else if (engagementScore >= 5.0) { |
|
engagementMultiplier = 1.2; |
|
} |
|
|
|
let recencyComponent = 0; |
|
if (timeSinceInstallSeconds <= 86400) { |
|
recencyComponent = 0.1 * Math.exp(-0.00003 * timeSinceInstallSeconds); |
|
} |
|
|
|
let timeOfDayBoost = 0; |
|
if (timeOfDayScore >= timeOfDayMinScore) { |
|
timeOfDayBoost = Math.min(timeOfDayMaxScore, timeOfDayScore) * timeOfDayWeight; |
|
} |
|
|
|
let clickRecencyBonus = 0; |
|
if (clickRecencyPriorityEnabled && mostRecentClickAt > 0) { |
|
const clickAgeMs = currentTime - mostRecentClickAt; |
|
if (clickAgeMs >= 0 && clickAgeMs <= clickRecencyPriorityWindowMs) { |
|
clickRecencyBonus = (clickRecencyPriorityWindowMs - clickAgeMs) / 1000; |
|
} |
|
} |
|
|
|
let installRecencyBonus = 0; |
|
if (installRecencyPriorityEnabled && app.installedAt > 0) { |
|
const installAgeMs = currentTime - app.installedAt; |
|
if (installAgeMs >= 0 && installAgeMs <= installRecencyPriorityWindowMs) { |
|
installRecencyBonus = (installRecencyPriorityWindowMs - installAgeMs) / 1000; |
|
} |
|
} |
|
|
|
app.rankingScore = usedVeryRecentlyScore |
|
+ usageScore |
|
+ (engagementScore * engagementMultiplier) |
|
+ installScore |
|
+ importantPackageScore |
|
+ recencyComponent |
|
+ timeOfDayBoost |
|
+ clickRecencyBonus |
|
+ installRecencyBonus; |
|
|
|
rankedApps.push({ app, mostRecentClickAt, installedAt: app.installedAt || 0 }); |
|
} |
|
} |
|
rankedApps.sort((a, b) => { |
|
if (installRecencyPriorityEnabled) { |
|
const aInstalledAt = a.installedAt; |
|
const bInstalledAt = b.installedAt; |
|
const aInstallAge = aInstalledAt > 0 ? currentTime - aInstalledAt : Number.POSITIVE_INFINITY; |
|
const bInstallAge = bInstalledAt > 0 ? currentTime - bInstalledAt : Number.POSITIVE_INFINITY; |
|
const aRecentInstall = aInstalledAt > 0 && aInstallAge >= 0 && aInstallAge <= installRecencyPriorityWindowMs; |
|
const bRecentInstall = bInstalledAt > 0 && bInstallAge >= 0 && bInstallAge <= installRecencyPriorityWindowMs; |
|
if (aRecentInstall && bRecentInstall) { |
|
if (aInstalledAt !== bInstalledAt) { |
|
return bInstalledAt - aInstalledAt; |
|
} |
|
} else if (aRecentInstall !== bRecentInstall) { |
|
return aRecentInstall ? -1 : 1; |
|
} |
|
} |
|
if (clickRecencyPriorityEnabled) { |
|
const aClickAt = a.mostRecentClickAt; |
|
const bClickAt = b.mostRecentClickAt; |
|
const aClickAge = aClickAt > 0 ? currentTime - aClickAt : Number.POSITIVE_INFINITY; |
|
const bClickAge = bClickAt > 0 ? currentTime - bClickAt : Number.POSITIVE_INFINITY; |
|
const aRecent = aClickAt > 0 && aClickAge >= 0 && aClickAge <= clickRecencyPriorityWindowMs; |
|
const bRecent = bClickAt > 0 && bClickAge >= 0 && bClickAge <= clickRecencyPriorityWindowMs; |
|
if (aRecent && bRecent) { |
|
if (aClickAt !== bClickAt) { |
|
return bClickAt - aClickAt; |
|
} |
|
} else if (aRecent !== bRecent) { |
|
return aRecent ? -1 : 1; |
|
} |
|
} |
|
return b.app.rankingScore - a.app.rankingScore; |
|
}); |
|
|
|
return rankedApps.map(entry => entry.app); |
|
}; |
|
|
|
const rankNotInstalledAds = (adsToRank) => { |
|
for (let i = 0; i < adsToRank.length; i += 1) { |
|
let ad = adsToRank[i]; |
|
deStringifyTargetingMetadata(ad); |
|
} |
|
adsToRank.sort((a, b) => { |
|
if (a.targetingMetadata.downloads && b.targetingMetadata.downloads && a.targetingMetadata.downloads !== b.targetingMetadata.downloads) { |
|
return b.targetingMetadata.downloads - a.targetingMetadata.downloads; |
|
} else if (a.targetingMetadata.reviews && b.targetingMetadata.reviews) { |
|
return b.targetingMetadata.reviews - a.targetingMetadata.reviews; |
|
} |
|
}); |
|
return adsToRank || []; |
|
}; |
|
|
|
const getRecommendations = () => { |
|
const resultCount = count ? count : 5; |
|
let adCount = 0; |
|
// if (resultCount > 6) { |
|
// adCount = 2; |
|
// } else if (resultCount < 4) { |
|
// adCount = 0; |
|
// } |
|
if (pureOrganicAdsEnabled || ads.length === 0) { |
|
adCount = 0; |
|
} |
|
let organicCount = resultCount - adCount; |
|
|
|
let packagesWithVariants = new Set(); |
|
|
|
if (typeof multiActivityApps !== 'undefined' && multiActivityApps) { |
|
for (let packageName of Object.keys(multiActivityApps)) { |
|
packagesWithVariants.add(packageName); |
|
} |
|
} |
|
|
|
if (typeof dualApps !== 'undefined' && dualApps) { |
|
for (let packageName of Object.keys(dualApps)) { |
|
packagesWithVariants.add(packageName); |
|
} |
|
} |
|
|
|
let appClickStatus = {}; |
|
|
|
if (events) { |
|
for (let packageName in events) { |
|
if (packagesWithVariants.has(packageName)) { |
|
let eventsForPackage = events[packageName]; |
|
for (let event of eventsForPackage) { |
|
if (event.event === 'click') { |
|
if (event.resId !== undefined) { |
|
appClickStatus[`${packageName}|${event.resId}`] = true; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
let packagesWithClickedApps = new Set(); |
|
let clickedApps = []; |
|
let clickedKeys = new Set(); |
|
|
|
if (typeof multiActivityApps !== 'undefined' && multiActivityApps && Object.keys(multiActivityApps).length > 0) { |
|
for (let multiActivityAppArray of Object.values(multiActivityApps)) { |
|
for (let app of multiActivityAppArray) { |
|
let appHasClicks = app.id !== undefined ? |
|
appClickStatus[`${app.packageName}|${app.id}`] : |
|
false; |
|
|
|
if (appHasClicks) { |
|
const key = `${app.packageName}|${app.component}|${app.userId}`; |
|
if (!clickedKeys.has(key)) { |
|
clickedKeys.add(key); |
|
clickedApps.push(app); |
|
packagesWithClickedApps.add(app.packageName); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (typeof dualApps !== 'undefined' && dualApps && Object.keys(dualApps).length > 0) { |
|
for (let dualAppArray of Object.values(dualApps)) { |
|
for (let app of dualAppArray) { |
|
let appHasClicks = app.id !== undefined ? |
|
appClickStatus[`${app.packageName}|${app.id}`] : |
|
false; |
|
if (appHasClicks) { |
|
const key = `${app.packageName}|${app.component}|${app.userId}`; |
|
if (!clickedKeys.has(key)) { |
|
clickedKeys.add(key); |
|
clickedApps.push(app); |
|
packagesWithClickedApps.add(app.packageName); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
let allApps = Object.values(apps).filter(app => { |
|
return !packagesWithClickedApps.has(app.packageName); |
|
}); |
|
|
|
allApps.push(...clickedApps); |
|
|
|
let rankedApps = rankInstalledApps(allApps); |
|
let visibleApps = []; |
|
let visibleAppLimit = Math.max(organicCount, softBoostCandidateEndIndex + 1); |
|
for (let i = 0; i < rankedApps.length && visibleApps.length < visibleAppLimit; i += 1) { |
|
let app = rankedApps[i]; |
|
if (blacklist && blacklist.includes(app.packageName)) { |
|
continue; |
|
} |
|
visibleApps.push(app); |
|
} |
|
|
|
let softBoostState = getSuggestedAppsSoftBoostState(visibleApps, organicCount); |
|
if (softBoostState && Math.random() < softBoostEligibleRate) { |
|
visibleApps[softBoostTargetSlotIndex] = visibleApps[softBoostState.candidateIndex]; |
|
} else { |
|
softBoostState = null; |
|
} |
|
|
|
let displayApps = visibleApps.slice(0, organicCount); |
|
let results = []; |
|
for (let i = 0; i < displayApps.length; i += 1) { |
|
let app = displayApps[i]; |
|
let resultObj = { |
|
app |
|
}; |
|
if (organicAdsEnabled) { |
|
let isSoftBoostedSlot = softBoostState && |
|
i === softBoostTargetSlotIndex && |
|
app.packageName === softBoostState.candidatePackage; |
|
let ad = getBestInstalledAdForApp(app, ads, { |
|
allowedTypes: isSoftBoostedSlot ? ['reen'] : null, |
|
includeWeb: !isSoftBoostedSlot, |
|
useSoftBoostConversion: !!isSoftBoostedSlot |
|
}); |
|
if (ad) { |
|
if (isSoftBoostedSlot) { |
|
ad.log = { |
|
sel: "organic-soft-boost", |
|
boost_from_rank: softBoostState.candidateIndex + 1, |
|
boost_into_slot: softBoostTargetSlotIndex + 1, |
|
displaced_package: softBoostState.displacedPackage, |
|
score_gap: softBoostState.scoreGap |
|
}; |
|
} else { |
|
ad.log = { |
|
sel: "organic" |
|
}; |
|
} |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
resultObj.ad = ad; |
|
selectedAdIds.push(ad.id); |
|
selectedPackages.push(ad.packageName); |
|
} |
|
} |
|
results.push(resultObj); |
|
} |
|
if (organicAdsBoostEnabled) { |
|
let organicAdsResults = [], otherResults = []; |
|
for (let i = 0; i < results.length; i += 1) { |
|
let result = results[i]; |
|
if (result.ad) { |
|
result.ad.log = { |
|
sel: "organic-boost" |
|
}; |
|
organicAdsResults.push(result); |
|
} else { |
|
otherResults.push(result); |
|
} |
|
} |
|
results = [...organicAdsResults, ...otherResults]; |
|
} |
|
results = applyMotoCampaignBoost(results, resultCount); |
|
if (adCount === 1) { |
|
let resultObj = {}; |
|
let ads = getAdsForDisplayWithUsage(adCount); |
|
if (ads.length > 0) { |
|
resultObj.ad = ads[0]; |
|
if (resultObj.ad.viewThrough && hasRecentVTA(resultObj.ad)) { |
|
resultObj.ad.viewThrough = 0; |
|
} |
|
if (apps[ads[0].packageName]) { |
|
resultObj.app = apps[ads[0].packageName]; |
|
} |
|
results.push(resultObj); |
|
} else if (rankedPackages.length >= resultCount) { |
|
let newPackage = rankedPackages[resultCount - 1]; |
|
resultObj.app = apps[newPackage]; |
|
results.push(resultObj); |
|
} |
|
} else if (adCount === 2) { |
|
let insertIndex = Math.floor(resultCount/2); |
|
let resultObj = {}; |
|
let ads = getAdsForDisplayWithUsage(adCount); |
|
if (ads.length > 0) { |
|
resultObj.ad = ads[0]; |
|
if (resultObj.ad.viewThrough && hasRecentVTA(resultObj.ad)) { |
|
resultObj.ad.viewThrough = 0; |
|
} |
|
if (apps[ads[0].packageName]) { |
|
resultObj.app = apps[ads[0].packageName]; |
|
} |
|
results.splice(insertIndex, 0, resultObj); |
|
resultObj = {}; |
|
if (ads.length > 1) { |
|
resultObj.ad = ads[1]; |
|
if (resultObj.ad.viewThrough && hasRecentVTA(resultObj.ad)) { |
|
resultObj.ad.viewThrough = 0; |
|
} |
|
if (apps[ads[1].packageName]) { |
|
resultObj.app = apps[ads[1].packageName]; |
|
} |
|
results.push(resultObj); |
|
} else if (rankedPackages.length >= resultCount) { |
|
let newPackage = rankedPackages[resultCount - 1]; |
|
resultObj.app = apps[newPackage]; |
|
results.push(resultObj); |
|
} |
|
} else if (rankedPackages.length >= resultCount) { |
|
let newPackage = rankedPackages[resultCount - 2]; |
|
resultObj.app = apps[newPackage]; |
|
results.push(resultObj); |
|
resultObj = {}; |
|
let fallbackPackage = rankedPackages[resultCount - 1]; |
|
resultObj.app = apps[fallbackPackage]; |
|
results.push(resultObj); |
|
} |
|
} |
|
|
|
return results; |
|
}; |
|
|
|
const getAdsForDisplayWithUsage = (adCount) => { |
|
let chosenAds = []; |
|
let numSelectedReenAds = 0; |
|
|
|
let recentInstallNoUsageConsideration = []; |
|
let recentInstallUsageConsideration = []; |
|
let recentUsageConsideration = []; |
|
let timeOfDayUsageConsideration = []; |
|
let distantInstallNoUsageConsideration = []; |
|
let distantInstallUsageConsideration = []; |
|
let competitorInstallConsideration = []; |
|
let uninstalledAppConsideration = []; |
|
let distantUsageConsideration = []; |
|
let competitiveUsageConsideration = []; |
|
let appInstalledConsideration = []; |
|
let notFreqCappedAds = []; |
|
let convertedAds = 0; |
|
|
|
const timeOfDayLookbackMs = d7delta; |
|
const timeOfDayWindowMinutes = 90; |
|
const timeOfDayMinScore = 0.25; |
|
const timeOfDayScoreByPkg = {}; |
|
const timeOfDayDate = new Date(currentTime); |
|
const timeOfDayNowMinutes = (timeOfDayDate.getHours() * 60) + timeOfDayDate.getMinutes(); |
|
const timeOfDayInvWindow = 1 / timeOfDayWindowMinutes; |
|
const timeOfDayInvLookback = 1 / timeOfDayLookbackMs; |
|
|
|
const getTimeOfDayScore = (packageName) => { |
|
if (!packageName) { |
|
return 0; |
|
} |
|
const cached = timeOfDayScoreByPkg[packageName]; |
|
if (cached !== undefined) { |
|
return cached; |
|
} |
|
const pkgEvents = events ? events[packageName] : null; |
|
if (!pkgEvents || pkgEvents.length === 0) { |
|
timeOfDayScoreByPkg[packageName] = 0; |
|
return 0; |
|
} |
|
|
|
const cutoffMs = currentTime - timeOfDayLookbackMs; |
|
const len = pkgEvents.length; |
|
const first = pkgEvents[0]; |
|
const last = pkgEvents[len - 1]; |
|
const firstTs = first && typeof first.completedAt === 'number' ? first.completedAt : 0; |
|
const lastTs = last && typeof last.completedAt === 'number' ? last.completedAt : 0; |
|
const ascending = firstTs <= lastTs; |
|
|
|
let score = 0; |
|
let matches = 0; |
|
|
|
const start = ascending ? len - 1 : 0; |
|
const end = ascending ? -1 : len; |
|
const step = ascending ? -1 : 1; |
|
for (let i = start; i !== end; i += step) { |
|
const event = pkgEvents[i]; |
|
if (!event || typeof event.completedAt !== 'number') { |
|
continue; |
|
} |
|
const ts = event.completedAt; |
|
if (ts < cutoffMs) { |
|
break; |
|
} |
|
if (event.event !== 'click') { |
|
continue; |
|
} |
|
const ageMs = currentTime - ts; |
|
if (ageMs < 0) { |
|
continue; |
|
} |
|
timeOfDayDate.setTime(ts); |
|
const eventMinutes = (timeOfDayDate.getHours() * 60) + timeOfDayDate.getMinutes(); |
|
let diff = eventMinutes - timeOfDayNowMinutes; |
|
if (diff < 0) { |
|
diff = -diff; |
|
} |
|
if (diff > 720) { |
|
diff = 1440 - diff; |
|
} |
|
if (diff > timeOfDayWindowMinutes) { |
|
continue; |
|
} |
|
const timeWeight = 1 - (diff * timeOfDayInvWindow); |
|
const recencyWeight = 1 - (ageMs * timeOfDayInvLookback); |
|
score += timeWeight * recencyWeight; |
|
matches += 1; |
|
} |
|
|
|
if (matches === 0 || score < timeOfDayMinScore) { |
|
timeOfDayScoreByPkg[packageName] = 0; |
|
return 0; |
|
} |
|
timeOfDayScoreByPkg[packageName] = score; |
|
return score; |
|
}; |
|
|
|
for (let i = 0; i < ads.length; i += 1) { |
|
const ad = ads[i]; |
|
if (hasBeenSelected(ad)) { |
|
continue; |
|
} |
|
const app = apps[ad.packageName]; |
|
if (blacklist && blacklist.includes(ad.packageName)) { |
|
continue; |
|
} |
|
if (!installAdsEnabled && (!app || (app && uninstalledApp(app)))) { |
|
continue; |
|
} |
|
if (app && ad.type === 'web') { |
|
continue; |
|
} |
|
if (!webAdsEnabled && ad.type === 'web') { |
|
continue; |
|
} |
|
if (app && isConvertedAd(ad, app)) { |
|
convertedAds += 1; |
|
continue; |
|
} |
|
if (ad.type === 'reen' && (!app || (app && uninstalledApp(app)))) { |
|
continue; |
|
} |
|
|
|
if (app && recentInstallNoUsage(ad, app)) { |
|
recentInstallNoUsageConsideration.push(ad); |
|
} |
|
if (app && recentInstallUsage(ad, app)) { |
|
recentInstallUsageConsideration.push(ad); |
|
} |
|
|
|
if (app && recentUsage(ad, app)) { |
|
recentUsageConsideration.push(ad); |
|
} |
|
if (app && ad.type === 'reen' && getTimeOfDayScore(ad.packageName) > 0) { |
|
timeOfDayUsageConsideration.push(ad); |
|
} |
|
if (app && distantInstallNoUsage(ad, app)) { |
|
distantInstallNoUsageConsideration.push(ad); |
|
} |
|
|
|
if (isFrequencyCapped(ad)) { |
|
continue; |
|
} else { |
|
notFreqCappedAds.push(ad); |
|
} |
|
|
|
if (app && ad.type === 'inst' && !uninstalledApp(app)) { |
|
continue; |
|
} |
|
|
|
if (app && distantInstallUsage(ad, app)) { |
|
distantInstallUsageConsideration.push(ad); |
|
} |
|
|
|
deStringifyTargetingMetadata(ad); |
|
|
|
if (competitorInstall(ad)) { |
|
competitorInstallConsideration.push(ad); |
|
} |
|
if (app && uninstalledApp(app)) { |
|
uninstalledAppConsideration.push(ad); |
|
} |
|
if (app && distantUsage(ad, app)) { |
|
distantUsageConsideration.push(ad); |
|
} |
|
if (competitiveUsage(ad)) { |
|
competitiveUsageConsideration.push(ad); |
|
} |
|
if (app && appInstalled(ad, app)) { |
|
appInstalledConsideration.push(ad); |
|
} |
|
} |
|
|
|
loggingObject.l1 = recentInstallNoUsageConsideration.length; |
|
loggingObject.l2 = recentInstallUsageConsideration.length; |
|
loggingObject.l3 = recentUsageConsideration.length; |
|
loggingObject.ltod = timeOfDayUsageConsideration.length; |
|
loggingObject.l4 = distantInstallNoUsageConsideration.length; |
|
loggingObject.l5 = distantInstallUsageConsideration.length; |
|
loggingObject.l6 = competitorInstallConsideration.length; |
|
loggingObject.l7 = uninstalledAppConsideration.length; |
|
loggingObject.l8 = distantUsageConsideration.length; |
|
loggingObject.l9 = competitiveUsageConsideration.length; |
|
loggingObject.l10 = appInstalledConsideration.length; |
|
loggingObject.lfr = (ads.length-notFreqCappedAds.length); |
|
loggingObject.lc = convertedAds; |
|
|
|
if (recentInstallNoUsageConsideration.length > 0) { |
|
recentInstallNoUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return compareInstalledAdsWithTypePriority(a, b, aApp.installedAt - bApp.installedAt); |
|
}); |
|
|
|
for (let i = 0; i < recentInstallNoUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = recentInstallNoUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "recent-inst-no-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
|
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
|
|
if (recentInstallUsageConsideration.length > 0) { |
|
recentInstallUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return compareInstalledAdsWithTypePriority(a, b, aApp.installedAt - bApp.installedAt); |
|
}); |
|
|
|
for (let i = 0; i < recentInstallUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = recentInstallUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "recent-inst-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
|
|
if (recentUsageConsideration.length > 0) { |
|
recentUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return aApp.timeUsedInLast7D - bApp.timeUsedInLast7D; |
|
}); |
|
|
|
for (let i = 0; i < recentUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = recentUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "recent-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
|
|
if (timeOfDayUsageConsideration.length > 0) { |
|
timeOfDayUsageConsideration.sort((a, b) => { |
|
const aScore = timeOfDayScoreByPkg[a.packageName] || 0; |
|
const bScore = timeOfDayScoreByPkg[b.packageName] || 0; |
|
if (aScore !== bScore) { |
|
return bScore - aScore; |
|
} |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return (bApp ? bApp.lastTimeInForeground : 0) - (aApp ? aApp.lastTimeInForeground : 0); |
|
}); |
|
|
|
for (let i = 0; i < timeOfDayUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = timeOfDayUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "time-of-day-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
|
|
if (distantInstallNoUsageConsideration.length > 0) { |
|
distantInstallNoUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return compareInstalledAdsWithTypePriority(a, b, aApp.installedAt - bApp.installedAt); |
|
}); |
|
|
|
for (let i = 0; i < distantInstallNoUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = distantInstallNoUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "dist-inst-no-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
|
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (distantInstallUsageConsideration.length > 0) { |
|
distantInstallUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return compareInstalledAdsWithTypePriority(a, b, bApp.timeUsedInLast3Y - aApp.timeUsedInLast3Y); |
|
}); |
|
|
|
for (let i = 0; i < distantInstallUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = distantInstallUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "dist-inst-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (competitorInstallConsideration.length > 0) { |
|
competitorInstallConsideration.sort((a, b) => { |
|
let aCompetitiveInstall = 0; |
|
let bCompetitiveInstall = 0; |
|
for (let i = 0; i < a.targetingMetadata.competitors.length; i += 1) { |
|
let competitorPackage = a.targetingMetadata.competitors[i]; |
|
let app = apps[competitorPackage]; |
|
if (app && app.installedAt > aCompetitiveInstall) { |
|
aCompetitiveInstall = app.installedAt; |
|
} |
|
} |
|
for (let i = 0; i < b.targetingMetadata.competitors.length; i += 1) { |
|
const competitorPackage = b.targetingMetadata.competitors[i]; |
|
const app = apps[competitorPackage]; |
|
if (app && app.installedAt > bCompetitiveInstall) { |
|
bCompetitiveInstall = app.installedAt; |
|
} |
|
} |
|
return aCompetitiveInstall - bCompetitiveInstall; |
|
}); |
|
|
|
for (let i = 0; i < competitorInstallConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = competitorInstallConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (apps[selectedAd.packageName] && numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "competitor-install", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
if (apps[selectedAd.packageName]) { |
|
numSelectedReenAds += 1; |
|
} |
|
} |
|
|
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (uninstalledAppConsideration.length > 0) { |
|
uninstalledAppConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return aApp.timeUsedInLast3Y - bApp.timeUsedInLast3Y; |
|
}); |
|
|
|
for (let i = 0; i < uninstalledAppConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = uninstalledAppConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "uninstalled-app", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (distantUsageConsideration.length > 0) { |
|
distantUsageConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return aApp.timeUsedInLast3Y - bApp.timeUsedInLast3Y; |
|
}); |
|
|
|
for (let i = 0; i < distantUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = distantUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "dist-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (competitiveUsageConsideration.length > 0) { |
|
competitiveUsageConsideration.sort((a, b) => { |
|
let aCompetitiveUsage = 0; |
|
let bCompetitiveUsage = 0; |
|
for (let i = 0; i < a.targetingMetadata.competitors.length; i += 1) { |
|
const competitorPackage = a.targetingMetadata.competitors[i]; |
|
const app = apps[competitorPackage]; |
|
if (app) { |
|
aCompetitiveUsage += app.timeUsedInLast7D; |
|
} |
|
} |
|
for (let i = 0; i < b.targetingMetadata.competitors.length; i += 1) { |
|
const competitorPackage = b.targetingMetadata.competitors[i]; |
|
const app = apps[competitorPackage]; |
|
if (app) { |
|
bCompetitiveUsage += app.timeUsedInLast7D; |
|
} |
|
} |
|
return aCompetitiveUsage - bCompetitiveUsage; |
|
}); |
|
|
|
for (let i = 0; i < competitiveUsageConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = competitiveUsageConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (apps[selectedAd.packageName] && numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "competitive-usage", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
if (apps[selectedAd.packageName]) { |
|
numSelectedReenAds += 1; |
|
} |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (appInstalledConsideration.length > 0) { |
|
appInstalledConsideration.sort((a, b) => { |
|
const aApp = apps[a.packageName]; |
|
const bApp = apps[b.packageName]; |
|
return aApp.lastTimeInForeground - bApp.lastTimeInForeground; |
|
}); |
|
|
|
for (let i = 0; i < appInstalledConsideration.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = appInstalledConsideration[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "app-installed", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
numSelectedReenAds += 1; |
|
} |
|
if (chosenAds.length === adCount) { |
|
return chosenAds; |
|
} |
|
} |
|
if (notFreqCappedAds.length > 0) { |
|
notFreqCappedAds = rankNotInstalledAds(notFreqCappedAds); |
|
|
|
for (let i = 0; i < notFreqCappedAds.length && chosenAds.length < adCount; i += 1) { |
|
let selectedAd = notFreqCappedAds[i]; |
|
if (hasBeenSelected(selectedAd)) { |
|
continue; |
|
} |
|
if (apps[selectedAd.packageName] && numSelectedReenAds >= displayReenAdCap) { |
|
continue; |
|
} |
|
selectedAd.log = { "sel": "by-downloads", ...loggingObject }; |
|
selectedAdIds.push(selectedAd.id); |
|
selectedPackages.push(selectedAd.packageName); |
|
chosenAds.push(selectedAd); |
|
if (apps[selectedAd.packageName]) { |
|
numSelectedReenAds += 1; |
|
} |
|
} |
|
} |
|
return chosenAds; |
|
}; |
|
|
|
const processQueryIntoParts = () => { |
|
// Return the full query as a single part instead of splitting by spaces/periods |
|
let queryParts = []; |
|
if (query && query.length > 0) { |
|
queryParts.push(query.toLowerCase()); |
|
} |
|
return queryParts; |
|
}; |
|
|
|
const processAppNameIntoParts = (appName) => { |
|
let appNameParts = [appName.toLowerCase()]; |
|
let parts = appName.split(' '); |
|
for (let j = 0; j < parts.length; j += 1) { |
|
let part = parts[j]; |
|
let lowerPart = part.toLowerCase(); |
|
if (appNameParts.includes(lowerPart)) { |
|
continue; |
|
} |
|
if (part.length > 0) { |
|
let capParts = part.split(/(?<=[a-z])(?=[A-Z])/); |
|
if (capParts.length > 1) { |
|
for (let k = 0; k < capParts.length; k += 1) { |
|
let capPart = capParts[k]; |
|
if (capPart.length > 0) { |
|
appNameParts.push(capPart.toLowerCase()); |
|
} |
|
} |
|
} |
|
appNameParts.push(lowerPart); |
|
} |
|
} |
|
return appNameParts; |
|
}; |
|
|
|
const KEYBOARD_ADJACENCY_MATRIX = { |
|
'a': ['q', 'w', 's', 'z'], |
|
'b': ['v', 'g', 'h', 'n'], |
|
'c': ['x', 'd', 'f', 'v'], |
|
'd': ['s', 'e', 'r', 'f', 'x', 'c'], |
|
'e': ['w', 's', 'd', 'r'], |
|
'f': ['d', 'r', 't', 'g', 'v', 'c'], |
|
'g': ['f', 't', 'y', 'h', 'b', 'v'], |
|
'h': ['g', 'y', 'u', 'j', 'n', 'b'], |
|
'i': ['u', 'j', 'k', 'o'], |
|
'j': ['h', 'u', 'i', 'k', 'm', 'n'], |
|
'k': ['j', 'i', 'o', 'l'], |
|
'l': ['k', 'o', 'p'], |
|
'm': ['n', 'j', 'k'], |
|
'n': ['b', 'h', 'j', 'm'], |
|
'o': ['i', 'k', 'l', 'p'], |
|
'p': ['o', 'l'], |
|
'q': ['a', 'w'], |
|
'r': ['e', 'd', 'f', 't'], |
|
's': ['a', 'w', 'e', 'd', 'x', 'z'], |
|
't': ['r', 'f', 'g', 'y'], |
|
'u': ['y', 'h', 'j', 'i'], |
|
'v': ['c', 'f', 'g', 'b'], |
|
'w': ['q', 'a', 's', 'e'], |
|
'x': ['z', 's', 'd', 'c'], |
|
'y': ['t', 'g', 'h', 'u'], |
|
'z': ['a', 's', 'x', ','] |
|
}; |
|
|
|
const startsWithAllowingTwoAdjacents = (queryPart, appNamePart) => { |
|
if (queryPart.length > appNamePart.length) { |
|
return false; |
|
} |
|
let maxAllowedAdjacents = 2; |
|
const areAdjacentChars = (char1, char2) => { |
|
return KEYBOARD_ADJACENCY_MATRIX[char1]?.includes(char2); |
|
} |
|
let doesMatch = true; |
|
let adjacents = 0; |
|
for (let i = 0; i < queryPart.length; i += 1) { |
|
if (queryPart[i] === appNamePart[i]) { |
|
continue; |
|
} |
|
if (areAdjacentChars(appNamePart[i], queryPart[i]) && adjacents < maxAllowedAdjacents) { |
|
adjacents += 1; |
|
} else { |
|
doesMatch = false; |
|
break; |
|
} |
|
} |
|
return doesMatch; |
|
}; |
|
|
|
const filterAppsByNameAndQuery = (apps, queryParts) => { |
|
let startPrefixMatchedApps = []; |
|
let laterPrefixMatchedApps = []; |
|
let fuzzyPrefixedMatchedApps = []; |
|
let startPrefixAliasMatchedApps = []; |
|
let fuzzyLaterPrefixedMatchedApps = []; |
|
let containsMatchedApps = []; |
|
let shortcutMatchedApps = []; |
|
|
|
// Since we now have only one query part (the full query), we can simplify the logic |
|
if (queryParts.length === 0) { |
|
return { |
|
startPrefixMatchedApps, |
|
laterPrefixMatchedApps, |
|
fuzzyPrefixedMatchedApps, |
|
startPrefixAliasMatchedApps, |
|
fuzzyLaterPrefixedMatchedApps, |
|
containsMatchedApps |
|
}; |
|
} |
|
|
|
const fullQuery = queryParts[0]; // The full query string |
|
|
|
for (let i = 0; i < apps.length; i += 1) { |
|
let app = apps[i]; |
|
let appName = app.appName; |
|
if (app.normAppName && app.normAppName.length > 0) { |
|
appName = app.normAppName; |
|
} |
|
let appNameParts = processAppNameIntoParts(appName); |
|
let aliases = app.aliases ? app.aliases.split(',') : []; |
|
if (aliases.length === 0 && app.targetingMetadata) { |
|
deStringifyTargetingMetadata(app); |
|
aliases = app.targetingMetadata.aliases || []; |
|
} |
|
let matched = false; |
|
|
|
// Check for start prefix match (query matches start of first app name part) |
|
if (!matched && appNameParts.length > 0 && appNameParts[0].startsWith(fullQuery)) { |
|
if (!app.log) { |
|
app.log = {}; |
|
} |
|
app.log = { "sel": "start-prefix-match", ...loggingObject }; |
|
startPrefixMatchedApps.push(app); |
|
matched = true; |
|
} |
|
|
|
// Check for later prefix match (query matches start of any non-first app name part) |
|
if (!matched) { |
|
for (let j = 1; j < appNameParts.length; j += 1) { |
|
if (appNameParts[j].startsWith(fullQuery)) { |
|
if (!app.log) { |
|
app.log = {}; |
|
} |
|
app.log = { "sel": "later-prefix-match", ...loggingObject }; |
|
laterPrefixMatchedApps.push(app); |
|
matched = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// Check for fuzzy prefix match (query matches with typos) |
|
if (!matched) { |
|
for (let j = 0; j < appNameParts.length; j += 1) { |
|
if (startsWithAllowingTwoAdjacents(fullQuery, appNameParts[j])) { |
|
if (!app.log) { |
|
app.log = {}; |
|
} |
|
if (j === 0) { |
|
app.log = { "sel": "start-prefix-fuzz", ...loggingObject }; |
|
fuzzyPrefixedMatchedApps.push(app); |
|
} else { |
|
app.log = { "sel": "later-prefix-fuzz", ...loggingObject }; |
|
fuzzyLaterPrefixedMatchedApps.push(app); |
|
} |
|
matched = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// Check for alias match (query matches start of any alias) |
|
if (!matched && aliases.length > 0) { |
|
for (let j = 0; j < aliases.length; j += 1) { |
|
if (aliases[j].startsWith(fullQuery)) { |
|
if (!app.log) { |
|
app.log = {}; |
|
} |
|
app.log = { "sel": "start-prefix-alias", ...loggingObject }; |
|
startPrefixAliasMatchedApps.push(app); |
|
matched = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
// Check for contains match (query is contained within app name parts) |
|
if (!matched && fullQuery.length > 2) { |
|
for (let j = 0; j < appNameParts.length; j += 1) { |
|
if (appNameParts[j].includes(fullQuery)) { |
|
if (!app.log) { |
|
app.log = {}; |
|
} |
|
app.log = { "sel": "contains-matched", ...loggingObject }; |
|
containsMatchedApps.push(app); |
|
matched = true; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
return { |
|
startPrefixMatchedApps, |
|
laterPrefixMatchedApps, |
|
fuzzyPrefixedMatchedApps, |
|
startPrefixAliasMatchedApps, |
|
fuzzyLaterPrefixedMatchedApps, |
|
containsMatchedApps |
|
}; |
|
}; |
|
|
|
const collectSearchCandidateApps = () => { |
|
let allApps = []; |
|
const seenVariantKeys = new Set(); |
|
|
|
const addAppVariant = (app) => { |
|
const variantKey = getAppVariantKey(app); |
|
if (!variantKey || seenVariantKeys.has(variantKey)) { |
|
return; |
|
} |
|
seenVariantKeys.add(variantKey); |
|
allApps.push(app); |
|
}; |
|
|
|
for (let app of Object.values(apps || {})) { |
|
addAppVariant(app); |
|
} |
|
|
|
if (typeof multiActivityApps !== 'undefined' && multiActivityApps && Object.keys(multiActivityApps).length > 0) { |
|
for (let multiActivityAppArray of Object.values(multiActivityApps)) { |
|
for (let app of multiActivityAppArray) { |
|
addAppVariant(app); |
|
} |
|
} |
|
} |
|
|
|
return allApps; |
|
}; |
|
|
|
const dedupeSearchMatches = (matchedApps) => { |
|
const groupedApps = new Map(); |
|
const groupedOrder = []; |
|
|
|
for (let i = 0; i < matchedApps.length; i += 1) { |
|
const app = matchedApps[i]; |
|
const appKey = getAppVariantKey(app); |
|
if (!appKey) { |
|
continue; |
|
} |
|
if (!groupedApps.has(appKey)) { |
|
groupedApps.set(appKey, []); |
|
groupedOrder.push(appKey); |
|
} |
|
groupedApps.get(appKey).push(app); |
|
} |
|
|
|
const dedupedApps = []; |
|
for (let i = 0; i < groupedOrder.length; i += 1) { |
|
const preferredApp = pickPreferredAppVariant(groupedApps.get(groupedOrder[i])); |
|
if (preferredApp) { |
|
dedupedApps.push(preferredApp); |
|
} |
|
} |
|
|
|
return dedupedApps; |
|
}; |
|
|
|
const collectDistinctDualSearchVariants = (packageName) => { |
|
if (!packageName || !dualApps || !dualApps[packageName] || blockedDualAppPackages.includes(packageName)) { |
|
return []; |
|
} |
|
|
|
const groupedApps = new Map(); |
|
const groupedOrder = []; |
|
for (let i = 0; i < dualApps[packageName].length; i += 1) { |
|
const dualApp = dualApps[packageName][i]; |
|
if (!dualApp || uninstalledApp(dualApp)) { |
|
continue; |
|
} |
|
const dualKey = getAppVariantKey(dualApp); |
|
if (!dualKey) { |
|
continue; |
|
} |
|
if (!groupedApps.has(dualKey)) { |
|
groupedApps.set(dualKey, []); |
|
groupedOrder.push(dualKey); |
|
} |
|
groupedApps.get(dualKey).push(dualApp); |
|
} |
|
|
|
const distinctApps = []; |
|
for (let i = 0; i < groupedOrder.length; i += 1) { |
|
const preferredApp = pickPreferredAppVariant(groupedApps.get(groupedOrder[i])); |
|
if (preferredApp) { |
|
distinctApps.push(preferredApp); |
|
} |
|
} |
|
return distinctApps; |
|
}; |
|
|
|
const getSearchResults = () => { |
|
let results = []; |
|
let queryParts = processQueryIntoParts(); |
|
let allApps = collectSearchCandidateApps(); |
|
|
|
let organicMatchingObj = filterAppsByNameAndQuery(allApps, queryParts); |
|
|
|
// TEMPORARILY DISABLED - Complex usage-based ranking |
|
// organicMatchingObj.startPrefixMatchedApps = rankInstalledApps(organicMatchingObj.startPrefixMatchedApps); |
|
// organicMatchingObj.laterPrefixMatchedApps = rankInstalledApps(organicMatchingObj.laterPrefixMatchedApps); |
|
// organicMatchingObj.fuzzyPrefixedMatchedApps = rankInstalledApps(organicMatchingObj.fuzzyPrefixedMatchedApps); |
|
// organicMatchingObj.fuzzyLaterPrefixedMatchedApps = rankInstalledApps(organicMatchingObj.fuzzyLaterPrefixedMatchedApps); |
|
// organicMatchingObj.startPrefixAliasMatchedApps = rankInstalledApps(organicMatchingObj.startPrefixAliasMatchedApps); |
|
// organicMatchingObj.containsMatchedApps = rankInstalledApps(organicMatchingObj.containsMatchedApps); |
|
// console.log('organicMatchingObj: ', organicMatchingObj); |
|
|
|
// TEMPORARY ALPHABETICAL SORTING - Combine all results and sort alphabetically |
|
let tempResults = [ |
|
...organicMatchingObj.startPrefixMatchedApps.sort((a, b) => { |
|
let aName = (a.appName || a.packageName || '').toLowerCase(); |
|
let bName = (b.appName || b.packageName || '').toLowerCase(); |
|
return aName.localeCompare(bName); |
|
}), |
|
...organicMatchingObj.laterPrefixMatchedApps.sort((a, b) => { |
|
let aName = (a.appName || a.packageName || '').toLowerCase(); |
|
let bName = (b.appName || b.packageName || '').toLowerCase(); |
|
return aName.localeCompare(bName); |
|
})]; |
|
// ...organicMatchingObj.fuzzyPrefixedMatchedApps, |
|
// ...organicMatchingObj.startPrefixAliasMatchedApps, |
|
// ...organicMatchingObj.fuzzyLaterPrefixedMatchedApps, |
|
// ...organicMatchingObj.containsMatchedApps]; |
|
|
|
// Sort alphabetically first, but don't filter uninstalled apps yet (we need them for dual app logic) |
|
tempResults = tempResults.filter(app => { |
|
return app && app.packageName !== currentPackage; |
|
}); |
|
tempResults = dedupeSearchMatches(tempResults); |
|
|
|
for (let i = 0; i < tempResults.length; i += 1) { |
|
let app = tempResults[i]; |
|
let resultObj = { |
|
app |
|
}; |
|
if (organicAdsEnabled) { |
|
let ad = getBestInstalledAdForApp(app, ads, { |
|
allowedTypes: ['inst', 'acti', 'reen'] |
|
}); |
|
if (ad) { |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
resultObj.ad = ad; |
|
resultObj.ad.log = app.log; |
|
delete app.log; |
|
selectedAdIds.push(ad.id); |
|
selectedPackages.push(ad.packageName); |
|
} |
|
} |
|
results.push(resultObj); |
|
} |
|
if (organicAdsBoostEnabled) { |
|
let organicAdsResults = [], otherResults = []; |
|
for (let i = 0; i < results.length; i += 1) { |
|
let result = results[i]; |
|
if (result.ad) { |
|
organicAdsResults.push(result); |
|
} else { |
|
otherResults.push(result); |
|
} |
|
} |
|
results = [...organicAdsResults, ...otherResults]; |
|
} |
|
|
|
if (!organicAdsEnabled) { |
|
let reenAdsToInsert = []; |
|
for (let i = 0; i < searchReenAdCap; i += 1) { |
|
for (let j = 0; j < results.length; j += 1) { |
|
let result = results[j]; |
|
let ad = getBestInstalledAdForApp(result.app, ads, { |
|
allowedTypes: ['acti', 'reen'] |
|
}); |
|
if (ad) { |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
let resultObj = { |
|
app: result.app, |
|
ad |
|
} |
|
result.ad.log = result.app.log; |
|
delete result.app.log; |
|
reenAdsToInsert.push(resultObj); |
|
selectedAdIds.push(ad.id); |
|
selectedPackages.push(ad.packageName); |
|
break; |
|
} |
|
} |
|
} |
|
results = [...reenAdsToInsert, ...results]; |
|
} |
|
|
|
if (typeof dualApps !== 'undefined' && dualApps && Object.keys(dualApps).length > 0) { |
|
// Track which app variants are already present so we don't insert duplicates. |
|
const seenAppKeys = new Set(); |
|
for (let k = 0; k < results.length; k += 1) { |
|
const a = results[k] && results[k].app; |
|
if (a) { |
|
const appKey = getAppVariantKey(a); |
|
if (appKey) { |
|
seenAppKeys.add(appKey); |
|
} |
|
} |
|
} |
|
|
|
// Use a separate counter to track position as we modify the array |
|
for (let i = 0; i < results.length; i += 1) { |
|
let result = results[i]; |
|
if (result.app && |
|
result.app.packageName && |
|
dualApps[result.app.packageName] && |
|
!blockedDualAppPackages.includes(result.app.packageName)) { |
|
const distinctDualApps = collectDistinctDualSearchVariants(result.app.packageName); |
|
let dualAppsToInsert = []; |
|
for (let j = 0; j < distinctDualApps.length; j += 1) { |
|
let dualApp = distinctDualApps[j]; |
|
if (!dualApp || dualApp.userId === userId || dualApp.userId === null || uninstalledApp(dualApp)) { |
|
continue; |
|
} |
|
|
|
const dualKey = getAppVariantKey(dualApp); |
|
if (seenAppKeys.has(dualKey)) { |
|
continue; |
|
} |
|
seenAppKeys.add(dualKey); |
|
|
|
// Work on a copy so we don't mutate the original dualApp object |
|
let dualAppCopy = { ...dualApp }; |
|
let dualResult = { |
|
app: dualAppCopy |
|
}; |
|
dualAppsToInsert.push(dualResult); |
|
} |
|
|
|
// Insert dual apps right after current position |
|
if (dualAppsToInsert.length > 0) { |
|
results.splice(i + 1, 0, ...dualAppsToInsert); |
|
// Skip over the newly inserted items |
|
i += dualAppsToInsert.length; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// Now filter out uninstalled apps (after dual apps have been processed) |
|
results = results.filter(result => { |
|
return result.app && !uninstalledApp(result.app); |
|
}); |
|
|
|
if (installAdsEnabled && !pureOrganicAdsEnabled) { |
|
let potentialInstallAds = []; |
|
for (let i = 0; i < ads.length; i += 1) { |
|
let ad = ads[i]; |
|
if (!apps[ad.packageName] && ad.type !== 'reen') { |
|
if (ad.type === 'web' && !webAdsEnabled) { |
|
continue; |
|
} |
|
potentialInstallAds.push(ad); |
|
} |
|
} |
|
|
|
let installAdsMatchObject = filterAppsByNameAndQuery(potentialInstallAds, queryParts); |
|
installAdsMatchObject.startPrefixMatchedApps = rankNotInstalledAds(installAdsMatchObject.startPrefixMatchedApps); |
|
installAdsMatchObject.laterPrefixMatchedApps = rankNotInstalledAds(installAdsMatchObject.laterPrefixMatchedApps); |
|
installAdsMatchObject.fuzzyPrefixedMatchedApps = rankNotInstalledAds(installAdsMatchObject.fuzzyPrefixedMatchedApps); |
|
installAdsMatchObject.fuzzyLaterPrefixedMatchedApps = rankNotInstalledAds(installAdsMatchObject.fuzzyLaterPrefixedMatchedApps); |
|
installAdsMatchObject.startPrefixAliasMatchedApps = rankNotInstalledAds(installAdsMatchObject.startPrefixAliasMatchedApps); |
|
|
|
let tempInstallAds = [ |
|
...installAdsMatchObject.startPrefixMatchedApps, |
|
...installAdsMatchObject.laterPrefixMatchedApps, |
|
...installAdsMatchObject.fuzzyPrefixedMatchedApps, |
|
...installAdsMatchObject.startPrefixAliasMatchedApps, |
|
...installAdsMatchObject.fuzzyLaterPrefixedMatchedApps]; |
|
|
|
let localAppCount = results.length; |
|
|
|
let selectedCount = 0; |
|
for (let i = 0; i < tempInstallAds.length; i += 1) { |
|
if (selectedCount >= searchInstAdCap) { |
|
break; |
|
} |
|
let ad = tempInstallAds[i]; |
|
if (hasBeenSelected(ad)) { |
|
continue; |
|
} |
|
if (hasRecentVTA(ad) || |
|
(i > 0 && localAppCount > 2) || |
|
query.length === 1 || |
|
(ad.log && ad.log.sel && |
|
(ad.log.sel.indexOf("fuzz") > -1 || |
|
ad.log.sel.indexOf("alias") > -1 || |
|
ad.log.sel.indexOf("contains") > -1) && query.length < 3)) { |
|
ad.viewThrough = 0; |
|
} |
|
let resultObj = { |
|
ad |
|
}; |
|
results.push(resultObj); |
|
selectedAdIds.push(ad.id); |
|
selectedPackages.push(ad.packageName); |
|
selectedCount += 1; |
|
} |
|
} |
|
|
|
if (results.length > 0 && results[0].app) { |
|
let app = results[0].app; |
|
let sublinks = []; |
|
if (shortcuts[app.packageName]) { |
|
shortcuts[app.packageName].sort((a, b) => { |
|
return a.rank - b.rank; |
|
}); |
|
shortcuts[app.packageName].map((shortcut) => { |
|
sublinks.push({ |
|
id: shortcut.id, |
|
appName: app.appName, |
|
title: shortcut.name, |
|
description: shortcut.description, |
|
packageName: shortcut.packageName, |
|
intentUri: shortcut.intentUri, |
|
type: 'shortcut' |
|
}); |
|
}); |
|
} |
|
results[0].sublinks = sublinks.slice(0, 3); |
|
} |
|
|
|
return results; |
|
}; |
|
|
|
const getAdsForSearch = () => { |
|
let adsToReturn = []; |
|
|
|
let queryParts = processQueryIntoParts(); |
|
let matchedAds = filterAppsByNameAndQuery(ads, queryParts); |
|
|
|
let installedAdFilterFunction = (ad) => { |
|
return apps[ad.packageName] && |
|
(ad.type === 'reen' || ad.type === 'acti') && |
|
!isConvertedAd(ad, apps[ad.packageName]) && |
|
!uninstalledApp(apps[ad.packageName]); |
|
}; |
|
let installedMatchedAds = { |
|
startPrefixMatchedAds: matchedAds.startPrefixMatchedApps.filter(installedAdFilterFunction), |
|
laterPrefixMatchedAds: matchedAds.laterPrefixMatchedApps.filter(installedAdFilterFunction), |
|
fuzzyPrefixedMatchedAds: matchedAds.fuzzyPrefixedMatchedApps.filter(installedAdFilterFunction), |
|
startPrefixAliasMatchedAds: matchedAds.startPrefixAliasMatchedApps.filter(installedAdFilterFunction), |
|
fuzzyLaterPrefixedMatchedAds: matchedAds.fuzzyLaterPrefixedMatchedApps.filter(installedAdFilterFunction) |
|
}; |
|
|
|
let uninstalledAdFilterFunction = (ad) => { |
|
if (ad.type === 'web' && !webAdsEnabled) { |
|
return false; |
|
} |
|
return (!apps[ad.packageName] || uninstalledApp(apps[ad.packageName])) && |
|
ad.type !== 'reen'; |
|
}; |
|
|
|
let uninstalledMatchedAds = { |
|
startPrefixMatchedAds: matchedAds.startPrefixMatchedApps.filter(uninstalledAdFilterFunction), |
|
laterPrefixMatchedAds: matchedAds.laterPrefixMatchedApps.filter(uninstalledAdFilterFunction), |
|
fuzzyPrefixedMatchedAds: matchedAds.fuzzyPrefixedMatchedApps.filter(uninstalledAdFilterFunction), |
|
startPrefixAliasMatchedAds: matchedAds.startPrefixAliasMatchedApps.filter(uninstalledAdFilterFunction), |
|
fuzzyLaterPrefixedMatchedAds: matchedAds.fuzzyLaterPrefixedMatchedApps.filter(uninstalledAdFilterFunction) |
|
}; |
|
|
|
installedMatchedAds.startPrefixMatchedAds = rankInstalledAds(installedMatchedAds.startPrefixMatchedAds); |
|
installedMatchedAds.laterPrefixMatchedAds = rankInstalledAds(installedMatchedAds.laterPrefixMatchedAds); |
|
installedMatchedAds.fuzzyPrefixedMatchedAds = rankInstalledAds(installedMatchedAds.fuzzyPrefixedMatchedAds); |
|
installedMatchedAds.startPrefixAliasMatchedAds = rankInstalledAds(installedMatchedAds.startPrefixAliasMatchedAds); |
|
installedMatchedAds.fuzzyLaterPrefixedMatchedAds = rankInstalledAds(installedMatchedAds.fuzzyLaterPrefixedMatchedAds); |
|
|
|
let installedAds = [ |
|
...installedMatchedAds.startPrefixMatchedAds, |
|
...installedMatchedAds.laterPrefixMatchedAds, |
|
...installedMatchedAds.fuzzyPrefixedMatchedAds, |
|
...installedMatchedAds.startPrefixAliasMatchedAds, |
|
...installedMatchedAds.fuzzyLaterPrefixedMatchedAds |
|
]; |
|
|
|
uninstalledMatchedAds.startPrefixMatchedAds = rankNotInstalledAds(uninstalledMatchedAds.startPrefixMatchedAds); |
|
uninstalledMatchedAds.laterPrefixMatchedAds = rankNotInstalledAds(uninstalledMatchedAds.laterPrefixMatchedAds); |
|
uninstalledMatchedAds.fuzzyPrefixedMatchedAds = rankNotInstalledAds(uninstalledMatchedAds.fuzzyPrefixedMatchedAds); |
|
uninstalledMatchedAds.startPrefixAliasMatchedAds = rankNotInstalledAds(uninstalledMatchedAds.startPrefixAliasMatchedAds); |
|
uninstalledMatchedAds.fuzzyLaterPrefixedMatchedAds = rankNotInstalledAds(uninstalledMatchedAds.fuzzyLaterPrefixedMatchedAds); |
|
|
|
let uninstalledAds = [ |
|
...uninstalledMatchedAds.startPrefixMatchedAds, |
|
...uninstalledMatchedAds.laterPrefixMatchedAds, |
|
...uninstalledMatchedAds.fuzzyPrefixedMatchedAds, |
|
...uninstalledMatchedAds.startPrefixAliasMatchedAds, |
|
...uninstalledMatchedAds.fuzzyLaterPrefixedMatchedAds |
|
]; |
|
|
|
for (let i = 0; i < Math.min(searchReenAdCap, installedAds.length); i += 1) { |
|
let ad = installedAds[i]; |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
adsToReturn.push({ |
|
ad |
|
}); |
|
} |
|
|
|
for (let i = 0; i < Math.min(searchInstAdCap, uninstalledAds.length); i += 1) { |
|
let ad = uninstalledAds[i]; |
|
if (ad.viewThrough && hasRecentVTA(ad)) { |
|
ad.viewThrough = 0; |
|
} |
|
adsToReturn.push({ |
|
ad |
|
}); |
|
} |
|
|
|
return adsToReturn; |
|
}; |
|
|
|
let results = []; |
|
if (effectiveOrganicRequest) { |
|
if (query === null || query === undefined || query === '') { |
|
if (linksOnly) { |
|
results = []; |
|
} else { |
|
results = getRecommendations(); |
|
} |
|
} else { |
|
results = getSearchResults(); |
|
} |
|
} else { |
|
if (query === null || query === undefined || query === '') { |
|
let ads = getAdsForDisplayWithUsage(count); |
|
for (let i = 0; i < ads.length; i += 1) { |
|
if (ads[i].viewThrough && hasRecentVTA(ads[i])) { |
|
ads[i].viewThrough = 0; |
|
} |
|
results.push({ |
|
ad: ads[i] |
|
}); |
|
} |
|
} else { |
|
results = getAdsForSearch(); |
|
} |
|
} |
|
|
|
for (let i = 0; i < results.length; i += 1) { |
|
let res = results[i]; |
|
if (res.ad) { |
|
deStringifyTargetingMetadata(res.ad); |
|
let tMeta = res.ad.targetingMetadata || {}; |
|
if (tMeta.downloads) { |
|
res.ad.downloads = tMeta.downloads; |
|
} |
|
if (tMeta.reviews) { |
|
res.ad.ratings = tMeta.reviews; |
|
} |
|
if (tMeta.rating) { |
|
res.ad.rating = tMeta.rating; |
|
} |
|
if (tMeta.category) { |
|
res.ad.categories = tMeta.category; |
|
} |
|
} |
|
} |
|
|
|
return JSON.stringify(results); |
|
})(); |