@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
text/typescript
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));
}
}),
);
}
}