import rfdc from "rfdc"; import { FeatureConfigService } from "../services/FeatureConfig.service"; import { ChoiceRequest, splitReqCampaignGroups, CampaignGroup, Campaign } from "../models"; import { BucketSet } from "../services/metric.service"; import { Step } from "./step"; import { StepResult } from "./stepResult"; import logger from "../utils/logger"; import _ from "lodash"; const clone = rfdc(); const stepBaseLabels = ["fill"]; export abstract class AbstractStep implements Step { protected name: string; protected metricName?: string; protected readonly labels: string[] = []; protected reportCreativeFiltering: boolean = false; protected metricBucket: number[] = BucketSet.zeroTo5ms; protected autoPassCampaignGroup: CampaignGroup = CampaignGroup.None; protected featureConfigService: FeatureConfigService; private _noRun: boolean = false; private _dryRun: boolean = false; private metadataKey: string; constructor(name: string, labels: string[] = []) { this.name = name; this.labels = [...stepBaseLabels, ...labels]; this.featureConfigService = FeatureConfigService.getInstance(); this.metadataKey = "pass_" + _.snakeCase(this.getName()); } /** * Dryrun ON: Launch the execution of step(s) under this one. Do not wait for the result. Build a new result with all campaign passing instead. * Dryrun OFF: change the request's campaigns according to step result. * @return {Promise} */ async execute(request: ChoiceRequest): Promise { if (this._noRun) { return this.buildDryRunResult(request); } if (this._dryRun) { // launch the step execution, but do not wait for result and build a fake one const cloned: ChoiceRequest = clone(request); this.executeStep(cloned); return this.buildDryRunResult(request); } const result: StepResult = await this.executeStep(request); this.applyResult(request, result); return result; } buildDryRunResult(request: ChoiceRequest): StepResult { const result: StepResult = new StepResult(); result.passCampaigns(...request.campaigns); return result; } /** * add a flag in metadata to know if the campaign has or hasn't passed the step * add passing campaigns to 'campaigns' array * add non-passing campaigns to 'excluded' array */ public applyResult(request: ChoiceRequest, result: StepResult): void { const excluded: Campaign[] = []; request.excluded = request.excluded || []; const passingCampaignsMap = result.passedCampaigns.reduce((map, campaign) => { map.set(campaign.id, campaign); return map; }, new Map()); const ignoredCampaignsMap = result.ignoredCampaigns.reduce((map, campaign) => { map.set(campaign.id, campaign); return map; }, new Map()); for (const campaign of request.campaigns) { const passingCampaign: Campaign | undefined = passingCampaignsMap.get(campaign.id); const ignoredCampaign: Campaign | undefined = ignoredCampaignsMap.get(campaign.id); if (passingCampaign) { campaign.stepsPassedStatus[this.metadataKey] = "true"; } else if (ignoredCampaign) { campaign.stepsPassedStatus[this.metadataKey] = "bypassed"; } else { campaign.stepsPassedStatus[this.metadataKey] = "false"; excluded.push(campaign); } } result.passedCampaigns.push(...result.ignoredCampaigns); request.campaigns = result.passedCampaigns; if (excluded.length > 0) { request.excluded.unshift(...excluded); logger.debug( () => `[step] exclude campaigns [${excluded .map((c) => c.id) .join(",")}] in step ${this.getName()}` ); } } private async executeStep(request: ChoiceRequest): Promise { const [autopass, stepFiltered] = splitReqCampaignGroups(request, this.autoPassCampaignGroup); let result: StepResult; if (stepFiltered.length !== 0) { request.campaigns = stepFiltered; result = await this.run(request); } else { result = new StepResult(); } result.passCampaigns(...autopass); return result; } /** * Step job. * If the step wants to exclude a campaign, it should remove it from request, as well as * adding it in the result object. * The step can also add some metadata and metrics to the result object, for an eventual * decorators use. * @param {ChoiceRequest} request * @return {Promise} */ abstract run(request: ChoiceRequest): Promise; public dryRun(): Step { this._dryRun = true; return this; } public noRun(): Step { this._noRun = true; return this; } getName(): string { return this.name; } getMetricName(): string { return this.metricName ?? this.name; } getLabels(): string[] { return this.labels; } shouldReportCreativeFiltering(): boolean { return this.reportCreativeFiltering; } getMetricBucket(): number[] { return this.metricBucket; } }