From a1ecaf1f44597d44c6771347e4192aca63999989 Mon Sep 17 00:00:00 2001 From: JP Mohan Date: Tue, 24 Mar 2026 19:47:49 +0530 Subject: [PATCH 1/4] mileRtdProvider initial commit --- modules/.submodules.json | 1 + modules/mileRtdProvider.js | 217 ++++++++++++ modules/mileRtdProvider.md | 70 ++++ .../eslint/approvedLoadExternalScriptPaths.js | 1 + test/spec/modules/mileRtdProvider_spec.js | 316 ++++++++++++++++++ 5 files changed, 605 insertions(+) create mode 100644 modules/mileRtdProvider.js create mode 100644 modules/mileRtdProvider.md create mode 100644 test/spec/modules/mileRtdProvider_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index c0e972f3c5d..2037a084f8c 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -105,6 +105,7 @@ "mediafilterRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "mileRtdProvider", "mobianRtdProvider", "neuwoRtdProvider", "nodalsAiRtdProvider", diff --git a/modules/mileRtdProvider.js b/modules/mileRtdProvider.js new file mode 100644 index 00000000000..508b265fe2d --- /dev/null +++ b/modules/mileRtdProvider.js @@ -0,0 +1,217 @@ +/** + * Thin Mile RTD client. + * Pulls pre-hashed targeting values from runtime global and applies slot targeting. + */ +import { submodule } from "../src/hook.js"; +import { loadExternalScript } from "../src/adloader.js"; +import { MODULE_TYPE_RTD } from "../src/activities/modules.js"; +import { auctionManager } from "../src/auctionManager.js"; +import { logError, logInfo } from "../src/utils.js"; + +const MODULE_NAME = "realTimeData"; +const SUBMODULE_NAME = "mile"; +const TARGETING_KEY = "mile_rtd"; +const LOG_PREFIX = "[mileRtdProvider]"; +const DEFAULT_ENGINE_GLOBAL = "mileRtdRuntime"; + +let moduleParams = {}; +let engineLoadPromise = null; + +function isFlooringEnforcedForAuction(auctionDetails = {}) { + const bidderRequestBids = (auctionDetails?.bidderRequests || []).flatMap( + (request) => request?.bids || [], + ); + const bidsReceived = auctionDetails?.bidsReceived || []; + const allBids = [...bidderRequestBids, ...bidsReceived]; + return allBids.some((bid) => { + const floorData = bid?.floorData; + if (!floorData) return false; + + const enforceJS = floorData?.enforcements?.enforceJS; + if (typeof enforceJS === "boolean") { + return enforceJS; + } + + // Some floor data payloads only expose signal/skip flags. + if (floorData?.skipped === false && floorData?.noFloorSignaled === false) { + return true; + } + return false; + }); +} + +function getRuntimeEngine() { + const globalName = moduleParams?.runtimeGlobalName || DEFAULT_ENGINE_GLOBAL; + return window?.[globalName]; +} + +export function loadRuntimeScript() { + if (engineLoadPromise) return engineLoadPromise; + const runtimeScriptUrl = moduleParams?.runtimeScriptUrl; + if (!runtimeScriptUrl) return Promise.resolve(false); + + engineLoadPromise = new Promise((resolve) => { + loadExternalScript( + runtimeScriptUrl, + MODULE_TYPE_RTD, + SUBMODULE_NAME, + () => { + logInfo(LOG_PREFIX, "runtime script loaded", runtimeScriptUrl); + resolve(true); + }, + ); + }).catch((error) => { + logError(LOG_PREFIX, "unable to load runtime script", error); + engineLoadPromise = null; + return false; + }); + return engineLoadPromise; +} + +export function getTargetingFromRuntime(auctionSnapshot, context = {}) { + const runtimeEngine = getRuntimeEngine(); + if ( + !runtimeEngine || + typeof runtimeEngine.getMileTargetingByAdUnit !== "function" + ) { + logInfo(LOG_PREFIX, "runtime engine missing getMileTargetingByAdUnit()"); + return Promise.resolve(null); + } + try { + return Promise.resolve( + runtimeEngine.getMileTargetingByAdUnit(auctionSnapshot, context), + ); + } catch (error) { + logError( + LOG_PREFIX, + "runtime engine failed while computing targeting", + error, + ); + return Promise.resolve(null); + } +} + +export function setSlotTargeting( + targetingByAdUnit, + googletag = window.googletag, +) { + if (!googletag?.cmd?.push || typeof googletag.pubads !== "function") { + logInfo(LOG_PREFIX, "GPT is not available, skipping slot targeting"); + return false; + } + googletag.cmd.push(() => { + const slots = googletag.pubads()?.getSlots?.() || []; + slots.forEach((slot) => { + if (typeof slot?.setTargeting !== "function") return; + const slotElementId = slot.getSlotElementId?.(); + const adUnitPath = slot.getAdUnitPath?.(); + const targetingValue = + targetingByAdUnit?.[slotElementId] ?? targetingByAdUnit?.[adUnitPath]; + if (targetingValue != null) { + slot.setTargeting(TARGETING_KEY, targetingValue); + } + }); + }); + return true; +} + +function buildAuctionDetailsFromAuction(auction) { + return { + adUnitCodes: auction?.getAdUnitCodes?.() || [], + adUnits: auction?.getAdUnits?.() || [], + bidderRequests: auction?.getBidRequests?.() || [], + bidsReceived: auction?.getBidsReceived?.() || [], + }; +} + +function extractAuctionSnapshot(auctionDetails = {}) { + const extractSnapshot = window?.mileRtdRuntimeUtils?.extractAuctionSnapshot; + if (typeof extractSnapshot === "function") { + return extractSnapshot(auctionDetails); + } + return { adUnitCodes: auctionDetails?.adUnitCodes || [] }; +} + +function applyRuntimeTargeting(auctionSnapshot, context = {}) { + return getTargetingFromRuntime(auctionSnapshot, context).then( + (targetingByAdUnit) => { + if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { + setSlotTargeting(targetingByAdUnit); + } + }, + ); +} + +export function onAuctionInitEvent(auctionDetails = {}) { + let snapshotDetails = auctionDetails; + if (!auctionDetails?.adUnitCodes?.length) { + snapshotDetails = buildAuctionDetailsFromAuction( + auctionManager.index.getAuction({ + auctionId: auctionManager.getLastAuctionId(), + }), + ); + } + if (!snapshotDetails?.adUnitCodes?.length) return; + if (!isFlooringEnforcedForAuction(snapshotDetails)) { + logInfo(LOG_PREFIX, "skipping targeting; floor enforcement is disabled"); + return; + } + const auctionSnapshot = extractAuctionSnapshot(snapshotDetails); + applyRuntimeTargeting(auctionSnapshot, { mode: "auctionInit" }); +} + +export function onBidResponseEvent( + bidResponse, + config, + userConsent, + auctionDetailsOverride, +) { + const adUnitCode = bidResponse?.adUnitCode; + if (!adUnitCode) return; + const auctionDetails = + auctionDetailsOverride || + buildAuctionDetailsFromAuction( + auctionManager.index.getAuction(bidResponse || {}), + ); + if (!isFlooringEnforcedForAuction(auctionDetails)) { + logInfo(LOG_PREFIX, "skipping targeting; floor enforcement is disabled"); + return; + } + const auctionSnapshot = extractAuctionSnapshot(auctionDetails); + if (!auctionSnapshot.adUnitCodes?.includes(adUnitCode)) { + auctionSnapshot.adUnitCodes = [ + ...(auctionSnapshot.adUnitCodes || []), + adUnitCode, + ]; + } + applyRuntimeTargeting(auctionSnapshot, { mode: "bidResponse", bidResponse }); +} + +export function init(moduleConfig) { + moduleParams = moduleConfig?.params || {}; + if (moduleParams?.runtimeScriptUrl) { + loadRuntimeScript(); + } else { + logInfo( + LOG_PREFIX, + "runtimeScriptUrl not provided; runtime script will not load", + ); + } + return true; +} + +export const mileRtdSubmodule = { + name: SUBMODULE_NAME, + init, + onAuctionInitEvent, + onBidResponseEvent, +}; + +export const __testing__ = { + setModuleParams(params) { + moduleParams = params || {}; + engineLoadPromise = null; + }, +}; + +submodule(MODULE_NAME, mileRtdSubmodule); diff --git a/modules/mileRtdProvider.md b/modules/mileRtdProvider.md new file mode 100644 index 00000000000..7e165e16c75 --- /dev/null +++ b/modules/mileRtdProvider.md @@ -0,0 +1,70 @@ +# Mile RTD Provider + +## Overview + +The `mile` RTD provider computes per-slot targeting values through a runtime engine and sets GPT slot targeting used by floor logic. + +It sets a single targeting key: + +- `mile_rtd` + +The value is provider-specific and is returned by `window[params.runtimeGlobalName].getMileTargetingByAdUnit(...)` + +## When targeting is applied + +Targeting is applied during: + +- `onAuctionInitEvent` +- `onBidResponseEvent` + +The provider only applies targeting when flooring is enforced for the auction. + +If flooring is not enforced, no `mile_rtd` targeting is set. + +## Key-value mapping + +The runtime engine returns a map keyed by ad unit identifier (slot element ID or ad unit path), and each resolved slot gets: + +- key: `mile_rtd` +- value: `targetingByAdUnit[slotElementId]` or `targetingByAdUnit[adUnitPath]` + +Example runtime response: + +```js +{ + "div-gpt-ad-123": "segA_floorHigh", + "/1234567/homepage/top": "segB_floorMid" +} +``` + +Resulting GPT slot targeting: + +```js +slot.setTargeting("mile_rtd", "segA_floorHigh"); +``` + +## Configuration + +Use the RTD module with provider name `mile`: + +```js +pbjs.setConfig({ + realTimeData: { + dataProviders: [ + { + name: "mile", + waitForIt: false, + params: { + runtimeScriptUrl: "https://cdn.example.com/mile-rtd-runtime.js", + runtimeGlobalName: "mileRtdRuntime", // optional, default shown + }, + }, + ], + }, +}); +``` + +### Params + +- `runtimeScriptUrl` (optional): URL of runtime script to load. +- `runtimeGlobalName` (optional): global object name exposing `getMileTargetingByAdUnit`; defaults to `mileRtdRuntime`. diff --git a/plugins/eslint/approvedLoadExternalScriptPaths.js b/plugins/eslint/approvedLoadExternalScriptPaths.js index 0eab8545924..91841ae24dc 100644 --- a/plugins/eslint/approvedLoadExternalScriptPaths.js +++ b/plugins/eslint/approvedLoadExternalScriptPaths.js @@ -33,6 +33,7 @@ const APPROVED_LOAD_EXTERNAL_SCRIPT_PATHS = [ 'modules/optableRtdProvider.js', 'modules/oftmediaRtdProvider.js', 'modules/panxoRtdProvider.js', + 'modules/mileRtdProvider.js', // UserId Submodules 'modules/justIdSystem.js', 'modules/tncIdSystem.js', diff --git a/test/spec/modules/mileRtdProvider_spec.js b/test/spec/modules/mileRtdProvider_spec.js new file mode 100644 index 00000000000..73ff12ab91b --- /dev/null +++ b/test/spec/modules/mileRtdProvider_spec.js @@ -0,0 +1,316 @@ +import { + __testing__, + getTargetingFromRuntime, + init, + loadRuntimeScript, + mileRtdSubmodule, + onAuctionInitEvent, + onBidResponseEvent, + setSlotTargeting, +} from "modules/mileRtdProvider.js"; +import { loadExternalScriptStub } from "test/mocks/adloaderStub.js"; + +describe("mile RTD provider", function () { + let sandbox; + + function flushPromises() { + return new Promise((resolve) => setTimeout(resolve, 0)); + } + + function createMockSlot({ elementId, adUnitPath } = {}) { + return { + getSlotElementId: sandbox.stub().returns(elementId), + getAdUnitPath: sandbox.stub().returns(adUnitPath), + setTargeting: sandbox.spy(), + }; + } + + beforeEach(function () { + sandbox = sinon.createSandbox(); + __testing__.setModuleParams({}); + delete window.mileRtdRuntime; + delete window.customRuntimeGlobal; + delete window.mileRtdRuntimeUtils; + delete window.googletag; + }); + + afterEach(function () { + sandbox.restore(); + __testing__.setModuleParams({}); + delete window.mileRtdRuntime; + delete window.customRuntimeGlobal; + delete window.mileRtdRuntimeUtils; + delete window.googletag; + }); + + describe("mileRtdSubmodule", function () { + it("registers with expected name", function () { + expect(mileRtdSubmodule.name).to.equal("mile"); + }); + }); + + describe("init / runtime loading", function () { + it("returns true and does not load script when runtimeScriptUrl is missing", function () { + expect(init({ params: {} })).to.equal(true); + expect(loadExternalScriptStub.called).to.equal(false); + }); + + it("loads runtime script only once when loadRuntimeScript is called repeatedly", async function () { + __testing__.setModuleParams({ + runtimeScriptUrl: "https://cdn.example.com/mile-runtime.js", + }); + + await Promise.all([loadRuntimeScript(), loadRuntimeScript()]); + expect(loadExternalScriptStub.calledOnce).to.equal(true); + expect(loadExternalScriptStub.firstCall.args[0]).to.equal( + "https://cdn.example.com/mile-runtime.js", + ); + }); + + it("init loads runtime script when runtimeScriptUrl is provided", function () { + init({ + params: { + runtimeScriptUrl: "https://cdn.example.com/mile-runtime.js", + }, + }); + + expect(loadExternalScriptStub.calledOnce).to.equal(true); + expect(loadExternalScriptStub.firstCall.args[0]).to.equal( + "https://cdn.example.com/mile-runtime.js", + ); + }); + }); + + describe("getTargetingFromRuntime", function () { + it("resolves null when runtime global is missing", async function () { + const result = await getTargetingFromRuntime({ adUnitCodes: ["ad-1"] }); + expect(result).to.equal(null); + }); + + it("uses default runtime global when available", async function () { + const runtimeResult = { "div-gpt-ad-1": "segment_a" }; + const runtimeStub = sandbox.stub().returns(runtimeResult); + window.mileRtdRuntime = { + getMileTargetingByAdUnit: runtimeStub, + }; + + const snapshot = { adUnitCodes: ["ad-1"] }; + const context = { mode: "auctionInit" }; + const result = await getTargetingFromRuntime(snapshot, context); + + expect(result).to.deep.equal(runtimeResult); + expect(runtimeStub.calledOnceWith(snapshot, context)).to.equal(true); + }); + + it("uses custom runtime global name when configured", async function () { + __testing__.setModuleParams({ runtimeGlobalName: "customRuntimeGlobal" }); + const runtimeResult = { "div-gpt-ad-2": "segment_b" }; + window.customRuntimeGlobal = { + getMileTargetingByAdUnit: sandbox.stub().returns(runtimeResult), + }; + + const result = await getTargetingFromRuntime({ adUnitCodes: ["ad-2"] }); + expect(result).to.deep.equal(runtimeResult); + }); + + it("resolves null when runtime function throws", async function () { + window.mileRtdRuntime = { + getMileTargetingByAdUnit: sandbox + .stub() + .throws(new Error("runtime failed")), + }; + + const result = await getTargetingFromRuntime({ adUnitCodes: ["ad-1"] }); + expect(result).to.equal(null); + }); + }); + + describe("setSlotTargeting", function () { + it("returns false when GPT is unavailable", function () { + const result = setSlotTargeting({ "div-gpt-ad-1": "value-1" }, null); + expect(result).to.equal(false); + }); + + it("sets mile_rtd targeting by slot element id and ad unit path", function () { + const slotWithElementId = createMockSlot({ + elementId: "div-gpt-ad-1", + adUnitPath: "/123/home/top", + }); + const slotWithPathFallback = createMockSlot({ + elementId: "unknown-slot", + adUnitPath: "/123/home/path-only", + }); + const slotWithoutMatch = createMockSlot({ + elementId: "unknown-2", + adUnitPath: "/123/home/no-match", + }); + + const googletag = { + cmd: { + push: (fn) => fn(), + }, + pubads: sandbox.stub().returns({ + getSlots: sandbox + .stub() + .returns([ + slotWithElementId, + slotWithPathFallback, + slotWithoutMatch, + ]), + }), + }; + + const result = setSlotTargeting( + { + "div-gpt-ad-1": "segment_a", + "/123/home/path-only": "segment_b", + }, + googletag, + ); + + expect(result).to.equal(true); + expect( + slotWithElementId.setTargeting.calledOnceWith("mile_rtd", "segment_a"), + ).to.equal(true); + expect( + slotWithPathFallback.setTargeting.calledOnceWith( + "mile_rtd", + "segment_b", + ), + ).to.equal(true); + expect(slotWithoutMatch.setTargeting.called).to.equal(false); + }); + }); + + describe("onAuctionInitEvent", function () { + it("skips runtime targeting when flooring is not enforced", async function () { + const runtimeStub = sandbox + .stub() + .returns({ "div-gpt-ad-1": "segment_a" }); + window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; + + onAuctionInitEvent({ + adUnitCodes: ["ad-1"], + bidderRequests: [ + { bids: [{ floorData: { enforcements: { enforceJS: false } } }] }, + ], + }); + await flushPromises(); + + expect(runtimeStub.called).to.equal(false); + }); + + it("extracts snapshot and applies targeting when flooring is enforced", async function () { + const runtimeStub = sandbox + .stub() + .returns({ "div-gpt-ad-1": "segment_a" }); + const slot = createMockSlot({ + elementId: "div-gpt-ad-1", + adUnitPath: "/123/home/top", + }); + window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; + window.mileRtdRuntimeUtils = { + extractAuctionSnapshot: sandbox + .stub() + .returns({ adUnitCodes: ["ad-1"] }), + }; + window.googletag = { + cmd: { push: (fn) => fn() }, + pubads: sandbox + .stub() + .returns({ getSlots: sandbox.stub().returns([slot]) }), + }; + + onAuctionInitEvent({ + adUnitCodes: ["ad-1"], + bidderRequests: [ + { bids: [{ floorData: { enforcements: { enforceJS: true } } }] }, + ], + }); + await flushPromises(); + + expect( + window.mileRtdRuntimeUtils.extractAuctionSnapshot.calledOnce, + ).to.equal(true); + expect(runtimeStub.calledOnce).to.equal(true); + expect(runtimeStub.firstCall.args[1]).to.deep.equal({ + mode: "auctionInit", + }); + expect( + slot.setTargeting.calledOnceWith("mile_rtd", "segment_a"), + ).to.equal(true); + }); + }); + + describe("onBidResponseEvent", function () { + it("does nothing when bidResponse lacks adUnitCode", async function () { + const runtimeStub = sandbox.stub().returns({}); + window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; + + onBidResponseEvent( + {}, + {}, + {}, + { + adUnitCodes: ["ad-1"], + bidderRequests: [ + { bids: [{ floorData: { enforcements: { enforceJS: true } } }] }, + ], + }, + ); + await flushPromises(); + + expect(runtimeStub.called).to.equal(false); + }); + + it("skips runtime targeting when flooring is not enforced", async function () { + const runtimeStub = sandbox.stub().returns({}); + window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; + + onBidResponseEvent( + { adUnitCode: "ad-1" }, + {}, + {}, + { + adUnitCodes: ["ad-1"], + bidderRequests: [ + { bids: [{ floorData: { enforcements: { enforceJS: false } } }] }, + ], + }, + ); + await flushPromises(); + + expect(runtimeStub.called).to.equal(false); + }); + + it("adds adUnitCode to snapshot and calls runtime with bidResponse context", async function () { + const runtimeStub = sandbox.stub().returns({}); + window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; + window.mileRtdRuntimeUtils = { + extractAuctionSnapshot: sandbox + .stub() + .returns({ adUnitCodes: ["ad-1"] }), + }; + + const bidResponse = { adUnitCode: "ad-2", requestId: "request-1" }; + onBidResponseEvent( + bidResponse, + {}, + {}, + { + adUnitCodes: ["ad-1"], + bidderRequests: [ + { bids: [{ floorData: { enforcements: { enforceJS: true } } }] }, + ], + }, + ); + await flushPromises(); + + expect(runtimeStub.calledOnce).to.equal(true); + const [snapshot, context] = runtimeStub.firstCall.args; + expect(snapshot.adUnitCodes).to.deep.equal(["ad-1", "ad-2"]); + expect(context.mode).to.equal("bidResponse"); + expect(context.bidResponse).to.equal(bidResponse); + }); + }); +}); From 8afc14454b8201942a6ed3627fadbfee6a0c7adc Mon Sep 17 00:00:00 2001 From: JP Mohan Date: Wed, 1 Apr 2026 19:01:39 +0530 Subject: [PATCH 2/4] removing the isFlooringEnforcedForAuction gate --- modules/mileRtdProvider.js | 116 ++++++++----------------------------- 1 file changed, 23 insertions(+), 93 deletions(-) diff --git a/modules/mileRtdProvider.js b/modules/mileRtdProvider.js index 508b265fe2d..8b81374d6ac 100644 --- a/modules/mileRtdProvider.js +++ b/modules/mileRtdProvider.js @@ -17,29 +17,6 @@ const DEFAULT_ENGINE_GLOBAL = "mileRtdRuntime"; let moduleParams = {}; let engineLoadPromise = null; -function isFlooringEnforcedForAuction(auctionDetails = {}) { - const bidderRequestBids = (auctionDetails?.bidderRequests || []).flatMap( - (request) => request?.bids || [], - ); - const bidsReceived = auctionDetails?.bidsReceived || []; - const allBids = [...bidderRequestBids, ...bidsReceived]; - return allBids.some((bid) => { - const floorData = bid?.floorData; - if (!floorData) return false; - - const enforceJS = floorData?.enforcements?.enforceJS; - if (typeof enforceJS === "boolean") { - return enforceJS; - } - - // Some floor data payloads only expose signal/skip flags. - if (floorData?.skipped === false && floorData?.noFloorSignaled === false) { - return true; - } - return false; - }); -} - function getRuntimeEngine() { const globalName = moduleParams?.runtimeGlobalName || DEFAULT_ENGINE_GLOBAL; return window?.[globalName]; @@ -51,15 +28,10 @@ export function loadRuntimeScript() { if (!runtimeScriptUrl) return Promise.resolve(false); engineLoadPromise = new Promise((resolve) => { - loadExternalScript( - runtimeScriptUrl, - MODULE_TYPE_RTD, - SUBMODULE_NAME, - () => { - logInfo(LOG_PREFIX, "runtime script loaded", runtimeScriptUrl); - resolve(true); - }, - ); + loadExternalScript(runtimeScriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, () => { + logInfo(LOG_PREFIX, "runtime script loaded", runtimeScriptUrl); + resolve(true); + }); }).catch((error) => { logError(LOG_PREFIX, "unable to load runtime script", error); engineLoadPromise = null; @@ -70,31 +42,19 @@ export function loadRuntimeScript() { export function getTargetingFromRuntime(auctionSnapshot, context = {}) { const runtimeEngine = getRuntimeEngine(); - if ( - !runtimeEngine || - typeof runtimeEngine.getMileTargetingByAdUnit !== "function" - ) { + if (!runtimeEngine || typeof runtimeEngine.getMileTargetingByAdUnit !== "function") { logInfo(LOG_PREFIX, "runtime engine missing getMileTargetingByAdUnit()"); return Promise.resolve(null); } try { - return Promise.resolve( - runtimeEngine.getMileTargetingByAdUnit(auctionSnapshot, context), - ); + return Promise.resolve(runtimeEngine.getMileTargetingByAdUnit(auctionSnapshot, context)); } catch (error) { - logError( - LOG_PREFIX, - "runtime engine failed while computing targeting", - error, - ); + logError(LOG_PREFIX, "runtime engine failed while computing targeting", error); return Promise.resolve(null); } } -export function setSlotTargeting( - targetingByAdUnit, - googletag = window.googletag, -) { +export function setSlotTargeting(targetingByAdUnit, googletag = window.googletag) { if (!googletag?.cmd?.push || typeof googletag.pubads !== "function") { logInfo(LOG_PREFIX, "GPT is not available, skipping slot targeting"); return false; @@ -105,11 +65,8 @@ export function setSlotTargeting( if (typeof slot?.setTargeting !== "function") return; const slotElementId = slot.getSlotElementId?.(); const adUnitPath = slot.getAdUnitPath?.(); - const targetingValue = - targetingByAdUnit?.[slotElementId] ?? targetingByAdUnit?.[adUnitPath]; - if (targetingValue != null) { - slot.setTargeting(TARGETING_KEY, targetingValue); - } + const targetingValue = targetingByAdUnit?.[slotElementId] ?? targetingByAdUnit?.[adUnitPath]; + if (targetingValue != null) slot.setTargeting(TARGETING_KEY, targetingValue); }); }); return true; @@ -133,56 +90,32 @@ function extractAuctionSnapshot(auctionDetails = {}) { } function applyRuntimeTargeting(auctionSnapshot, context = {}) { - return getTargetingFromRuntime(auctionSnapshot, context).then( - (targetingByAdUnit) => { - if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { - setSlotTargeting(targetingByAdUnit); - } - }, - ); + return getTargetingFromRuntime(auctionSnapshot, context).then((targetingByAdUnit) => { + if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { + setSlotTargeting(targetingByAdUnit); + } + }); } export function onAuctionInitEvent(auctionDetails = {}) { - let snapshotDetails = auctionDetails; - if (!auctionDetails?.adUnitCodes?.length) { - snapshotDetails = buildAuctionDetailsFromAuction( - auctionManager.index.getAuction({ - auctionId: auctionManager.getLastAuctionId(), - }), + const snapshotDetails = auctionDetails?.adUnitCodes?.length + ? auctionDetails + : buildAuctionDetailsFromAuction( + auctionManager.index.getAuction({ auctionId: auctionManager.getLastAuctionId() }) ); - } if (!snapshotDetails?.adUnitCodes?.length) return; - if (!isFlooringEnforcedForAuction(snapshotDetails)) { - logInfo(LOG_PREFIX, "skipping targeting; floor enforcement is disabled"); - return; - } const auctionSnapshot = extractAuctionSnapshot(snapshotDetails); applyRuntimeTargeting(auctionSnapshot, { mode: "auctionInit" }); } -export function onBidResponseEvent( - bidResponse, - config, - userConsent, - auctionDetailsOverride, -) { +export function onBidResponseEvent(bidResponse, config, userConsent, auctionDetailsOverride) { const adUnitCode = bidResponse?.adUnitCode; if (!adUnitCode) return; const auctionDetails = - auctionDetailsOverride || - buildAuctionDetailsFromAuction( - auctionManager.index.getAuction(bidResponse || {}), - ); - if (!isFlooringEnforcedForAuction(auctionDetails)) { - logInfo(LOG_PREFIX, "skipping targeting; floor enforcement is disabled"); - return; - } + auctionDetailsOverride || buildAuctionDetailsFromAuction(auctionManager.index.getAuction(bidResponse || {})); const auctionSnapshot = extractAuctionSnapshot(auctionDetails); if (!auctionSnapshot.adUnitCodes?.includes(adUnitCode)) { - auctionSnapshot.adUnitCodes = [ - ...(auctionSnapshot.adUnitCodes || []), - adUnitCode, - ]; + auctionSnapshot.adUnitCodes = [...(auctionSnapshot.adUnitCodes || []), adUnitCode]; } applyRuntimeTargeting(auctionSnapshot, { mode: "bidResponse", bidResponse }); } @@ -192,10 +125,7 @@ export function init(moduleConfig) { if (moduleParams?.runtimeScriptUrl) { loadRuntimeScript(); } else { - logInfo( - LOG_PREFIX, - "runtimeScriptUrl not provided; runtime script will not load", - ); + logInfo(LOG_PREFIX, "runtimeScriptUrl not provided; runtime script will not load"); } return true; } From ec0600a55e4f46bb197eb95c96287b7642499570 Mon Sep 17 00:00:00 2001 From: JP Mohan Date: Wed, 1 Apr 2026 22:38:05 +0530 Subject: [PATCH 3/4] converted to typescript and removed the flooringEnforcedCheck inside the RTD module --- modules/mileRtdProvider.js | 147 ----------- modules/mileRtdProvider.md | 5 +- modules/mileRtdProvider.ts | 234 ++++++++++++++++++ .../eslint/approvedLoadExternalScriptPaths.js | 2 +- test/spec/modules/mileRtdProvider_spec.js | 8 +- 5 files changed, 240 insertions(+), 156 deletions(-) delete mode 100644 modules/mileRtdProvider.js create mode 100644 modules/mileRtdProvider.ts diff --git a/modules/mileRtdProvider.js b/modules/mileRtdProvider.js deleted file mode 100644 index 8b81374d6ac..00000000000 --- a/modules/mileRtdProvider.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Thin Mile RTD client. - * Pulls pre-hashed targeting values from runtime global and applies slot targeting. - */ -import { submodule } from "../src/hook.js"; -import { loadExternalScript } from "../src/adloader.js"; -import { MODULE_TYPE_RTD } from "../src/activities/modules.js"; -import { auctionManager } from "../src/auctionManager.js"; -import { logError, logInfo } from "../src/utils.js"; - -const MODULE_NAME = "realTimeData"; -const SUBMODULE_NAME = "mile"; -const TARGETING_KEY = "mile_rtd"; -const LOG_PREFIX = "[mileRtdProvider]"; -const DEFAULT_ENGINE_GLOBAL = "mileRtdRuntime"; - -let moduleParams = {}; -let engineLoadPromise = null; - -function getRuntimeEngine() { - const globalName = moduleParams?.runtimeGlobalName || DEFAULT_ENGINE_GLOBAL; - return window?.[globalName]; -} - -export function loadRuntimeScript() { - if (engineLoadPromise) return engineLoadPromise; - const runtimeScriptUrl = moduleParams?.runtimeScriptUrl; - if (!runtimeScriptUrl) return Promise.resolve(false); - - engineLoadPromise = new Promise((resolve) => { - loadExternalScript(runtimeScriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, () => { - logInfo(LOG_PREFIX, "runtime script loaded", runtimeScriptUrl); - resolve(true); - }); - }).catch((error) => { - logError(LOG_PREFIX, "unable to load runtime script", error); - engineLoadPromise = null; - return false; - }); - return engineLoadPromise; -} - -export function getTargetingFromRuntime(auctionSnapshot, context = {}) { - const runtimeEngine = getRuntimeEngine(); - if (!runtimeEngine || typeof runtimeEngine.getMileTargetingByAdUnit !== "function") { - logInfo(LOG_PREFIX, "runtime engine missing getMileTargetingByAdUnit()"); - return Promise.resolve(null); - } - try { - return Promise.resolve(runtimeEngine.getMileTargetingByAdUnit(auctionSnapshot, context)); - } catch (error) { - logError(LOG_PREFIX, "runtime engine failed while computing targeting", error); - return Promise.resolve(null); - } -} - -export function setSlotTargeting(targetingByAdUnit, googletag = window.googletag) { - if (!googletag?.cmd?.push || typeof googletag.pubads !== "function") { - logInfo(LOG_PREFIX, "GPT is not available, skipping slot targeting"); - return false; - } - googletag.cmd.push(() => { - const slots = googletag.pubads()?.getSlots?.() || []; - slots.forEach((slot) => { - if (typeof slot?.setTargeting !== "function") return; - const slotElementId = slot.getSlotElementId?.(); - const adUnitPath = slot.getAdUnitPath?.(); - const targetingValue = targetingByAdUnit?.[slotElementId] ?? targetingByAdUnit?.[adUnitPath]; - if (targetingValue != null) slot.setTargeting(TARGETING_KEY, targetingValue); - }); - }); - return true; -} - -function buildAuctionDetailsFromAuction(auction) { - return { - adUnitCodes: auction?.getAdUnitCodes?.() || [], - adUnits: auction?.getAdUnits?.() || [], - bidderRequests: auction?.getBidRequests?.() || [], - bidsReceived: auction?.getBidsReceived?.() || [], - }; -} - -function extractAuctionSnapshot(auctionDetails = {}) { - const extractSnapshot = window?.mileRtdRuntimeUtils?.extractAuctionSnapshot; - if (typeof extractSnapshot === "function") { - return extractSnapshot(auctionDetails); - } - return { adUnitCodes: auctionDetails?.adUnitCodes || [] }; -} - -function applyRuntimeTargeting(auctionSnapshot, context = {}) { - return getTargetingFromRuntime(auctionSnapshot, context).then((targetingByAdUnit) => { - if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { - setSlotTargeting(targetingByAdUnit); - } - }); -} - -export function onAuctionInitEvent(auctionDetails = {}) { - const snapshotDetails = auctionDetails?.adUnitCodes?.length - ? auctionDetails - : buildAuctionDetailsFromAuction( - auctionManager.index.getAuction({ auctionId: auctionManager.getLastAuctionId() }) - ); - if (!snapshotDetails?.adUnitCodes?.length) return; - const auctionSnapshot = extractAuctionSnapshot(snapshotDetails); - applyRuntimeTargeting(auctionSnapshot, { mode: "auctionInit" }); -} - -export function onBidResponseEvent(bidResponse, config, userConsent, auctionDetailsOverride) { - const adUnitCode = bidResponse?.adUnitCode; - if (!adUnitCode) return; - const auctionDetails = - auctionDetailsOverride || buildAuctionDetailsFromAuction(auctionManager.index.getAuction(bidResponse || {})); - const auctionSnapshot = extractAuctionSnapshot(auctionDetails); - if (!auctionSnapshot.adUnitCodes?.includes(adUnitCode)) { - auctionSnapshot.adUnitCodes = [...(auctionSnapshot.adUnitCodes || []), adUnitCode]; - } - applyRuntimeTargeting(auctionSnapshot, { mode: "bidResponse", bidResponse }); -} - -export function init(moduleConfig) { - moduleParams = moduleConfig?.params || {}; - if (moduleParams?.runtimeScriptUrl) { - loadRuntimeScript(); - } else { - logInfo(LOG_PREFIX, "runtimeScriptUrl not provided; runtime script will not load"); - } - return true; -} - -export const mileRtdSubmodule = { - name: SUBMODULE_NAME, - init, - onAuctionInitEvent, - onBidResponseEvent, -}; - -export const __testing__ = { - setModuleParams(params) { - moduleParams = params || {}; - engineLoadPromise = null; - }, -}; - -submodule(MODULE_NAME, mileRtdSubmodule); diff --git a/modules/mileRtdProvider.md b/modules/mileRtdProvider.md index 7e165e16c75..e6eb2ba4135 100644 --- a/modules/mileRtdProvider.md +++ b/modules/mileRtdProvider.md @@ -2,7 +2,7 @@ ## Overview -The `mile` RTD provider computes per-slot targeting values through a runtime engine and sets GPT slot targeting used by floor logic. +The `mile` RTD provider computes per-slot targeting values through a runtime engine and sets GPT slot targeting used by GAM unified Pricing rules. It sets a single targeting key: @@ -17,9 +17,6 @@ Targeting is applied during: - `onAuctionInitEvent` - `onBidResponseEvent` -The provider only applies targeting when flooring is enforced for the auction. - -If flooring is not enforced, no `mile_rtd` targeting is set. ## Key-value mapping diff --git a/modules/mileRtdProvider.ts b/modules/mileRtdProvider.ts new file mode 100644 index 00000000000..d60805482e6 --- /dev/null +++ b/modules/mileRtdProvider.ts @@ -0,0 +1,234 @@ +/** + * Thin Mile RTD client. + * Pulls pre-hashed targeting values from runtime global and applies slot targeting. + */ +import { submodule } from '../src/hook.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { logError, logInfo } from '../src/utils.js'; +import type { Bid } from '../src/bidfactory.ts'; +import type { AllConsentData } from '../src/consentHandler.ts'; +import type { RTDProviderConfig, RtdProviderSpec } from './rtdModule/spec.ts'; + +type ModuleParams = { + runtimeScriptUrl?: string; + runtimeGlobalName?: string; +}; + +declare module './rtdModule/spec' { + interface ProviderConfig { + mile: { + params?: ModuleParams; + }; + } +} + +type TargetingValue = string | number | boolean | Array; +type TargetingByAdUnit = Record; +type RuntimeContext = { + mode: 'auctionInit' | 'bidResponse'; + bidResponse?: Bid; +}; + +type AuctionSnapshot = { + adUnitCodes: string[]; + [key: string]: unknown; +}; + +type AuctionDetails = AuctionSnapshot & { + adUnits: unknown[]; + bidderRequests: unknown[]; + bidsReceived: unknown[]; +}; + +type AuctionLike = { + getAdUnitCodes?: () => string[]; + getAdUnits?: () => unknown[]; + getBidRequests?: () => unknown[]; + getBidsReceived?: () => unknown[]; +}; + +type MileRuntimeEngine = { + getMileTargetingByAdUnit: ( + auctionSnapshot: AuctionSnapshot, + context: RuntimeContext + ) => TargetingByAdUnit | null | Promise; +}; + +type ExtractAuctionSnapshot = (auctionDetails: AuctionDetails) => AuctionSnapshot; + +type MileRuntimeUtils = { + extractAuctionSnapshot?: ExtractAuctionSnapshot; +}; +type MileRtdProviderSpec = RtdProviderSpec<'mile'> & { + onAuctionInitEvent?: (auctionDetails: Partial) => void; + onBidResponseEvent?: ( + bidResponse: Partial, + config: RTDProviderConfig<'mile'>, + userConsent: AllConsentData + ) => void; +}; +type GoogletagSlot = { + getSlotElementId?: () => string; + getAdUnitPath?: () => string; + setTargeting?: (key: string, value: TargetingValue) => void; +}; + +type Googletag = { + cmd?: { push?: (fn: () => void) => void }; + pubads?: () => { getSlots?: () => GoogletagSlot[] }; +}; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'mile'; +const TARGETING_KEY = 'mile_rtd'; +const LOG_PREFIX = '[mileRtdProvider]'; +const DEFAULT_ENGINE_GLOBAL = 'mileRtdRuntime'; + +let moduleParams: ModuleParams = {}; +let engineLoadPromise: Promise | null = null; + +function isRuntimeEngine(value: unknown): value is MileRuntimeEngine { + return !!value && typeof (value as MileRuntimeEngine).getMileTargetingByAdUnit === 'function'; +} + +function getRuntimeEngine(): MileRuntimeEngine | null { + const globalName = moduleParams.runtimeGlobalName || DEFAULT_ENGINE_GLOBAL; + const runtimeEngine = (window as unknown as Record)?.[globalName]; + return isRuntimeEngine(runtimeEngine) ? runtimeEngine : null; +} + +export function loadRuntimeScript(): Promise { + if (engineLoadPromise) return engineLoadPromise; + const runtimeScriptUrl = moduleParams.runtimeScriptUrl; + if (!runtimeScriptUrl) return Promise.resolve(false); + + engineLoadPromise = new Promise((resolve) => { + loadExternalScript(runtimeScriptUrl, MODULE_TYPE_RTD, SUBMODULE_NAME, () => { + logInfo(LOG_PREFIX, 'runtime script loaded', runtimeScriptUrl); + resolve(true); + }, undefined, undefined); + }).catch((error: unknown) => { + logError(LOG_PREFIX, 'unable to load runtime script', error); + engineLoadPromise = null; + return false; + }); + return engineLoadPromise; +} + +export function getTargetingFromRuntime( + auctionSnapshot: AuctionSnapshot, + context: RuntimeContext = { mode: 'auctionInit' } +): Promise { + const runtimeEngine = getRuntimeEngine(); + if (!runtimeEngine) { + logInfo(LOG_PREFIX, 'runtime engine missing getMileTargetingByAdUnit()'); + return Promise.resolve(null); + } + try { + return Promise.resolve(runtimeEngine.getMileTargetingByAdUnit(auctionSnapshot, context)); + } catch (error: unknown) { + logError(LOG_PREFIX, 'runtime engine failed while computing targeting', error); + return Promise.resolve(null); + } +} + +export function setSlotTargeting( + targetingByAdUnit: TargetingByAdUnit, + googletag: Googletag | undefined = window.googletag as Googletag | undefined +): boolean { + if (!googletag?.cmd?.push || typeof googletag.pubads !== 'function') { + logInfo(LOG_PREFIX, 'GPT is not available, skipping slot targeting'); + return false; + } + googletag.cmd.push(() => { + const slots = googletag.pubads?.()?.getSlots?.() || []; + slots.forEach((slot) => { + if (typeof slot?.setTargeting !== 'function') return; + const slotElementId = slot.getSlotElementId?.(); + const adUnitPath = slot.getAdUnitPath?.(); + const targetingValue = (slotElementId && targetingByAdUnit[slotElementId]) ?? (adUnitPath && targetingByAdUnit[adUnitPath]); + if (targetingValue != null) slot.setTargeting(TARGETING_KEY, targetingValue); + }); + }); + return true; +} + +function buildAuctionDetailsFromAuction(auction: AuctionLike | undefined): AuctionDetails { + return { + adUnitCodes: auction?.getAdUnitCodes?.() || [], + adUnits: auction?.getAdUnits?.() || [], + bidderRequests: auction?.getBidRequests?.() || [], + bidsReceived: auction?.getBidsReceived?.() || [], + }; +} + +function extractAuctionSnapshot(auctionDetails: AuctionDetails): AuctionSnapshot { + const extractSnapshot = ((window as unknown as { mileRtdRuntimeUtils?: MileRuntimeUtils }).mileRtdRuntimeUtils)?.extractAuctionSnapshot; + if (typeof extractSnapshot === 'function') { + return extractSnapshot(auctionDetails); + } + return { adUnitCodes: auctionDetails.adUnitCodes || [] }; +} + +function applyRuntimeTargeting(auctionSnapshot: AuctionSnapshot, context: RuntimeContext): Promise { + return getTargetingFromRuntime(auctionSnapshot, context).then((targetingByAdUnit) => { + if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { + setSlotTargeting(targetingByAdUnit); + } + }); +} + +export function onAuctionInitEvent(auctionDetails: Partial = {}): void { + const snapshotDetails = auctionDetails.adUnitCodes?.length + ? (auctionDetails as AuctionDetails) + : buildAuctionDetailsFromAuction( + auctionManager.index.getAuction({ auctionId: auctionManager.getLastAuctionId() }) as AuctionLike + ); + if (!snapshotDetails.adUnitCodes?.length) return; + const auctionSnapshot = extractAuctionSnapshot(snapshotDetails); + applyRuntimeTargeting(auctionSnapshot, { mode: 'auctionInit' }); +} + +export function onBidResponseEvent( + bidResponse: Partial, + _config: RTDProviderConfig<'mile'>, + _userConsent: AllConsentData, + auctionDetailsOverride?: Partial +): void { + const adUnitCode = bidResponse?.adUnitCode; + if (!adUnitCode) return; + const auctionDetails = auctionDetailsOverride || buildAuctionDetailsFromAuction(auctionManager.index.getAuction(bidResponse) as AuctionLike); + const auctionSnapshot = extractAuctionSnapshot(auctionDetails as AuctionDetails); + if (!auctionSnapshot.adUnitCodes?.includes(adUnitCode)) { + auctionSnapshot.adUnitCodes = [...(auctionSnapshot.adUnitCodes || []), adUnitCode]; + } + applyRuntimeTargeting(auctionSnapshot, { mode: 'bidResponse', bidResponse: bidResponse as Bid }); +} + +export function init(moduleConfig: RTDProviderConfig<'mile'>): boolean { + moduleParams = moduleConfig?.params || {}; + if (moduleParams.runtimeScriptUrl) { + loadRuntimeScript(); + } else { + logInfo(LOG_PREFIX, 'runtimeScriptUrl not provided; runtime script will not load'); + } + return true; +} + +export const mileRtdSubmodule: MileRtdProviderSpec = { + name: SUBMODULE_NAME, + init, + onAuctionInitEvent, + onBidResponseEvent: (bidResponse, config, userConsent) => onBidResponseEvent(bidResponse, config, userConsent), +}; + +export const __testing__ = { + setModuleParams(params?: ModuleParams): void { + moduleParams = params || {}; + engineLoadPromise = null; + }, +}; + +submodule(MODULE_NAME, mileRtdSubmodule); diff --git a/plugins/eslint/approvedLoadExternalScriptPaths.js b/plugins/eslint/approvedLoadExternalScriptPaths.js index 91841ae24dc..2ee1905d03c 100644 --- a/plugins/eslint/approvedLoadExternalScriptPaths.js +++ b/plugins/eslint/approvedLoadExternalScriptPaths.js @@ -33,7 +33,7 @@ const APPROVED_LOAD_EXTERNAL_SCRIPT_PATHS = [ 'modules/optableRtdProvider.js', 'modules/oftmediaRtdProvider.js', 'modules/panxoRtdProvider.js', - 'modules/mileRtdProvider.js', + 'modules/mileRtdProvider.ts', // UserId Submodules 'modules/justIdSystem.js', 'modules/tncIdSystem.js', diff --git a/test/spec/modules/mileRtdProvider_spec.js b/test/spec/modules/mileRtdProvider_spec.js index 73ff12ab91b..f8a0835744f 100644 --- a/test/spec/modules/mileRtdProvider_spec.js +++ b/test/spec/modules/mileRtdProvider_spec.js @@ -183,7 +183,7 @@ describe("mile RTD provider", function () { }); describe("onAuctionInitEvent", function () { - it("skips runtime targeting when flooring is not enforced", async function () { + it("applies runtime targeting even when flooring is not enforced", async function () { const runtimeStub = sandbox .stub() .returns({ "div-gpt-ad-1": "segment_a" }); @@ -197,7 +197,7 @@ describe("mile RTD provider", function () { }); await flushPromises(); - expect(runtimeStub.called).to.equal(false); + expect(runtimeStub.called).to.equal(true); }); it("extracts snapshot and applies targeting when flooring is enforced", async function () { @@ -263,7 +263,7 @@ describe("mile RTD provider", function () { expect(runtimeStub.called).to.equal(false); }); - it("skips runtime targeting when flooring is not enforced", async function () { + it("applies runtime targeting even when flooring is not enforced", async function () { const runtimeStub = sandbox.stub().returns({}); window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; @@ -280,7 +280,7 @@ describe("mile RTD provider", function () { ); await flushPromises(); - expect(runtimeStub.called).to.equal(false); + expect(runtimeStub.called).to.equal(true); }); it("adds adUnitCode to snapshot and calls runtime with bidResponse context", async function () { From bf6a4fd70b180301a80637a2f144e5893addf4c1 Mon Sep 17 00:00:00 2001 From: JP Mohan Date: Fri, 8 May 2026 21:28:05 +0530 Subject: [PATCH 4/4] using adUnit.adserverTargeting --- modules/mileRtdProvider.ts | 86 ++++++++++------ test/spec/modules/mileRtdProvider_spec.js | 115 +++++++++++----------- 2 files changed, 113 insertions(+), 88 deletions(-) diff --git a/modules/mileRtdProvider.ts b/modules/mileRtdProvider.ts index d60805482e6..ff398343283 100644 --- a/modules/mileRtdProvider.ts +++ b/modules/mileRtdProvider.ts @@ -1,6 +1,7 @@ /** * Thin Mile RTD client. - * Pulls pre-hashed targeting values from runtime global and applies slot targeting. + * Pulls pre-hashed targeting values from runtime global and merges them into + * ad unit adserver targeting. */ import { submodule } from '../src/hook.js'; import { loadExternalScript } from '../src/adloader.js'; @@ -49,6 +50,19 @@ type AuctionLike = { getBidsReceived?: () => unknown[]; }; +type AdUnitLike = { + code?: string; + ortb2Imp?: { + ext?: { + gpid?: string; + data?: { + pbadslot?: string; + }; + }; + }; + adserverTargeting?: Record; +}; + type MileRuntimeEngine = { getMileTargetingByAdUnit: ( auctionSnapshot: AuctionSnapshot, @@ -69,16 +83,6 @@ type MileRtdProviderSpec = RtdProviderSpec<'mile'> & { userConsent: AllConsentData ) => void; }; -type GoogletagSlot = { - getSlotElementId?: () => string; - getAdUnitPath?: () => string; - setTargeting?: (key: string, value: TargetingValue) => void; -}; - -type Googletag = { - cmd?: { push?: (fn: () => void) => void }; - pubads?: () => { getSlots?: () => GoogletagSlot[] }; -}; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'mile'; @@ -134,25 +138,43 @@ export function getTargetingFromRuntime( } } -export function setSlotTargeting( +function getAdUnitTargetingValue(adUnit: AdUnitLike, targetingByAdUnit: TargetingByAdUnit): TargetingValue | null { + const targetingIdentifiers = [ + adUnit.code, + adUnit.ortb2Imp?.ext?.gpid, + adUnit.ortb2Imp?.ext?.data?.pbadslot, + ]; + + for (const identifier of targetingIdentifiers) { + if (identifier != null && targetingByAdUnit[identifier] != null) { + return targetingByAdUnit[identifier]; + } + } + + return null; +} + +export function setAdUnitTargeting( targetingByAdUnit: TargetingByAdUnit, - googletag: Googletag | undefined = window.googletag as Googletag | undefined + adUnits: AdUnitLike[] = [] ): boolean { - if (!googletag?.cmd?.push || typeof googletag.pubads !== 'function') { - logInfo(LOG_PREFIX, 'GPT is not available, skipping slot targeting'); + if (!adUnits.length) { + logInfo(LOG_PREFIX, 'auction ad units are unavailable, skipping adserver targeting'); return false; } - googletag.cmd.push(() => { - const slots = googletag.pubads?.()?.getSlots?.() || []; - slots.forEach((slot) => { - if (typeof slot?.setTargeting !== 'function') return; - const slotElementId = slot.getSlotElementId?.(); - const adUnitPath = slot.getAdUnitPath?.(); - const targetingValue = (slotElementId && targetingByAdUnit[slotElementId]) ?? (adUnitPath && targetingByAdUnit[adUnitPath]); - if (targetingValue != null) slot.setTargeting(TARGETING_KEY, targetingValue); + + let appliedTargeting = false; + adUnits.forEach((adUnit) => { + const targetingValue = getAdUnitTargetingValue(adUnit, targetingByAdUnit); + if (targetingValue == null) return; + + adUnit.adserverTargeting = Object.assign(adUnit.adserverTargeting || {}, { + [TARGETING_KEY]: targetingValue, }); + appliedTargeting = true; }); - return true; + + return appliedTargeting; } function buildAuctionDetailsFromAuction(auction: AuctionLike | undefined): AuctionDetails { @@ -172,10 +194,14 @@ function extractAuctionSnapshot(auctionDetails: AuctionDetails): AuctionSnapshot return { adUnitCodes: auctionDetails.adUnitCodes || [] }; } -function applyRuntimeTargeting(auctionSnapshot: AuctionSnapshot, context: RuntimeContext): Promise { +function applyRuntimeTargeting( + auctionSnapshot: AuctionSnapshot, + context: RuntimeContext, + adUnits: AdUnitLike[] = [] +): Promise { return getTargetingFromRuntime(auctionSnapshot, context).then((targetingByAdUnit) => { if (targetingByAdUnit && Object.keys(targetingByAdUnit).length > 0) { - setSlotTargeting(targetingByAdUnit); + setAdUnitTargeting(targetingByAdUnit, adUnits); } }); } @@ -188,7 +214,7 @@ export function onAuctionInitEvent(auctionDetails: Partial = {}) ); if (!snapshotDetails.adUnitCodes?.length) return; const auctionSnapshot = extractAuctionSnapshot(snapshotDetails); - applyRuntimeTargeting(auctionSnapshot, { mode: 'auctionInit' }); + applyRuntimeTargeting(auctionSnapshot, { mode: 'auctionInit' }, snapshotDetails.adUnits as AdUnitLike[]); } export function onBidResponseEvent( @@ -204,7 +230,11 @@ export function onBidResponseEvent( if (!auctionSnapshot.adUnitCodes?.includes(adUnitCode)) { auctionSnapshot.adUnitCodes = [...(auctionSnapshot.adUnitCodes || []), adUnitCode]; } - applyRuntimeTargeting(auctionSnapshot, { mode: 'bidResponse', bidResponse: bidResponse as Bid }); + applyRuntimeTargeting( + auctionSnapshot, + { mode: 'bidResponse', bidResponse: bidResponse as Bid }, + auctionDetails.adUnits as AdUnitLike[] + ); } export function init(moduleConfig: RTDProviderConfig<'mile'>): boolean { diff --git a/test/spec/modules/mileRtdProvider_spec.js b/test/spec/modules/mileRtdProvider_spec.js index f8a0835744f..16d3c71772d 100644 --- a/test/spec/modules/mileRtdProvider_spec.js +++ b/test/spec/modules/mileRtdProvider_spec.js @@ -6,7 +6,7 @@ import { mileRtdSubmodule, onAuctionInitEvent, onBidResponseEvent, - setSlotTargeting, + setAdUnitTargeting, } from "modules/mileRtdProvider.js"; import { loadExternalScriptStub } from "test/mocks/adloaderStub.js"; @@ -17,11 +17,18 @@ describe("mile RTD provider", function () { return new Promise((resolve) => setTimeout(resolve, 0)); } - function createMockSlot({ elementId, adUnitPath } = {}) { + function createMockAdUnit({ code, gpid, pbadslot, adserverTargeting } = {}) { return { - getSlotElementId: sandbox.stub().returns(elementId), - getAdUnitPath: sandbox.stub().returns(adUnitPath), - setTargeting: sandbox.spy(), + code, + ortb2Imp: { + ext: { + gpid, + data: { + pbadslot, + }, + }, + }, + adserverTargeting, }; } @@ -31,7 +38,6 @@ describe("mile RTD provider", function () { delete window.mileRtdRuntime; delete window.customRuntimeGlobal; delete window.mileRtdRuntimeUtils; - delete window.googletag; }); afterEach(function () { @@ -40,7 +46,6 @@ describe("mile RTD provider", function () { delete window.mileRtdRuntime; delete window.customRuntimeGlobal; delete window.mileRtdRuntimeUtils; - delete window.googletag; }); describe("mileRtdSubmodule", function () { @@ -125,60 +130,57 @@ describe("mile RTD provider", function () { }); }); - describe("setSlotTargeting", function () { - it("returns false when GPT is unavailable", function () { - const result = setSlotTargeting({ "div-gpt-ad-1": "value-1" }, null); + describe("setAdUnitTargeting", function () { + it("returns false when ad units are unavailable", function () { + const result = setAdUnitTargeting({ "div-gpt-ad-1": "value-1" }, []); expect(result).to.equal(false); }); - it("sets mile_rtd targeting by slot element id and ad unit path", function () { - const slotWithElementId = createMockSlot({ - elementId: "div-gpt-ad-1", - adUnitPath: "/123/home/top", + it("merges mile_rtd targeting using code, gpid, and pbadslot", function () { + const adUnitWithCode = createMockAdUnit({ + code: "div-gpt-ad-1", + gpid: "/123/home/top", + adserverTargeting: { existing: "targeting" }, }); - const slotWithPathFallback = createMockSlot({ - elementId: "unknown-slot", - adUnitPath: "/123/home/path-only", + const adUnitWithGpid = createMockAdUnit({ + code: "unknown-slot", + gpid: "/123/home/path-only", }); - const slotWithoutMatch = createMockSlot({ - elementId: "unknown-2", - adUnitPath: "/123/home/no-match", + const adUnitWithPbadslot = createMockAdUnit({ + code: "unknown-2", + pbadslot: "/123/home/pbadslot-only", + }); + const adUnitWithoutMatch = createMockAdUnit({ + code: "unknown-3", + gpid: "/123/home/no-match", }); - const googletag = { - cmd: { - push: (fn) => fn(), - }, - pubads: sandbox.stub().returns({ - getSlots: sandbox - .stub() - .returns([ - slotWithElementId, - slotWithPathFallback, - slotWithoutMatch, - ]), - }), - }; - - const result = setSlotTargeting( + const result = setAdUnitTargeting( { "div-gpt-ad-1": "segment_a", "/123/home/path-only": "segment_b", + "/123/home/pbadslot-only": "segment_c", }, - googletag, + [ + adUnitWithCode, + adUnitWithGpid, + adUnitWithPbadslot, + adUnitWithoutMatch, + ], ); expect(result).to.equal(true); - expect( - slotWithElementId.setTargeting.calledOnceWith("mile_rtd", "segment_a"), - ).to.equal(true); - expect( - slotWithPathFallback.setTargeting.calledOnceWith( - "mile_rtd", - "segment_b", - ), - ).to.equal(true); - expect(slotWithoutMatch.setTargeting.called).to.equal(false); + expect(adUnitWithCode.adserverTargeting).to.deep.equal({ + existing: "targeting", + mile_rtd: "segment_a", + }); + expect(adUnitWithGpid.adserverTargeting).to.deep.equal({ + mile_rtd: "segment_b", + }); + expect(adUnitWithPbadslot.adserverTargeting).to.deep.equal({ + mile_rtd: "segment_c", + }); + expect(adUnitWithoutMatch.adserverTargeting).to.equal(undefined); }); }); @@ -204,9 +206,8 @@ describe("mile RTD provider", function () { const runtimeStub = sandbox .stub() .returns({ "div-gpt-ad-1": "segment_a" }); - const slot = createMockSlot({ - elementId: "div-gpt-ad-1", - adUnitPath: "/123/home/top", + const adUnit = createMockAdUnit({ + code: "div-gpt-ad-1", }); window.mileRtdRuntime = { getMileTargetingByAdUnit: runtimeStub }; window.mileRtdRuntimeUtils = { @@ -214,15 +215,9 @@ describe("mile RTD provider", function () { .stub() .returns({ adUnitCodes: ["ad-1"] }), }; - window.googletag = { - cmd: { push: (fn) => fn() }, - pubads: sandbox - .stub() - .returns({ getSlots: sandbox.stub().returns([slot]) }), - }; - onAuctionInitEvent({ adUnitCodes: ["ad-1"], + adUnits: [adUnit], bidderRequests: [ { bids: [{ floorData: { enforcements: { enforceJS: true } } }] }, ], @@ -236,9 +231,9 @@ describe("mile RTD provider", function () { expect(runtimeStub.firstCall.args[1]).to.deep.equal({ mode: "auctionInit", }); - expect( - slot.setTargeting.calledOnceWith("mile_rtd", "segment_a"), - ).to.equal(true); + expect(adUnit.adserverTargeting).to.deep.equal({ + mile_rtd: "segment_a", + }); }); });