UNPKG

@mediarithmics/plugins-nodejs-sdk

Version:

This is the mediarithmics nodejs to help plugin developers bootstrapping their plugin without having to deal with most of the plugin boilerplate

180 lines (154 loc) 6.62 kB
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<BidOptimizerBaseInstanceContext> { instanceContext: Promise<BidOptimizerBaseInstanceContext>; constructor(enableThrottling = false) { super(enableThrottling); this.initBidDecisions(); this.setErrorHandler(); } /** * * @param bidOptimizerId */ async fetchBidOptimizer(bidOptimizerId: string): Promise<BidOptimizer> { const bidOptimizerResponse = await super.requestGatewayHelper<BidOptimizerResponse>( '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<PluginProperty[]> { const bidOptimizerPropertyResponse = await super.requestGatewayHelper<PluginPropertyResponse>( '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<BidOptimizerBaseInstanceContext> { 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<BidOptimizerPluginResponse>; protected async getInstanceContext(bidOptimizerId: string): Promise<BidOptimizerBaseInstanceContext> { 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<BidOptimizerBaseInstanceContext>; } 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)); } }), ); } }