@@ -11,12 +11,91 @@ import { registerBidder } from '../src/adapters/bidderFactory.js';
1111
1212const BIDDER_CODE = 'holid' ;
1313const GVLID = 1177 ;
14+
1415const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction' ;
1516const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html' ;
17+
1618const TIME_TO_LIVE = 300 ;
17- const TMAX = 500 ;
19+
20+ // Keep win URLs in-memory (per page-load)
1821const wurlMap = { } ;
1922
23+ /**
24+ * Resolve tmax for the outgoing ORTB request.
25+ * Goal: respect publisher's Prebid timeout (bidderRequest.timeout) and allow an optional per-bid override,
26+ * without hard-forcing an arbitrary 500ms.
27+ *
28+ * Rules:
29+ * - If bid.params.tmax is a positive number, use it, but never exceed bidderRequest.timeout when available.
30+ * - Else if bidderRequest.timeout is a positive number, use it.
31+ * - Else omit tmax entirely (PBS will apply its own default / config).
32+ */
33+ function resolveTmax ( bid , bidderRequest ) {
34+ const auctionTimeout = Number ( bidderRequest ?. timeout ) ;
35+ const paramTmax = Number ( bid ?. params ?. tmax ) ;
36+
37+ const hasAuctionTimeout = Number . isFinite ( auctionTimeout ) && auctionTimeout > 0 ;
38+ const hasParamTmax = Number . isFinite ( paramTmax ) && paramTmax > 0 ;
39+
40+ if ( hasParamTmax && hasAuctionTimeout ) {
41+ return Math . min ( paramTmax , auctionTimeout ) ;
42+ }
43+ if ( hasParamTmax ) {
44+ return paramTmax ;
45+ }
46+ if ( hasAuctionTimeout ) {
47+ return auctionTimeout ;
48+ }
49+ return undefined ;
50+ }
51+
52+ /**
53+ * Merge stored request ID into request.ext.prebid.storedrequest.id (without clobbering other ext fields).
54+ * Keeps behavior consistent with the existing adapter expectation of bid.params.adUnitID.
55+ */
56+ function mergeStoredRequest ( ortbRequest , bid ) {
57+ const storedId = getBidIdParameter ( 'adUnitID' , bid . params ) ;
58+ if ( storedId ) {
59+ deepSetValue ( ortbRequest , 'ext.prebid.storedrequest.id' , storedId ) ;
60+ }
61+ }
62+
63+ /**
64+ * Merge schain into request.source.ext.schain (without overwriting request.source / request.ext).
65+ */
66+ function mergeSchain ( ortbRequest , bid ) {
67+ const schain = deepAccess ( bid , 'ortb2.source.ext.schain' ) ;
68+ if ( schain ) {
69+ deepSetValue ( ortbRequest , 'source.ext.schain' , schain ) ;
70+ }
71+ }
72+
73+ /**
74+ * Build a sync URL for our sync endpoint.
75+ */
76+ function buildSyncUrl ( { bidders, gdprConsent, uspConsent, type } ) {
77+ const queryParams = [ ] ;
78+
79+ queryParams . push ( 'bidders=' + bidders ) ;
80+
81+ if ( gdprConsent ) {
82+ queryParams . push ( 'gdpr=' + ( gdprConsent . gdprApplies ? 1 : 0 ) ) ;
83+ queryParams . push (
84+ 'gdpr_consent=' + encodeURIComponent ( gdprConsent . consentString || '' )
85+ ) ;
86+ } else {
87+ queryParams . push ( 'gdpr=0' ) ;
88+ }
89+
90+ if ( typeof uspConsent !== 'undefined' ) {
91+ queryParams . push ( 'us_privacy=' + encodeURIComponent ( uspConsent ) ) ;
92+ }
93+
94+ queryParams . push ( 'type=' + encodeURIComponent ( type ) ) ;
95+
96+ return COOKIE_SYNC_ENDPOINT + '?' + queryParams . join ( '&' ) ;
97+ }
98+
2099export const spec = {
21100 code : BIDDER_CODE ,
22101 gvlid : GVLID ,
@@ -30,19 +109,23 @@ export const spec = {
30109 // Build request payload including GDPR, GPP, and US Privacy data if available
31110 buildRequests : function ( validBidRequests , bidderRequest ) {
32111 return validBidRequests . map ( ( bid ) => {
112+ // Start from ortb2 (publisher modules may have populated site/user/device/ext/etc)
33113 const requestData = {
34114 ...bid . ortb2 ,
35- source : {
36- ext : {
37- schain : bid ?. ortb2 ?. source ?. ext ?. schain
38- }
39- } ,
40115 id : bidderRequest . bidderRequestId ,
41116 imp : [ getImp ( bid ) ] ,
42- tmax : TMAX ,
43- ...buildStoredRequest ( bid ) ,
44117 } ;
45118
119+ // Merge (don’t overwrite) schain + storedrequest
120+ mergeSchain ( requestData , bid ) ;
121+ mergeStoredRequest ( requestData , bid ) ;
122+
123+ // Resolve and set tmax (don’t hard-force)
124+ const tmax = resolveTmax ( bid , bidderRequest ) ;
125+ if ( tmax ) {
126+ requestData . tmax = tmax ;
127+ }
128+
46129 // GDPR
47130 if ( bidderRequest && bidderRequest . gdprConsent ) {
48131 deepSetValue (
@@ -67,7 +150,11 @@ export const spec = {
67150
68151 // US Privacy
69152 if ( bidderRequest && bidderRequest . usPrivacy ) {
70- deepSetValue ( requestData , 'regs.ext.us_privacy' , bidderRequest . usPrivacy ) ;
153+ deepSetValue (
154+ requestData ,
155+ 'regs.ext.us_privacy' ,
156+ bidderRequest . usPrivacy
157+ ) ;
71158 }
72159
73160 // User IDs
@@ -87,6 +174,7 @@ export const spec = {
87174 // Interpret response: group bids by unique impid and select the highest CPM bid per imp
88175 interpretResponse : function ( serverResponse , bidRequest ) {
89176 const bidResponsesMap = { } ; // Maps impid -> highest bid object
177+
90178 if ( ! serverResponse . body || ! serverResponse . body . seatbid ) {
91179 return [ ] ;
92180 }
@@ -98,20 +186,30 @@ export const spec = {
98186 // --- MINIMAL CHANGE START ---
99187 // Build meta object and propagate advertiser domains for hb_adomain
100188 const meta = deepAccess ( bid , 'ext.prebid.meta' , { } ) || { } ;
189+
101190 // Read ORTB adomain; normalize to array of clean strings
102191 let advertiserDomains = deepAccess ( bid , 'adomain' , [ ] ) ;
103192 advertiserDomains = Array . isArray ( advertiserDomains )
104193 ? advertiserDomains
105194 . filter ( Boolean )
106- . map ( d => String ( d ) . toLowerCase ( ) . replace ( / ^ h t t p s ? : \/ \/ / , '' ) . replace ( / ^ w w w \. / , '' ) . trim ( ) )
195+ . map ( ( d ) =>
196+ String ( d )
197+ . toLowerCase ( )
198+ . replace ( / ^ h t t p s ? : \/ \/ / , '' )
199+ . replace ( / ^ w w w \. / , '' )
200+ . trim ( )
201+ )
107202 : [ ] ;
203+
108204 if ( advertiserDomains . length > 0 ) {
109205 meta . advertiserDomains = advertiserDomains ; // <-- Prebid uses this to set hb_adomain
110206 }
207+
111208 const networkId = deepAccess ( bid , 'ext.prebid.meta.networkId' ) ;
112209 if ( networkId ) {
113210 meta . networkId = networkId ;
114211 }
212+
115213 // Keep writing back for completeness (preserves existing behavior)
116214 deepSetValue ( bid , 'ext.prebid.meta' , meta ) ;
117215 // --- MINIMAL CHANGE END ---
@@ -157,39 +255,41 @@ export const spec = {
157255 } ,
158256 ] ;
159257
160- if ( ! serverResponse || ( Array . isArray ( serverResponse ) && serverResponse . length === 0 ) ) {
258+ if (
259+ ! serverResponse ||
260+ ( Array . isArray ( serverResponse ) && serverResponse . length === 0 )
261+ ) {
161262 return syncs ;
162263 }
163264
164- const responses = Array . isArray ( serverResponse )
165- ? serverResponse
166- : [ serverResponse ] ;
265+ const responses = Array . isArray ( serverResponse ) ? serverResponse : [ serverResponse ] ;
167266 const bidders = getBidders ( responses ) ;
168267
268+ // Prefer iframe when allowed
169269 if ( optionsType . iframeEnabled && bidders ) {
170- const queryParams = [ ] ;
171- queryParams . push ( 'bidders=' + bidders ) ;
172-
173- if ( gdprConsent ) {
174- queryParams . push ( 'gdpr=' + ( gdprConsent . gdprApplies ? 1 : 0 ) ) ;
175- queryParams . push (
176- 'gdpr_consent=' +
177- encodeURIComponent ( gdprConsent . consentString || '' )
178- ) ;
179- } else {
180- queryParams . push ( 'gdpr=0' ) ;
181- }
182-
183- if ( typeof uspConsent !== 'undefined' ) {
184- queryParams . push ( 'us_privacy=' + encodeURIComponent ( uspConsent ) ) ;
185- }
186-
187- queryParams . push ( 'type=iframe' ) ;
188- const strQueryParams = '?' + queryParams . join ( '&' ) ;
189-
190270 syncs . push ( {
191271 type : 'iframe' ,
192- url : COOKIE_SYNC_ENDPOINT + strQueryParams ,
272+ url : buildSyncUrl ( {
273+ bidders,
274+ gdprConsent,
275+ uspConsent,
276+ type : 'iframe' ,
277+ } ) ,
278+ } ) ;
279+ return syncs ;
280+ }
281+
282+ // Fallback: if iframe is disabled but pixels are enabled, attempt a pixel-based sync call
283+ // (Your sync endpoint must support this mode for it to be effective.)
284+ if ( optionsType . pixelEnabled && bidders ) {
285+ syncs . push ( {
286+ type : 'image' ,
287+ url : buildSyncUrl ( {
288+ bidders,
289+ gdprConsent,
290+ uspConsent,
291+ type : 'image' ,
292+ } ) ,
193293 } ) ;
194294 }
195295
@@ -211,8 +311,8 @@ export const spec = {
211311function getImp ( bid ) {
212312 const imp = buildStoredRequest ( bid ) ;
213313 imp . id = bid . bidId ; // Ensure imp.id is unique to match the bid response correctly
214- const sizes =
215- bid . sizes && ! Array . isArray ( bid . sizes [ 0 ] ) ? [ bid . sizes ] : bid . sizes ;
314+
315+ const sizes = bid . sizes && ! Array . isArray ( bid . sizes [ 0 ] ) ? [ bid . sizes ] : bid . sizes ;
216316
217317 if ( deepAccess ( bid , 'mediaTypes.banner' ) ) {
218318 imp . banner = {
@@ -244,13 +344,26 @@ function buildStoredRequest(bid) {
244344}
245345
246346// Helper: Extract unique bidders from responses for user syncs
347+ // Primary source: ext.responsetimemillis (PBS), fallback: seatbid[].seat
247348function getBidders ( responses ) {
248- const bidders = responses
249- . map ( ( res ) => Object . keys ( res . body . ext ?. responsetimemillis || { } ) )
250- . flat ( ) ;
349+ const bidderSet = new Set ( ) ;
350+
351+ responses . forEach ( ( res ) => {
352+ const rtm = deepAccess ( res , 'body.ext.responsetimemillis' ) ;
353+ if ( rtm && typeof rtm === 'object' ) {
354+ Object . keys ( rtm ) . forEach ( ( k ) => bidderSet . add ( k ) ) ;
355+ }
356+
357+ const seatbid = deepAccess ( res , 'body.seatbid' , [ ] ) ;
358+ if ( Array . isArray ( seatbid ) ) {
359+ seatbid . forEach ( ( sb ) => {
360+ if ( sb && sb . seat ) bidderSet . add ( sb . seat ) ;
361+ } ) ;
362+ }
363+ } ) ;
251364
252- if ( bidders . length ) {
253- return encodeURIComponent ( JSON . stringify ( [ ...new Set ( bidders ) ] ) ) ;
365+ if ( bidderSet . size ) {
366+ return encodeURIComponent ( JSON . stringify ( [ ...bidderSet ] ) ) ;
254367 }
255368}
256369
0 commit comments