import express from 'express'; import _ from 'lodash'; import { BidOptimizer, BidOptimizerResponse } from '../../api/core/bidoptimizer/BidOptimizerInterface'; import { PluginProperty, PluginPropertyResponse } from '../../api/core/plugin/PluginPropertyInterface'; import { BidDecision } from '../../api/plugin/bidoptimizer/BidDecision'; import { BidOptimizerRequest, SaleCondition } from '../../api/plugin/bidoptimizer/BidOptimizerRequestInterface'; import { BasePlugin, PropertiesWrapper } from '../common'; export type BidOptimizerPluginResponse = BidDecision; export interface BidOptimizerBaseInstanceContext { properties: PropertiesWrapper; bidOptimizer: BidOptimizer; } export abstract class BidOptimizerPlugin extends BasePlugin { instanceContext: Promise; constructor(enableThrottling = false) { super(enableThrottling); this.initBidDecisions(); this.setErrorHandler(); } /** * * @param bidOptimizerId */ async fetchBidOptimizer(bidOptimizerId: string): Promise { const bidOptimizerResponse = await super.requestGatewayHelper( 'GET', `${this.outboundPlatformUrl}/v1/bid_optimizers/${bidOptimizerId}`, ); this.logger.debug(`Fetched Bid Optimizer: ${bidOptimizerId} - ${JSON.stringify(bidOptimizerResponse.data)}`); return bidOptimizerResponse.data; } /** * * @param bidOptimizerId */ async fetchBidOptimizerProperties(bidOptimizerId: string): Promise { const bidOptimizerPropertyResponse = await super.requestGatewayHelper( 'GET', `${this.outboundPlatformUrl}/v1/bid_optimizers/${bidOptimizerId}/properties`, ); this.logger.debug( `Fetched BidOptimizer Properties: ${bidOptimizerId} - ${JSON.stringify(bidOptimizerPropertyResponse.data)}`, ); return bidOptimizerPropertyResponse.data; } findBestSalesConditions(bidPrice: number, salesConditions: SaleCondition[]): SaleCondition { // Optimization, we only do the stringify if we are really on debug / silly mode if (this.logger.level === 'debug' || this.logger.level === 'silly') { this.logger.debug( `Looking to find the best sale condition for CPM: ${bidPrice} in: ${JSON.stringify(salesConditions, null, 4)}`, ); } const eligibleSalesConditions = salesConditions.filter((sc) => { return sc.floor_price <= bidPrice; }); // Optimization, we only do the stringify if we are really on debug / silly mode if (this.logger.level === 'debug' || this.logger.level === 'silly') { this.logger.debug( `Found eligible sales condition for CPM: ${bidPrice} in: ${JSON.stringify(eligibleSalesConditions, null, 4)}`, ); } const sortedEligibleSalesConditions = eligibleSalesConditions.sort((a, b) => { return a.floor_price - b.floor_price; }); // Optimization, we only do the stringify if we are really on debug / silly mode if (this.logger.level === 'debug' || this.logger.level === 'silly') { this.logger.debug( `Sorted eligible sales condition for CPM: ${bidPrice} in: ${JSON.stringify( sortedEligibleSalesConditions, null, 4, )}`, ); } return sortedEligibleSalesConditions[0]; } /** * Method to build an instance context * To be overriden to get a cutom behavior * This is a default provided implementation * @param bidOptimizerId */ protected async instanceContextBuilder(bidOptimizerId: string): Promise { const bidOptimizerP = this.fetchBidOptimizer(bidOptimizerId); const bidOptimizerPropsP = this.fetchBidOptimizerProperties(bidOptimizerId); const results = await Promise.all([bidOptimizerP, bidOptimizerPropsP]); const bidOptimizer = results[0]; const bidOptimizerProps = results[1]; const context = { bidOptimizer: bidOptimizer, properties: new PropertiesWrapper(bidOptimizerProps), }; return context; } /** * * @param request * @param instanceContext */ protected abstract onBidDecisions( request: BidOptimizerRequest, instanceContext: BidOptimizerBaseInstanceContext, ): Promise; protected async getInstanceContext(bidOptimizerId: string): Promise { if (!this.pluginCache.get(bidOptimizerId)) { void this.pluginCache.put( bidOptimizerId, this.instanceContextBuilder(bidOptimizerId).catch((err) => { this.logger.error(`Error while caching instance context: ${(err as Error).message}`); this.pluginCache.del(bidOptimizerId); throw err; }), this.getInstanceContextCacheExpiration(), ); } return this.pluginCache.get(bidOptimizerId) as Promise; } private initBidDecisions(): void { this.app.post( '/v1/bid_decisions', this.asyncMiddleware(async (req: express.Request, res: express.Response) => { if (!this.httpIsReady()) { const msg = { error: 'Plugin not initialized', }; this.logger.error('POST /v1/bid_decisions : %s', JSON.stringify(msg)); return res.status(500).json(msg); } else if (!req.body || _.isEmpty(req.body)) { const msg = { error: 'Missing request body', }; this.logger.error('POST /v1/bid_decisions : %s', JSON.stringify(msg)); return res.status(500).json(msg); } else { if (this.logger.level === 'debug' || this.logger.level === 'silly') { this.logger.debug(`POST /v1/bid_decisions ${JSON.stringify(req.body)}`); } const bidOptimizerRequest = req.body as BidOptimizerRequest; if (!this.onBidDecisions) { const errMsg = 'No BidOptimizer listener registered!'; this.logger.error(errMsg); return res.status(500).json({ error: errMsg }); } const instanceContext = await this.getInstanceContext(bidOptimizerRequest.campaign_info.bid_optimizer_id); const bidOptimizerResponse = await this.onBidDecisions(bidOptimizerRequest, instanceContext); if (this.logger.level === 'debug' || this.logger.level === 'silly') { this.logger.debug(`Returning: ${JSON.stringify(bidOptimizerResponse)}`); } return res.status(200).send(JSON.stringify(bidOptimizerResponse)); } }), ); } }