import { CampaignCreative } from "../../steps/ordering/ordering.model"; import { config } from "../../utils/config"; import constants from "../../utils/constants"; import logger from "../../utils/logger"; import MetricService, { BucketSet } from "../metric.service"; import AbstractScoreHashMap, { NormStats } from "./abstractScoreHashMap.service"; interface CTAFeatures { format?: string | null; ssp?: string | null; campaignId: string; assetId?: string | null; assetUnit?: string | null; } interface AssetContext { ssp?: string | null; assetId?: string | null; assetUnit?: string | null; } export type CtaScore = { score?: number | null; statistics: NormStats; }; const LOG_ID = "[CTA_HASHMAP_SERVICE]"; class Metrics { private static readonly prefix = "cta_hashmap_"; static readonly ADD_SCORES = Metrics.prefix + "add_scores"; static readonly GET_SCORE = Metrics.prefix + "get_score"; static readonly REFRESH = Metrics.prefix + "refresh"; static readonly CAMPAIGN_SSP_FALLBACK = Metrics.prefix + "campain_ssp_fallback"; static readonly FORMAT_FALLBACK = Metrics.prefix + "format_fallback"; static readonly NO_SCORE = Metrics.prefix + "no_score"; static initMetrics(metricService: MetricService): void { metricService.addHistogram( Metrics.ADD_SCORES, "Duration of getting all scores for a request", undefined, BucketSet.zeroTo10ms ); metricService.addHistogram( Metrics.GET_SCORE, "Duration of getting a score for a campaign", undefined, BucketSet.zeroTo100us ); metricService.addSection(Metrics.REFRESH); metricService.addCounter( Metrics.CAMPAIGN_SSP_FALLBACK, "Number of calls to getScore that fallback on campaign and ssp" ); metricService.addCounter( Metrics.FORMAT_FALLBACK, "Number of calls to getScore that fallback on format" ); metricService.addCounter(Metrics.NO_SCORE, "Number of calls to getScore that return no score"); } } export default class CallToActionHashMap extends AbstractScoreHashMap { private static instance: CallToActionHashMap; protected logId: string = LOG_ID; protected s3Bucket: string = config.CTA_S3_BUCKET; protected s3Prefix: string = config.CTA_S3_PREFIX; protected maxRetries: number = parseInt(config.CTA_MAX_RETRIES); protected retryDelay: number = parseInt(config.CTA_RETRY_DELAY); protected refreshInterval: number = parseInt(config.CTA_REFRESH_INTERVAL); protected refreshMetricLabel: string = Metrics.REFRESH; constructor() { super(); Metrics.initMetrics(this.metricService); this.scheduleRefresh(); } public static getInstance(): CallToActionHashMap { return this.instance || (this.instance = new CallToActionHashMap()); } /** * For each campaign, get the corresponding score and add it to the campaign's metadata. * @param campaignCreatives Array of campaigns-creative. * @param appBundleId Id of the application, or domain of the site for web-mobile requests. * @param adUnitId Id of the application, or domain of the site for web-mobile requests. * @param ssp ssp label of the site for web-mobile requests. */ public addScores(campaignCreatives: CampaignCreative[], assetContext: AssetContext): void { const endAddScoresTimer = this.metricService.startTimer(Metrics.ADD_SCORES); for (const campaignCreative of campaignCreatives) { const endGetScoreTimer = this.metricService.startTimer(Metrics.GET_SCORE); const ctaMetadata = this.getScore({ format: campaignCreative.metadataTmp.format || constants.FALLBACK_STR, ssp: assetContext.ssp, campaignId: campaignCreative.campaign.id, assetId: assetContext.assetId, assetUnit: assetContext.assetUnit, }); logger.debug( () => `${LOG_ID} ctaScore for campaign ${campaignCreative.campaign.id} and crea ${ campaignCreative.creativeId } is ${JSON.stringify(ctaMetadata)}` ); campaignCreative.metadata.cta_score = ctaMetadata && ctaMetadata.score ? ctaMetadata.score : null; campaignCreative.metadataTmp.ctaMetadata = ctaMetadata.statistics; endGetScoreTimer(); } endAddScoresTimer(); } /** * Get statistics by campaign. Fallback on format average if campaign entry does not exist * @param format * @param campaignId */ public getStatistics(format: string | null | undefined, campaignId: string): NormStats { const statistics: NormStats | undefined = this.normMap.get(`${format}|${campaignId}`); const fallbackStatistics: NormStats | undefined = this.normMap.get( `${format}|${constants.FALLBACK_STR}` ); return this.formatStatistics(statistics, fallbackStatistics); } private buildLevelKey(features: CTAFeatures, level: number): string { switch (level) { case 1: return `${features.format}|${features.ssp}|${features.campaignId}|${features.assetId}|${features.assetUnit}`; case 2: return `${features.format}|${features.ssp}|${features.campaignId}|${features.assetId}|${constants.FALLBACK_STR}`; case 3: return `${features.format}|${features.ssp}|${features.campaignId}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; case 4: return `${features.format}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}|${constants.FALLBACK_STR}`; default: throw new Error("Error level key should be betwen 1 and 4 (included)"); } } /** * Get campaign cta score based on contextual data with fallback mechanism * @param {CTAFeatures} features context features on which CTA score is based * @param {number} startLevel fallback level to start get score process * @return {CtaScore} cta rate for context and avergae score od the campaign */ public getScore(features: CTAFeatures): CtaScore { // get score let score; let level = features.assetUnit ? 1 : 2; while (score === undefined && level < 5) { score = this.scoresMap.get(this.buildLevelKey(features, level)); level++; } // get average score const statistics = this.getStatistics(features.format, features.campaignId); if (score === undefined) { this.metricService.incCounter(Metrics.NO_SCORE, 1); } return { score: score, statistics: statistics }; } }