import _ from "lodash"; import MetricService, { BucketSet } from "../metric.service"; import { Campaign, ChoiceRequest } from "../../models"; import { config } from "../../utils/config"; import { CampaignData, CampaignModel } from "../../models/CampaignModel"; import logger from "../../utils/logger"; import { App } from "../../models/app.model"; import { AdModel } from "../../models/AdModel"; import { IApp, IAsset, ISite, IAuctionResponse, IAuctionRequest } from "../../models/auction.model"; import CircuitBreaker from "opossum"; import MsAuctionClient from "./auction.client"; const AUCTION_SECTION = "auction_calls"; const MARKETPLACE_OGURY_BIDFLOOR_HISTOGRAM = "mkp_ogury_bidfloor"; export const MARKETPLACE_MARGIN = 0.3; export class MsAuctionService { host: string; client: MsAuctionClient; metricService: MetricService; adModel: AdModel; constructor(private readonly campaignModel: CampaignModel, host: string, adModel: AdModel) { this.host = host.startsWith("http://") || host.startsWith("https://") ? host : `http://${host}`; this.client = new MsAuctionClient(this.host, { headers: { Authorization: config.MS_AUCTION_TOKEN, }, }); this.metricService = MetricService.getInstance(); this.metricService.addSection( AUCTION_SECTION, ["auction_type", "has_aaid"], BucketSet.zeroTo600ms ); this.metricService.addHistogram( MARKETPLACE_OGURY_BIDFLOOR_HISTOGRAM, "Ogury bidfloor histogram", ["country"], [1, 2, 5, 10, 20, 30, 50, 70, 100, 200] ); this.adModel = adModel; } async getAuctionCampaign( request: ChoiceRequest, programmaticCampaigns: Campaign[], auctionType: string ): Promise { if (programmaticCampaigns.length <= 0) { return null; } const auctionRequest = this.buildAuctionRequest(request, programmaticCampaigns, auctionType); request.metadata.ogury_bidfloor = auctionRequest.ogury_bidfloor; let result: Campaign | null = null; logger.debug(() => `[AUCTION] payload to ms-auction: ${JSON.stringify(auctionRequest)}`); const validationErr = this.client.validateAuctionRequest(auctionRequest); if (validationErr) { logger.error(validationErr, `[AUCTION] invalid ms-auction request`); return null; } const hasAaid: string = auctionRequest.user?.aaid?.trim() ? "true" : "false"; const end = this.metricService.startSectionWithReq(request, AUCTION_SECTION); try { request.globalRequestLabels[auctionRequest.context.auction_type] = "true"; const response: IAuctionResponse | null = (await this.client.callAuction( auctionRequest )) as IAuctionResponse | null; logger.debug(() => `[AUCTION] ms-auction response: ${JSON.stringify(response)}`); if (response && response.campaign > 0) { const campaignId = `${response.campaign}`; const campaign = programmaticCampaigns.find((c) => c.id === campaignId); if (!campaign) { throw new Error("Campaign from response not found in request"); } campaign.metadata.is_programmatic_call = auctionType === "fixed_price"; campaign.metadata.vast_hash = `${config.AWS_REGION}_${request.id}`; result = campaign; if (auctionType !== "fixed_price") { this.saveMarketplaceResponse(response); } } end(true, { auction_type: auctionType, has_aaid: hasAaid }); } catch (err: any) { end(false, { auction_type: auctionType, has_aaid: hasAaid }); // filter circuitbreaker-specific errors if (!CircuitBreaker.isOurError(err)) { logger.debug(() => `[AUCTION] ms-auction error => ${err}`); } } return result; } buildAuctionRequest( request: ChoiceRequest, programmaticCampaigns: Campaign[], auctionType: string ): IAuctionRequest { const app = request.app ?? {}; const user = request.user ?? {}; const device = request.device ?? {}; const context = request.context ?? {}; const partialRequest: Exclude = { advert_id: request.id, ogury_bidfloor: _.round((app.eecpm ?? 0) / (1 - MARKETPLACE_MARGIN), 2), user: { aaid: device.ifa, odid: user.odid ?? "", is_data_optin: user.data_optin, consent: user.consent, consent_ads: user.consent_ads, tcf_version: user.tcf_version, is_gdpr_applied: user.is_gdpr_applied, }, device: { user_agent: device.ua ?? "", language: device.language, os_type: device.os ?? "", ip: device.ip ?? "", is_omid_compliant: device.is_omid_compliant || false, screen_size: device.screen_size, }, campaigns: programmaticCampaigns.map((campaign) => this.buildAuctionRequestCampaign(request.campaignData[campaign.id], app) ), context: { country_code: context.country ?? "", inventory_source: request.inventory_source, environment_type: MsAuctionService.getEnvironmentType(request), auction_type: auctionType, }, }; const partialAsset: IAsset = { name: app.name ?? "", seller_id: app.seller_id, publisher_name: app.publisher_name, publisher_is_network_partner: app.publisher_is_network_partner || false, ad_unit_id: app.ad_unit_id, ad_unit_type: app.ad_unit_type, sdk_version: app.sdk_version, }; const appBundle = app.bundle_id ?? ""; if (request.isWeb) { const site: ISite = { domain: appBundle, bsw_id: user.bidswitch_cookie_id, ttd_id: user.tdd_cookie_id, ...partialAsset, }; return { ...partialRequest, site, }; } const appAuction: IApp = { bundle: appBundle, categories: app.cat, ...partialAsset, }; return { ...partialRequest, app: appAuction, }; } buildAuctionRequestCampaign(campaignData: CampaignData, app: App) { const margin = this.campaignModel.getMargin(campaignData, app.ad_unit_type); const bidfloor = campaignData.prog_bid_floor ? campaignData.prog_bid_floor : _.round((app.eecpm ?? 0) / (1 - margin / 100), 2); return { id: parseInt(campaignData.id), deal_id: campaignData.prog_deal_id, wseat: campaignData.prog_wseat ? campaignData.prog_wseat.split(",") : [], bid_floor: bidfloor, dsp_buyer_id: campaignData.prog_dsp_buyer_id ?? "", vpaid: campaignData.vpaid_tracking ?? false, ad_type: this.campaignModel.getAdType(campaignData), }; } async saveMarketplaceResponse(data: { advert_id: string }) { // register in cappings table try { await this.adModel.save("mkp:" + data.advert_id, data); } catch (err: any) { logger.error(err); } } /** * Ms-auction currently uses 'app' instead of 'in-app' */ static getEnvironmentType(request: ChoiceRequest): string { if (request.environment_type === "in-app") { return "app"; } return request.environment_type; } }