Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,12 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
if err != nil {
logger.Errorf("Error setting seat non-bid: %v", err)
}
labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao)

useOpenAdsExtKey := false
if reqExt, err := req.GetRequestExt(); err == nil && reqExt != nil && reqExt.UseOpenAdsExtKey() {
useOpenAdsExtKey = true
}
labels, ao = sendAuctionResponse(w, hookExecutor, response, req.BidRequest, account, labels, ao, useOpenAdsExtKey)
}

// setSeatNonBidRaw is transitional function for setting SeatNonBid inside bidResponse.Ext
Expand Down Expand Up @@ -347,7 +352,7 @@ func rejectAuctionRequest(
ao.Response = response
ao.Errors = append(ao.Errors, rejectErr)

return sendAuctionResponse(w, hookExecutor, response, request, account, labels, ao)
return sendAuctionResponse(w, hookExecutor, response, request, account, labels, ao, false)
}

func sendAuctionResponse(
Expand All @@ -358,6 +363,7 @@ func sendAuctionResponse(
account *config.Account,
labels metrics.Labels,
ao analytics.AuctionObject,
useOpenAdsExtKey bool,
) (metrics.Labels, analytics.AuctionObject) {
hookExecutor.ExecuteAuctionResponseStage(response)

Expand Down Expand Up @@ -388,6 +394,12 @@ func sendAuctionResponse(
// Exitpoint will modify the response and set response headers according to hook implementation.
finalResponse := hookExecutor.ExecuteExitpointStage(response, w)

if useOpenAdsExtKey {
if br, ok := finalResponse.(*openrtb2.BidResponse); ok {
rekeyResponseForOpenAds(br)
}
}

// If an error happens when encoding the response, there isn't much we can do.
// If we've sent _any_ bytes, then Go would have sent the 200 status code first.
// That status code can't be un-sent... so the best we can do is log the error.
Expand All @@ -399,6 +411,45 @@ func sendAuctionResponse(
return labels, ao
}

func rekeyResponseForOpenAds(response *openrtb2.BidResponse) {
if response == nil {
return
}

response.Ext = rekeyPrebidInJSON(response.Ext)

for i := range response.SeatBid {
for j := range response.SeatBid[i].Bid {
response.SeatBid[i].Bid[j].Ext = rekeyPrebidInJSON(response.SeatBid[i].Bid[j].Ext)
}
}
}

func rekeyPrebidInJSON(data json.RawMessage) json.RawMessage {
if len(data) == 0 {
return data
}

var raw map[string]json.RawMessage
if err := jsonutil.Unmarshal(data, &raw); err != nil {
return data
}

v, ok := raw[openrtb_ext.PrebidExtKey]
if !ok {
return data
}

raw[openrtb_ext.OpenAdsExtKey] = v
delete(raw, openrtb_ext.PrebidExtKey)

result, err := jsonutil.Marshal(raw)
if err != nil {
return data
}
return result
}

// setBrowsingTopicsHeader always set the Observe-Browsing-Topics header to a value of ?1 if the Sec-Browsing-Topics is present in request
func setBrowsingTopicsHeader(w http.ResponseWriter, r *http.Request) {
if value := r.Header.Get(secBrowsingTopics); value != "" {
Expand Down
180 changes: 179 additions & 1 deletion endpoints/openrtb2/auction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5102,14 +5102,192 @@ func TestSendAuctionResponse_LogsErrors(t *testing.T) {
ao := analytics.AuctionObject{}
account := &config.Account{DebugAllow: true}

_, ao = sendAuctionResponse(writer, test.hookExecutor, test.response, test.request, account, labels, ao)
_, ao = sendAuctionResponse(writer, test.hookExecutor, test.response, test.request, account, labels, ao, false)

assert.Equal(t, ao.Errors, test.expectedErrors, "Invalid errors.")
assert.Equal(t, test.expectedStatus, ao.Status, "Invalid HTTP response status.")
})
}
}

func TestSendAuctionResponse_OpenAdsExtKey(t *testing.T) {
hookExecutor := &mockStageExecutor{}

bidResponse := &openrtb2.BidResponse{
ID: "test-response",
Ext: json.RawMessage(`{"prebid":{"auctiontimestamp":1715184000},"responsetimemillis":{"ttd":42}}`),
SeatBid: []openrtb2.SeatBid{
{
Seat: "ttd",
Bid: []openrtb2.Bid{
{
ID: "bid-1",
Ext: json.RawMessage(`{"prebid":{"type":"banner","meta":{"adaptercode":"ttd"}},"origbidcpm":1.5}`),
},
},
},
},
}

t.Run("ext.openads request produces openads response keys", func(t *testing.T) {
writer := httptest.NewRecorder()
labels := metrics.Labels{}
ao := analytics.AuctionObject{}
account := &config.Account{}

responseCopy := *bidResponse
responseCopy.SeatBid = make([]openrtb2.SeatBid, len(bidResponse.SeatBid))
copy(responseCopy.SeatBid, bidResponse.SeatBid)
responseCopy.SeatBid[0].Bid = make([]openrtb2.Bid, len(bidResponse.SeatBid[0].Bid))
copy(responseCopy.SeatBid[0].Bid, bidResponse.SeatBid[0].Bid)

sendAuctionResponse(writer, hookExecutor, &responseCopy, &openrtb2.BidRequest{ID: "test"}, account, labels, ao, true)

body := writer.Body.String()

var resp openrtb2.BidResponse
assert.NoError(t, json.Unmarshal([]byte(body), &resp))

var respExt map[string]json.RawMessage
assert.NoError(t, json.Unmarshal(resp.Ext, &respExt))
assert.Contains(t, respExt, "openads")
assert.NotContains(t, respExt, "prebid")

var bidExt map[string]json.RawMessage
assert.NoError(t, json.Unmarshal(resp.SeatBid[0].Bid[0].Ext, &bidExt))
assert.Contains(t, bidExt, "openads")
assert.NotContains(t, bidExt, "prebid")
})

t.Run("ext.prebid request preserves prebid response keys", func(t *testing.T) {
writer := httptest.NewRecorder()
labels := metrics.Labels{}
ao := analytics.AuctionObject{}
account := &config.Account{}

responseCopy := *bidResponse
responseCopy.SeatBid = make([]openrtb2.SeatBid, len(bidResponse.SeatBid))
copy(responseCopy.SeatBid, bidResponse.SeatBid)
responseCopy.SeatBid[0].Bid = make([]openrtb2.Bid, len(bidResponse.SeatBid[0].Bid))
copy(responseCopy.SeatBid[0].Bid, bidResponse.SeatBid[0].Bid)

sendAuctionResponse(writer, hookExecutor, &responseCopy, &openrtb2.BidRequest{ID: "test"}, account, labels, ao, false)

body := writer.Body.String()

var resp openrtb2.BidResponse
assert.NoError(t, json.Unmarshal([]byte(body), &resp))

var respExt map[string]json.RawMessage
assert.NoError(t, json.Unmarshal(resp.Ext, &respExt))
assert.Contains(t, respExt, "prebid")
assert.NotContains(t, respExt, "openads")

var bidExt map[string]json.RawMessage
assert.NoError(t, json.Unmarshal(resp.SeatBid[0].Bid[0].Ext, &bidExt))
assert.Contains(t, bidExt, "prebid")
assert.NotContains(t, bidExt, "openads")
})
}

func TestRekeyPrebidInJSON(t *testing.T) {
tests := []struct {
name string
input json.RawMessage
expected json.RawMessage
}{
{
name: "renames prebid to openads",
input: json.RawMessage(`{"prebid":{"auctiontimestamp":123},"responsetimemillis":{}}`),
expected: json.RawMessage(`{"openads":{"auctiontimestamp":123},"responsetimemillis":{}}`),
},
{
name: "no prebid key is no-op",
input: json.RawMessage(`{"other":{"foo":"bar"}}`),
expected: json.RawMessage(`{"other":{"foo":"bar"}}`),
},
{
name: "empty input returns empty",
input: nil,
expected: nil,
},
{
name: "invalid JSON returns unchanged",
input: json.RawMessage(`not json`),
expected: json.RawMessage(`not json`),
},
{
name: "nested prebid keys are untouched",
input: json.RawMessage(`{"prebid":{"seatnonbid":[{"nonbid":[{"ext":{"prebid":{"bid":{}}}}]}]}}`),
expected: json.RawMessage(`{"openads":{"seatnonbid":[{"nonbid":[{"ext":{"prebid":{"bid":{}}}}]}]}}`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := rekeyPrebidInJSON(tc.input)
if tc.expected == nil {
assert.Nil(t, result)
} else if !json.Valid(tc.expected) {
assert.Equal(t, string(tc.expected), string(result))
} else {
assert.JSONEq(t, string(tc.expected), string(result))
}
})
}
}

func TestRekeyResponseForOpenAds(t *testing.T) {
t.Run("renames response ext and bid ext", func(t *testing.T) {
response := &openrtb2.BidResponse{
Ext: json.RawMessage(`{"prebid":{"auctiontimestamp":123},"responsetimemillis":{}}`),
SeatBid: []openrtb2.SeatBid{
{
Bid: []openrtb2.Bid{
{Ext: json.RawMessage(`{"prebid":{"type":"banner","meta":{"adaptercode":"ttd"}},"origbidcpm":1.5}`)},
{Ext: json.RawMessage(`{"prebid":{"type":"video"},"origbidcpm":2.0}`)},
},
},
},
}

rekeyResponseForOpenAds(response)

assert.Contains(t, string(response.Ext), `"openads"`)
assert.NotContains(t, string(response.Ext), `"prebid"`)

for _, sb := range response.SeatBid {
for _, bid := range sb.Bid {
assert.Contains(t, string(bid.Ext), `"openads"`)
assert.NotContains(t, string(bid.Ext), `"prebid"`)
}
}
})

t.Run("nil response is safe", func(t *testing.T) {
rekeyResponseForOpenAds(nil)
})

t.Run("response with no ext is safe", func(t *testing.T) {
response := &openrtb2.BidResponse{}
rekeyResponseForOpenAds(response)
assert.Nil(t, response.Ext)
})

t.Run("preserves errors.prebid and warnings.prebid as bidder names", func(t *testing.T) {
response := &openrtb2.BidResponse{
Ext: json.RawMessage(`{"prebid":{"auctiontimestamp":123},"errors":{"prebid":["some error"]},"warnings":{"prebid":["some warning"]}}`),
}
rekeyResponseForOpenAds(response)

assert.Contains(t, string(response.Ext), `"openads"`)

var ext map[string]json.RawMessage
jsonutil.Unmarshal(response.Ext, &ext)
assert.Contains(t, string(ext["errors"]), `"prebid"`)
assert.Contains(t, string(ext["warnings"]), `"prebid"`)
})
}

func TestParseRequestMultiBid(t *testing.T) {
tests := []struct {
name string
Expand Down
18 changes: 12 additions & 6 deletions openrtb_ext/request_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,12 +749,13 @@ func (ue *UserExt) Clone() *UserExt {
// ---------------------------------------------------------------

type RequestExt struct {
ext map[string]json.RawMessage
extDirty bool
prebid *ExtRequestPrebid
prebidDirty bool
schain *openrtb2.SupplyChain // ORTB 2.4 location
schainDirty bool
ext map[string]json.RawMessage
extDirty bool
prebid *ExtRequestPrebid
prebidDirty bool
schain *openrtb2.SupplyChain // ORTB 2.4 location
schainDirty bool
useOpenAdsExtKey bool
}

func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
Expand All @@ -777,6 +778,7 @@ func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
// stored under prebidKey so outbound marshaling emits "prebid".
prebidJson, hasPrebid := re.ext[OpenAdsExtKey]
if hasPrebid {
re.useOpenAdsExtKey = true
re.ext[prebidKey] = prebidJson
} else {
prebidJson, hasPrebid = re.ext[prebidKey]
Expand Down Expand Up @@ -846,6 +848,10 @@ func (re *RequestExt) marshal() (json.RawMessage, error) {
return jsonutil.Marshal(re.ext)
}

func (re *RequestExt) UseOpenAdsExtKey() bool {
return re.useOpenAdsExtKey
}

func (re *RequestExt) Dirty() bool {
return re.extDirty || re.prebidDirty || re.schainDirty
}
Expand Down
30 changes: 30 additions & 0 deletions openrtb_ext/request_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2512,6 +2512,36 @@ func TestRequestExtOpenAdsAlias(t *testing.T) {
})
}

func TestRequestExtUseOpenAdsExtKey(t *testing.T) {
t.Run("openads key sets flag true", func(t *testing.T) {
re := &RequestExt{}
err := re.unmarshal([]byte(`{"openads":{"debug":true}}`))
assert.NoError(t, err)
assert.True(t, re.UseOpenAdsExtKey())
})

t.Run("prebid key leaves flag false", func(t *testing.T) {
re := &RequestExt{}
err := re.unmarshal([]byte(`{"prebid":{"debug":true}}`))
assert.NoError(t, err)
assert.False(t, re.UseOpenAdsExtKey())
})

t.Run("no ext key leaves flag false", func(t *testing.T) {
re := &RequestExt{}
err := re.unmarshal([]byte(`{}`))
assert.NoError(t, err)
assert.False(t, re.UseOpenAdsExtKey())
})

t.Run("both keys present, flag true because openads wins", func(t *testing.T) {
re := &RequestExt{}
err := re.unmarshal([]byte(`{"prebid":{"debug":true},"openads":{"debug":false}}`))
assert.NoError(t, err)
assert.True(t, re.UseOpenAdsExtKey())
})
}

func TestImpExtOpenAdsAlias(t *testing.T) {
t.Run("openads decodes into prebid and marshals as prebid", func(t *testing.T) {
ie := &ImpExt{}
Expand Down
Loading