import { ChoiceRequest, RequestInventorySource } from "../models"; import MetricService, { EndFunctionWithReq } from "../services/metric.service"; import { hasCampaign } from "../utils/RequestUtils"; import ChoicesTopicService from "../services/choicesTopic.service"; import { Step } from "../steps/step"; import { executeStep } from "../steps/step.util"; import { toResponseContract } from "../mappers/response.mapper"; import { ResponseContract } from "../data-contract/response.contract"; import logger from "../utils/logger"; import constants from "../utils/constants"; import { Response } from "express"; import { CampaignModel } from "../models/CampaignModel"; import { RequestContract } from "../data-contract/request.contract"; import { fromRequestContract } from "../mappers/request.mapper"; import { FeatureConfigService } from "../services/FeatureConfig.service"; import { ObjectSchema } from "joi"; import { validate } from "../validation/validation"; import DeliveryMonitoringService from "../services/deliveryMonitoringService"; import { StepFactory } from "../steps/stepFactory"; import { KafkaConnector } from "../models/connector/KafkaProducer"; import { bid } from "../beeswax-protos/beeswax"; abstract class Funnel { protected metricService: MetricService; protected choicesTopicService: ChoicesTopicService; protected deliveryTopicService: DeliveryMonitoringService; protected featureConfigService: FeatureConfigService; constructor( protected readonly inventorySource: RequestInventorySource, protected readonly campaignModel: CampaignModel, protected readonly stepFactory: StepFactory, private readonly kafkaRequestProducer: KafkaConnector | null, protected readonly requestSchemaValidation: ObjectSchema ) { this.metricService = MetricService.getInstance(); this.choicesTopicService = ChoicesTopicService.getInstance(); this.deliveryTopicService = DeliveryMonitoringService.getInstance(); this.featureConfigService = FeatureConfigService.getInstance(); } public async choose(request: RequestContract, res: Response): Promise { const end = this.metricService.startSectionWithDelayedReq(constants.CHOICES_REQUEST_SECTION); const shouldLog = this.shouldLog(); Funnel.logRequest(this.inventorySource, request, shouldLog); this.logKafkaRequest(request); let maybeRequest: ChoiceRequest | null = null; try { const validationError = validate(request, this.requestSchemaValidation, this.inventorySource); if (validationError) { res.status(400).json({ error: validationError }); return null; } const choicesRequest = (maybeRequest = await fromRequestContract( request, this.inventorySource, this.campaignModel )); await this.chooseAdCampaigns(choicesRequest); setImmediate(() => { this.choicesTopicService.sendReport(choicesRequest); this.deliveryTopicService.sendMessage(choicesRequest); }); const response = this.response(choicesRequest, end); Funnel.logResponse(this.inventorySource, response, shouldLog); return response; } catch (e: any) { logger.error(e, `[${this.inventorySource}] Unable to handle request`); end(false, maybeRequest, { inventory_source: this.inventorySource, }); res.status(500); res.end(); return null; } } abstract chooseAdCampaigns(request: ChoiceRequest): Promise; protected response(request: ChoiceRequest, end: EndFunctionWithReq): ResponseContract { logger.debug( () => `[${this.inventorySource}] Final selected campaigns: [${request.campaigns .map((campaign) => campaign.id) .join(",")}]` ); const response = toResponseContract(request); end(true, request, { fill: hasCampaign(request).toString(), ...request.globalRequestLabels, }); return response; } protected async runSteps(steps: Step[], request: ChoiceRequest): Promise { for (const step of steps) { if (await executeStep(step, request)) { return true; } } return false; } private shouldLog(): boolean { return ( Math.random() < this.featureConfigService.getFloatValue(null, "ms-choices.log-request-pct", 0) ); } private logKafkaRequest(request: RequestContract) { if ( Math.random() < this.featureConfigService.getFloatValue(null, "ms-choices.log-request-kafka-pct", 0.005) ) { this.kafkaRequestProducer?.send(request, request.id); } } public static logRequest( inventorySource: RequestInventorySource, request: RequestContract | bid.BidAgentRequest, shouldLog: boolean ) { if (shouldLog) { logger.info({ message: `[${inventorySource}] Received request`, req_body: request, }); } } public static logResponse( inventorySource: RequestInventorySource, response: ResponseContract | bid.IBidAgentResponse, shouldLog: boolean ) { if (shouldLog) { logger.info({ message: `[${inventorySource}] Response`, response_body: response, }); } } } export default Funnel;