From 2848f6b9c150a9884c1d78d993f9c57009e1d649 Mon Sep 17 00:00:00 2001 From: Mohammad Nurul Islam Shihan <93646635+ishihanvcs@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:23:51 +0600 Subject: [PATCH 01/69] ImproveDigital: Bad-Input Error (#3469) --- adapters/improvedigital/improvedigital.go | 14 ++- .../addtl-consent-multi-tilda.json | 94 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json diff --git a/adapters/improvedigital/improvedigital.go b/adapters/improvedigital/improvedigital.go index c00acfa74ad..3fd514be86d 100644 --- a/adapters/improvedigital/improvedigital.go +++ b/adapters/improvedigital/improvedigital.go @@ -287,13 +287,17 @@ func (a *ImprovedigitalAdapter) getAdditionalConsentProvidersUserExt(request ope } // End validating additional consent - // Check if string contain ~, then substring after ~ to end of string - consentStr := string(cpMapValue) - var tildaPosition int - if tildaPosition = strings.Index(consentStr, "~"); tildaPosition == -1 { + // Trim enclosing quotes after casting json.RawMessage to string + consentStr := strings.Trim((string)(cpMapValue), "\"") + // Split by ~ and take only the second string (if exists) as the consented providers spec + var consentStrParts = strings.Split(consentStr, "~") + if len(consentStrParts) < 2 { + return nil, nil + } + cpStr = strings.TrimSpace(consentStrParts[1]) + if len(cpStr) == 0 { return nil, nil } - cpStr = consentStr[tildaPosition+1 : len(consentStr)-1] // Prepare consent providers string cpStr = fmt.Sprintf("[%s]", strings.Replace(cpStr, ".", ",", -1)) diff --git a/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json new file mode 100644 index 00000000000..24c4b813ff3 --- /dev/null +++ b/adapters/improvedigital/improvedigitaltest/supplemental/addtl-consent-multi-tilda.json @@ -0,0 +1,94 @@ +{ + "mockBidRequest": { + "id": "addtl-consent-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "placementId": 13245 + } + } + }], + "user": { + "ext":{"consent":"ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"}} + } + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://localhost/pbs", + "body": { + "id": "addtl-consent-request-id", + "site": { + "page": "https://good.site/url" + }, + "imp": [{ + "id": "test-imp-id", + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "ext": { + "bidder": { + "placementId": 13245 + } + } + }], + "user": { + "ext": {"consent": "ABC","ConsentedProvidersSettings":{"consented_providers":"1~10.20.90~2"},"consented_providers_settings": {"consented_providers": [10,20,90]} + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "addtl-consent-request-id", + "seatbid": [{ + "seat": "improvedigital", + "bid": [{ + "id": "randomid", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "12345678", + "adm": "some-test-ad", + "cid": "987", + "crid": "12345678", + "h": 250, + "w": 300 + }] + }], + "cur": "USD" + } + } + }], + + "expectedBidResponses": [{ + "currency": "USD", + "bids": [{ + "bid": { + "id": "randomid", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12345678", + "cid": "987", + "crid": "12345678", + "w": 300, + "h": 250 + }, + "type": "banner" + }] + }] +} From 61a0fac155b936cefcfbc24b97b69461bfbb8dae Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Tue, 13 Feb 2024 20:55:56 +0530 Subject: [PATCH 02/69] Update validate pull request workflows to use node 20 actions (#3474) --- .github/workflows/validate-merge.yml | 4 ++-- .github/workflows/validate.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validate-merge.yml b/.github/workflows/validate-merge.yml index 07f1bacaa45..c44b4002f4e 100644 --- a/.github/workflows/validate-merge.yml +++ b/.github/workflows/validate-merge.yml @@ -10,12 +10,12 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: 1.20.5 - name: Checkout Merged Branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate run: | diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9047e1f468f..a3914c3c868 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -16,12 +16,12 @@ jobs: steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Resolves to empty string for push events and falls back to HEAD. ref: ${{ github.event.pull_request.head.sha }} From 2da77b0ecb697c42b63be937826ce0647623e332 Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Tue, 13 Feb 2024 21:03:11 +0530 Subject: [PATCH 03/69] Update issue tracking workflow to use actions with node version 20 support (#3479) --- .github/workflows/issue_prioritization.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_prioritization.yml b/.github/workflows/issue_prioritization.yml index 3843507b26e..ec58073d653 100644 --- a/.github/workflows/issue_prioritization.yml +++ b/.github/workflows/issue_prioritization.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + uses: tibdex/github-app-token@v2.1.0 with: app_id: ${{ secrets.PBS_PROJECT_APP_ID }} private_key: ${{ secrets.PBS_PROJECT_APP_PEM }} From 27c99f963ff2696715ca96b3bcafb4e7847f928e Mon Sep 17 00:00:00 2001 From: rajatgoyal2510 Date: Tue, 13 Feb 2024 21:08:52 +0530 Subject: [PATCH 04/69] Medianet: enable gzip and update usersync url (#3489) Co-authored-by: Aman Jain --- static/bidder-info/medianet.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/bidder-info/medianet.yaml b/static/bidder-info/medianet.yaml index ea47de2b11d..ad741e8fb4a 100644 --- a/static/bidder-info/medianet.yaml +++ b/static/bidder-info/medianet.yaml @@ -3,6 +3,7 @@ extra_info: "https://medianet.golang.pbs.com" maintainer: email: "prebid-support@media.net" gvlVendorID: 142 +endpointCompression: gzip modifyingVastXmlAllowed: true capabilities: app: @@ -17,5 +18,5 @@ capabilities: - native userSync: redirect: - url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}} userMacro: "" \ No newline at end of file From 8826556a98e8bcb762b9c1eeb8b30542b697e712 Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Wed, 14 Feb 2024 10:19:09 +0530 Subject: [PATCH 05/69] Update code semgrep workflow to use actions with node 20 support (#3471) --- .github/workflows/semgrep.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 565bb9b871a..b641d5667ed 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{github.event.pull_request.head.ref}} @@ -17,7 +17,7 @@ jobs: - name: Calculate diff id: calculate_diff - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: result-encoding: string script: | @@ -52,7 +52,7 @@ jobs: - name: Add pull request comment id: add_pull_request_comment if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true') - uses: actions/github-script@v6.4.1 + uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} result-encoding: string From 97021b43588b70645c593d1db821569d1259730d Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Wed, 14 Feb 2024 11:28:03 +0530 Subject: [PATCH 06/69] Update release workflow to use actions with node 20 support (#3478) --- .../workflows/helpers/pull-request-utils.js | 26 ++++++++++++++++++- .github/workflows/release.yml | 24 ++++++++++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js index 941ae5b6953..03f8fcc72aa 100644 --- a/.github/workflows/helpers/pull-request-utils.js +++ b/.github/workflows/helpers/pull-request-utils.js @@ -1,7 +1,9 @@ const synchronizeEvent = "synchronize", openedEvent = "opened", completedStatus = "completed", - resultSize = 100 + resultSize = 100, + adminPermission = "admin", + writePermission = "write" class diffHelper { constructor(input) { @@ -407,8 +409,30 @@ class coverageHelper { } } +class userHelper { + constructor(input) { + this.owner = input.context.repo.owner + this.repo = input.context.repo.repo + this.github = input.github + } + + /* + Checks if the user has write permissions for the repository + @returns {boolean} - returns true if the user has write permissions, otherwise false + */ + async hasWritePermissions() { + const { data } = await this.github.rest.repos.getCollaboratorPermissionLevel({ + owner: this.owner, + repo: this.repo, + username: this.owner, + }) + return data.permission === adminPermission || data.permission === writePermission + } +} + module.exports = { diffHelper: (input) => new diffHelper(input), semgrepHelper: (input) => new semgrepHelper(input), coverageHelper: (input) => new coverageHelper(input), + userHelper: (input) => new userHelper(input), } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1d2b10c41c..783946a5970 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,13 +25,25 @@ jobs: permissions: contents: read steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + repository: ${{ github.repository }} + ref: master - name: Check user permission - uses: actions-cool/check-user-permission@v2.2.0 + uses: actions/github-script@v7 id: check with: - require: 'write' + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const utils = require('./.github/workflows/helpers/pull-request-utils.js') + const helper = utils.userHelper({github, context}) + const hasPermission = await helper.hasWritePermissions() + return hasPermission outputs: - hasWritePermission: ${{ steps.check.outputs.require-result }} + hasWritePermission: ${{ steps.check.outputs.result }} build-master: name: Build master @@ -40,7 +52,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@master + uses: actions/checkout@v4 with: fetch-depth: 0 repository: ${{ github.repository }} @@ -58,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Prebid Server - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Create & publish tag @@ -111,7 +123,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Prebid Server - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build image From b3c75875ef3691be5588308048263e35d993a11a Mon Sep 17 00:00:00 2001 From: Jeremy Sadwith Date: Wed, 14 Feb 2024 05:51:16 -0500 Subject: [PATCH 07/69] Kargo Bidder-Info: Adding GPP macros (#3490) --- static/bidder-info/kargo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/kargo.yaml b/static/bidder-info/kargo.yaml index e1022032868..1a7a77eb8bb 100644 --- a/static/bidder-info/kargo.yaml +++ b/static/bidder-info/kargo.yaml @@ -11,7 +11,7 @@ capabilities: - native userSync: redirect: - url: "https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r={{.RedirectURL}}" + url: "https://crb.kargo.com/api/v1/dsync/PrebidServer?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}" userMacro: "$UID" endpointCompression: "GZIP" openrtb: From 9110a6a2286c3c5d16588c0017798094315b7998 Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Wed, 14 Feb 2024 18:56:44 +0530 Subject: [PATCH 08/69] Update trivy check workflow to use node 20 actions (#3472) --- .github/workflows/security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index baa10b93963..09a0fd56791 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: # Resolves to empty string for push events and falls back to HEAD. ref: ${{ github.event.pull_request.head.sha }} @@ -29,6 +29,6 @@ jobs: severity: 'CRITICAL,HIGH' - name: Upload Results To GitHub Security Tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' \ No newline at end of file From 2b10083e80e5945df31a37ec06d58d8acf9a6f28 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Wed, 14 Feb 2024 19:56:13 +0100 Subject: [PATCH 09/69] Fix function godoc comment (#3502) --- adapters/bidder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/bidder.go b/adapters/bidder.go index 2bbcb65701c..5f4867713e6 100644 --- a/adapters/bidder.go +++ b/adapters/bidder.go @@ -45,7 +45,7 @@ type Bidder interface { type TimeoutBidder interface { Bidder - // MakeTimeoutNotice functions much the same as MakeRequests, except it is fed the bidder request that timed out, + // MakeTimeoutNotification functions much the same as MakeRequests, except it is fed the bidder request that timed out, // and expects that only one notification "request" will be generated. A use case for multiple timeout notifications // has not been anticipated. // From e37154baae4a2c3ffe53ad08a7644c908d4ce3ad Mon Sep 17 00:00:00 2001 From: Lion Pierau Date: Wed, 14 Feb 2024 21:53:39 +0100 Subject: [PATCH 10/69] Yieldlab: Add Digital Service Act (DSA) handling (#3473) --- adapters/yieldlab/types.go | 56 +++++- adapters/yieldlab/yieldlab.go | 107 ++++++++++- adapters/yieldlab/yieldlab_test.go | 92 ++++++++++ .../yieldlab/yieldlabtest/exemplary/dsa.json | 169 ++++++++++++++++++ .../yieldlabtest/supplemental/dsa_empty.json | 127 +++++++++++++ .../supplemental/dsa_empty_transparency.json | 139 ++++++++++++++ .../supplemental/invalid_reg_ext.json | 67 +++++++ 7 files changed, 748 insertions(+), 9 deletions(-) create mode 100644 adapters/yieldlab/yieldlabtest/exemplary/dsa.json create mode 100644 adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json create mode 100644 adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json create mode 100644 adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json diff --git a/adapters/yieldlab/types.go b/adapters/yieldlab/types.go index 90612700713..9d3bc6a2fa6 100644 --- a/adapters/yieldlab/types.go +++ b/adapters/yieldlab/types.go @@ -1,18 +1,60 @@ package yieldlab import ( + "github.com/prebid/prebid-server/v2/openrtb_ext" "strconv" "time" ) type bidResponse struct { - ID uint64 `json:"id"` - Price uint `json:"price"` - Advertiser string `json:"advertiser"` - Adsize string `json:"adsize"` - Pid uint64 `json:"pid"` - Did uint64 `json:"did"` - Pvid string `json:"pvid"` + ID uint64 `json:"id"` + Price uint `json:"price"` + Advertiser string `json:"advertiser"` + Adsize string `json:"adsize"` + Pid uint64 `json:"pid"` + Did uint64 `json:"did"` + Pvid string `json:"pvid"` + DSA *dsaResponse `json:"dsa,omitempty"` +} + +// dsaResponse defines Digital Service Act (DSA) parameters from Yieldlab yieldprobe response. +type dsaResponse struct { + Behalf string `json:"behalf,omitempty"` + Paid string `json:"paid,omitempty"` + Adrender *int `json:"adrender,omitempty"` + Transparency []dsaTransparency `json:"transparency,omitempty"` +} + +// openRTBExtRegsWithDSA defines the contract for bidrequest.regs.ext with the missing DSA property. +// +// The openrtb_ext.ExtRegs needs to be extended on yieldlab adapter level until DSA has been implemented +// by the prebid server team (https://github.com/prebid/prebid-server/issues/3424). +type openRTBExtRegsWithDSA struct { + openrtb_ext.ExtRegs + DSA *dsaRequest `json:"dsa,omitempty"` +} + +// responseExtWithDSA defines seatbid.bid.ext with the DSA object. +type responseExtWithDSA struct { + DSA dsaResponse `json:"dsa"` +} + +// dsaRequest defines Digital Service Act (DSA) parameter +// as specified by the OpenRTB 2.X DSA Transparency community extension. +// +// Should rather come from openrtb_ext package but will be defined here until DSA has been +// implemented by the prebid server team (https://github.com/prebid/prebid-server/issues/3424). +type dsaRequest struct { + Required *int `json:"dsarequired"` + PubRender *int `json:"pubrender"` + DataToPub *int `json:"datatopub"` + Transparency []dsaTransparency `json:"transparency"` +} + +// dsaTransparency Digital Service Act (DSA) transparency object +type dsaTransparency struct { + Domain string `json:"domain,omitempty"` + Params []int `json:"dsaparams,omitempty"` } type cacheBuster func() string diff --git a/adapters/yieldlab/yieldlab.go b/adapters/yieldlab/yieldlab.go index 744a45ae3ec..ed7c283ead9 100644 --- a/adapters/yieldlab/yieldlab.go +++ b/adapters/yieldlab/yieldlab.go @@ -96,11 +96,94 @@ func (a *YieldlabAdapter) makeEndpointURL(req *openrtb2.BidRequest, params *open } } + dsa, err := getDSA(req) + if err != nil { + return "", err + } + if dsa != nil { + if dsa.Required != nil { + q.Set("dsarequired", strconv.Itoa(*dsa.Required)) + } + if dsa.PubRender != nil { + q.Set("dsapubrender", strconv.Itoa(*dsa.PubRender)) + } + if dsa.DataToPub != nil { + q.Set("dsadatatopub", strconv.Itoa(*dsa.DataToPub)) + } + if len(dsa.Transparency) != 0 { + transparencyParam := makeDSATransparencyURLParam(dsa.Transparency) + if len(transparencyParam) != 0 { + q.Set("dsatransparency", transparencyParam) + } + } + } + uri.RawQuery = q.Encode() return uri.String(), nil } +// getDSA extracts the Digital Service Act (DSA) properties from the request. +func getDSA(req *openrtb2.BidRequest) (*dsaRequest, error) { + if req.Regs == nil || req.Regs.Ext == nil { + return nil, nil + } + + var extRegs openRTBExtRegsWithDSA + err := json.Unmarshal(req.Regs.Ext, &extRegs) + if err != nil { + return nil, fmt.Errorf("failed to parse Regs.Ext object from Yieldlab response: %v", err) + } + + return extRegs.DSA, nil +} + +// makeDSATransparencyURLParam creates the transparency url parameter +// as specified by the OpenRTB 2.X DSA Transparency community extension. +// +// Example result: platform1domain.com~1~~SSP2domain.com~1_2 +func makeDSATransparencyURLParam(transparencyObjects []dsaTransparency) string { + valueSeparator, itemSeparator, objectSeparator := "_", "~", "~~" + + var b strings.Builder + + concatParams := func(params []int) { + b.WriteString(strconv.Itoa(params[0])) + for _, param := range params[1:] { + b.WriteString(valueSeparator) + b.WriteString(strconv.Itoa(param)) + } + } + + concatTransparency := func(object dsaTransparency) { + if len(object.Domain) == 0 { + return + } + + b.WriteString(object.Domain) + if len(object.Params) != 0 { + b.WriteString(itemSeparator) + concatParams(object.Params) + } + } + + concatTransparencies := func(objects []dsaTransparency) { + if len(objects) == 0 { + return + } + + concatTransparency(objects[0]) + for _, obj := range objects[1:] { + b.WriteString(objectSeparator) + concatTransparency(obj) + } + } + + concatTransparencies(transparencyObjects) + + return b.String() +} + func (a *YieldlabAdapter) makeFormats(req *openrtb2.BidRequest) (bool, string) { var formats []string const sizesSeparator, adslotSizesSeparator = "|", "," @@ -253,6 +336,7 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa } } + var bidErrors []error for _, bid := range bids { width, height, err := splitSize(bid.Adsize) if err != nil { @@ -269,7 +353,13 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa if imp, exists := adslotToImpMap[strconv.FormatUint(bid.ID, 10)]; !exists { continue } else { - var bidType openrtb_ext.BidType + extJson, err := makeResponseExt(bid) + if err != nil { + bidErrors = append(bidErrors, err) + // skip as bids with missing ext.dsa will be discarded anyway + continue + } + responseBid := &openrtb2.Bid{ ID: strconv.FormatUint(bid.ID, 10), Price: float64(bid.Price) / 100, @@ -278,8 +368,10 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa DealID: strconv.FormatUint(bid.Pid, 10), W: int64(width), H: int64(height), + Ext: extJson, } + var bidType openrtb_ext.BidType if imp.Video != nil { bidType = openrtb_ext.BidTypeVideo responseBid.NURL = a.makeAdSourceURL(internalRequest, req, bid) @@ -299,7 +391,18 @@ func (a *YieldlabAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externa } } - return bidderResponse, nil + return bidderResponse, bidErrors +} + +func makeResponseExt(bid *bidResponse) (json.RawMessage, error) { + if bid.DSA != nil { + extJson, err := json.Marshal(responseExtWithDSA{*bid.DSA}) + if err != nil { + return nil, fmt.Errorf("failed to make JSON for seatbid.bid.ext for adslotID %v. This is most likely a programming issue", bid.ID) + } + return extJson, nil + } + return nil, nil } func (a *YieldlabAdapter) findBidReq(adslotID uint64, params []*openrtb_ext.ExtImpYieldlab) *openrtb_ext.ExtImpYieldlab { diff --git a/adapters/yieldlab/yieldlab_test.go b/adapters/yieldlab/yieldlab_test.go index 7704c8862fb..49dcc31e598 100644 --- a/adapters/yieldlab/yieldlab_test.go +++ b/adapters/yieldlab/yieldlab_test.go @@ -257,6 +257,98 @@ func Test_makeSupplyChain(t *testing.T) { } } +func Test_makeDSATransparencyUrlParam(t *testing.T) { + tests := []struct { + name string + transparencies []dsaTransparency + expected string + }{ + { + name: "No transparency objects", + transparencies: []dsaTransparency{}, + expected: "", + }, + { + name: "Nil transparency", + transparencies: nil, + expected: "", + }, + { + name: "Params without a domain", + transparencies: []dsaTransparency{ + { + Params: []int{1, 2}, + }, + }, + expected: "", + }, + { + name: "Params without a params", + transparencies: []dsaTransparency{ + { + Domain: "domain.com", + }, + }, + expected: "domain.com", + }, + { + name: "One object; No Params", + transparencies: []dsaTransparency{ + { + Domain: "domain.com", + Params: []int{}, + }, + }, + expected: "domain.com", + }, + { + name: "One object; One Param", + transparencies: []dsaTransparency{ + { + Domain: "domain.com", + Params: []int{1}, + }, + }, + expected: "domain.com~1", + }, + { + name: "Three domain objects", + transparencies: []dsaTransparency{ + { + Domain: "domain1.com", + Params: []int{1, 2}, + }, + { + Domain: "domain2.com", + Params: []int{3, 4}, + }, + { + Domain: "domain3.com", + Params: []int{5, 6}, + }, + }, + expected: "domain1.com~1_2~~domain2.com~3_4~~domain3.com~5_6", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actual := makeDSATransparencyURLParam(test.transparencies) + assert.Equal(t, test.expected, actual) + }) + } +} + +func Test_getDSA_invalidRequestExt(t *testing.T) { + req := &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"DSA":"wrongValueType"}`)}, + } + + dsa, err := getDSA(req) + + assert.NotNil(t, err) + assert.Nil(t, dsa) +} + func TestYieldlabAdapter_makeEndpointURL_invalidEndpoint(t *testing.T) { bidder, buildErr := Builder(openrtb_ext.BidderYieldlab, config.Adapter{ Endpoint: "test$:/somethingĀ§"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) diff --git a/adapters/yieldlab/yieldlabtest/exemplary/dsa.json b/adapters/yieldlab/yieldlabtest/exemplary/dsa.json new file mode 100644 index 00000000000..0c815835797 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/exemplary/dsa.json @@ -0,0 +1,169 @@ +{ + "mockBidRequest": { + "id": "test-request-with-DSA", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "regs": { + "ext": { + "gdpr": 1, + "dsa": { + "dsarequired": 3, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + }, + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c", + "ext": { + "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&dsadatatopub=2&dsapubrender=0&dsarequired=3&dsatransparency=platform1domain.com~1~~SSP2domain.com~1_2&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", + "dsa": { + "required": 3, + "behalf": "on behalf of yieldlab", + "paid": "by yieldlab", + "transparency": [ + { + "domain": "yieldlab.de", + "dsaparams": [ + 1, + 2 + ] + } + ], + "adrender": 0 + } + } + ] + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "", + "crid": "12345123433", + "dealid": "1234", + "ext": { + "dsa": { + "adrender": 0, + "transparency": [ + { + "domain": "yieldlab.de", + "dsaparams": [ + 1, + 2 + ] + } + ], + "behalf": "on behalf of yieldlab", + "paid": "by yieldlab" + } + }, + "id": "12345", + "impid": "test-imp-id", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json new file mode 100644 index 00000000000..594e5bb4d0b --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty.json @@ -0,0 +1,127 @@ +{ + "mockBidRequest": { + "id": "test-request-with-empty-DSA", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "regs": { + "ext": { + "gdpr": 1, + "dsa": { + } + } + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + }, + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c", + "ext": { + "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", + "dsa": { + } + } + ] + + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "", + "crid": "12345123433", + "dealid": "1234", + "ext": { + "dsa": { + } + }, + "id": "12345", + "impid": "test-imp-id", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json new file mode 100644 index 00000000000..c966a7166a5 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/supplemental/dsa_empty_transparency.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "test-request-with-empty-DSA-transparency", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "regs": { + "ext": { + "gdpr": 1, + "dsa": { + "transparency": [ + { + } + ] + } + } + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + }, + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c", + "ext": { + "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Cookie": [ + "id=34a53e82-0dc3-4815-8b7e-b725ede0361c" + ], + "Referer": [ + "http://localhost:9090/gdpr.html" + ], + "User-Agent": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" + ], + "X-Forwarded-For": [ + "169.254.13.37" + ] + }, + "uri": "https://ad.yieldlab.net/testing/12345?consent=BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02&content=json&gdpr=1&ids=ylid%3A34a53e82-0dc3-4815-8b7e-b725ede0361c&lat=51.499488&lon=-0.128953&pvid=true&sizes=12345%3A728x90&t=key1%3Dvalue1%26key2%3Dvalue2&ts=testing&yl_rtb_connectiontype=6&yl_rtb_devicetype=4&yl_rtb_ifa=hello-ads" + }, + "mockResponse": { + "status": 200, + "body": [ + { + "id": 12345, + "price": 201, + "advertiser": "yieldlab", + "adsize": "728x90", + "pid": 1234, + "did": 5678, + "pvid": "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", + "dsa": { + "transparency": [ + { + } + ] + } + } + ] + + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "adm": "", + "crid": "12345123433", + "dealid": "1234", + "ext": { + "dsa": { + "transparency": [ + { + } + ] + } + }, + "id": "12345", + "impid": "test-imp-id", + "price": 2.01, + "w": 728, + "h": 90 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json b/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json new file mode 100644 index 00000000000..1723b8fbe80 --- /dev/null +++ b/adapters/yieldlab/yieldlabtest/supplemental/invalid_reg_ext.json @@ -0,0 +1,67 @@ +{ + "mockBidRequest": { + "id": "test-request-with-wrong-DSA-type", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 728, + "h": 90 + } + ] + }, + "ext": { + "bidder": { + "adslotId": "12345", + "supplyId": "123456789", + "targeting": { + "key1": "value1", + "key2": "value2" + }, + "extId": "abc" + } + } + } + ], + "device": { + "ifa": "hello-ads", + "devicetype": 4, + "connectiontype": 6, + "geo": { + "lat": 51.499488, + "lon": -0.128953 + }, + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", + "ip": "169.254.13.37", + "h": 1098, + "w": 814 + }, + "regs": { + "ext": { + "gdpr": 1, + "DSA": "" + } + }, + "site": { + "id": "fake-site-id", + "publisher": { + "id": "1" + }, + "page": "http://localhost:9090/gdpr.html" + }, + "user": { + "buyeruid": "34a53e82-0dc3-4815-8b7e-b725ede0361c", + "ext": { + "consent": "BOlOrv1OlOr2EAAABADECg-AAAApp7v______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-3zd4u_1vf99yfm1-7etr3tp_87ues2_Xur__79__3z3_9phP78k89r7337Ew-v02" + } + } + }, + "expectedMakeRequestsErrors": [ + { + "value": "failed to parse Regs.Ext object from Yieldlab response: json: cannot unmarshal string into Go struct field openRTBExtRegsWithDSA.dsa of type yieldlab.dsaRequest", + "comparison": "literal" + } + ] +} From 902d26234aab2d450627dbbc43e01edaf030b46e Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Thu, 15 Feb 2024 12:02:54 +0530 Subject: [PATCH 11/69] Add log lines in release workflow (#3504) --- .github/workflows/helpers/pull-request-utils.js | 3 ++- .github/workflows/release.yml | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js index 03f8fcc72aa..8b470618170 100644 --- a/.github/workflows/helpers/pull-request-utils.js +++ b/.github/workflows/helpers/pull-request-utils.js @@ -426,7 +426,8 @@ class userHelper { repo: this.repo, username: this.owner, }) - return data.permission === adminPermission || data.permission === writePermission + console.log(JSON.stringify(data)) + return data.permission === writePermission || data.permission === adminPermission } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 783946a5970..92d6a589e6a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,13 @@ jobs: return hasPermission outputs: hasWritePermission: ${{ steps.check.outputs.result }} + + debug-step: + name: Debug step + runs-on: ubuntu-latest + steps: + - name: Debug + run: echo ${{ needs.check-permission.outputs.hasWritePermission }} build-master: name: Build master From c35f67b20818711bd6f6e3b67981a011370fbcb4 Mon Sep 17 00:00:00 2001 From: Onkar Hanumante Date: Thu, 15 Feb 2024 21:22:31 +0530 Subject: [PATCH 12/69] Fix: Release workflow permissions (#3506) --- .github/workflows/helpers/pull-request-utils.js | 4 ++-- .github/workflows/release.yml | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js index 8b470618170..73c80396473 100644 --- a/.github/workflows/helpers/pull-request-utils.js +++ b/.github/workflows/helpers/pull-request-utils.js @@ -414,6 +414,7 @@ class userHelper { this.owner = input.context.repo.owner this.repo = input.context.repo.repo this.github = input.github + this.user = input.user } /* @@ -424,9 +425,8 @@ class userHelper { const { data } = await this.github.rest.repos.getCollaboratorPermissionLevel({ owner: this.owner, repo: this.repo, - username: this.owner, + username: this.user, }) - console.log(JSON.stringify(data)) return data.permission === writePermission || data.permission === adminPermission } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92d6a589e6a..d3559732077 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,18 +39,11 @@ jobs: result-encoding: string script: | const utils = require('./.github/workflows/helpers/pull-request-utils.js') - const helper = utils.userHelper({github, context}) + const helper = utils.userHelper({github, context, user: '${{ github.actor }}'}) const hasPermission = await helper.hasWritePermissions() return hasPermission outputs: hasWritePermission: ${{ steps.check.outputs.result }} - - debug-step: - name: Debug step - runs-on: ubuntu-latest - steps: - - name: Debug - run: echo ${{ needs.check-permission.outputs.hasWritePermission }} build-master: name: Build master @@ -71,7 +64,6 @@ jobs: publish-tag: name: Publish tag needs: build-master - if: contains(needs.check-permission.outputs.hasWritePermission, 'true') permissions: contents: write runs-on: ubuntu-latest From 34cfa9487d7316ae1168df89f1246997d7174cca Mon Sep 17 00:00:00 2001 From: AlexBVolcy <74930484+AlexBVolcy@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:16:41 -0800 Subject: [PATCH 13/69] Remove extra buffer from GZIP compression (#3494) --- exchange/bidder.go | 17 ++++++----------- exchange/bidder_test.go | 4 ++-- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/exchange/bidder.go b/exchange/bidder.go index 96f8ac9b0e0..8b54f9847bb 100644 --- a/exchange/bidder.go +++ b/exchange/bidder.go @@ -523,8 +523,6 @@ func (bidder *bidderAdapter) doRequest(ctx context.Context, req *adapters.Reques } func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.RequestData, logger util.LogMsg, bidderRequestStartTime time.Time, tmaxAdjustments *TmaxAdjustmentsPreprocessed) *httpCallInfo { - var requestBody []byte - requestBody, err := getRequestBody(req, bidder.config.EndpointCompression) if err != nil { return &httpCallInfo{ @@ -532,7 +530,7 @@ func (bidder *bidderAdapter) doRequestImpl(ctx context.Context, req *adapters.Re err: err, } } - httpReq, err := http.NewRequest(req.Method, req.Uri, bytes.NewBuffer(requestBody)) + httpReq, err := http.NewRequest(req.Method, req.Uri, requestBody) if err != nil { return &httpCallInfo{ request: req, @@ -746,18 +744,16 @@ func hasShorterDurationThanTmax(ctx bidderTmaxContext, tmaxAdjustments TmaxAdjus return false } -func getRequestBody(req *adapters.RequestData, endpointCompression string) ([]byte, error) { - var requestBody []byte - +func getRequestBody(req *adapters.RequestData, endpointCompression string) (*bytes.Buffer, error) { switch strings.ToUpper(endpointCompression) { case Gzip: // Compress to GZIP - var b bytes.Buffer + b := bytes.NewBuffer(make([]byte, 0, len(req.Body))) w := gzipWriterPool.Get().(*gzip.Writer) defer gzipWriterPool.Put(w) - w.Reset(&b) + w.Reset(b) _, err := w.Write(req.Body) if err != nil { return nil, err @@ -766,14 +762,13 @@ func getRequestBody(req *adapters.RequestData, endpointCompression string) ([]by if err != nil { return nil, err } - requestBody = b.Bytes() // Set Header req.Headers.Set("Content-Encoding", "gzip") - return requestBody, nil + return b, nil default: - return req.Body, nil + return bytes.NewBuffer(req.Body), nil } } diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go index a1662ab7128..9f092b7dea3 100644 --- a/exchange/bidder_test.go +++ b/exchange/bidder_test.go @@ -3377,11 +3377,11 @@ func TestGetRequestBody(t *testing.T) { if test.endpointCompression == "GZIP" { assert.Equal(t, "gzip", req.Headers.Get("Content-Encoding")) - decompressedReqBody, err := decompressGzip(requestBody) + decompressedReqBody, err := decompressGzip(requestBody.Bytes()) assert.NoError(t, err) assert.Equal(t, test.givenReqBody, decompressedReqBody) } else { - assert.Equal(t, test.givenReqBody, requestBody) + assert.Equal(t, test.givenReqBody, requestBody.Bytes()) } }) } From cff2fe11da00d0367eca8f87fc6abb3cd31aea23 Mon Sep 17 00:00:00 2001 From: AlexBVolcy <74930484+AlexBVolcy@users.noreply.github.com> Date: Fri, 16 Feb 2024 06:25:17 -0800 Subject: [PATCH 14/69] Targeting: Add alwaysincludedeals flag (#3454) --- exchange/auction.go | 20 +- exchange/auction_test.go | 66 ++-- exchange/exchange.go | 2 +- exchange/exchange_test.go | 14 +- .../targeting-always-include-deals.json | 296 ++++++++++++++++++ exchange/targeting.go | 33 +- exchange/targeting_test.go | 83 ++++- exchange/utils.go | 1 + openrtb_ext/request.go | 1 + 9 files changed, 435 insertions(+), 81 deletions(-) create mode 100644 exchange/exchangetest/targeting-always-include-deals.json diff --git a/exchange/auction.go b/exchange/auction.go index c02e9141c7a..94f0121d05a 100644 --- a/exchange/auction.go +++ b/exchange/auction.go @@ -108,7 +108,7 @@ func (d *DebugLog) PutDebugLogError(cache prebid_cache_client.Client, timeout in func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, numImps int, preferDeals bool) *auction { winningBids := make(map[string]*entities.PbsOrtbBid, numImps) - winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps) + allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid, numImps) for bidderName, seatBid := range seatBids { if seatBid != nil { @@ -118,10 +118,10 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, nu winningBids[bid.Bid.ImpID] = bid } - if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok { + if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok { bidMap[bidderName] = append(bidMap[bidderName], bid) } else { - winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ bidderName: {bid}, } } @@ -130,8 +130,8 @@ func newAuction(seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, nu } return &auction{ - winningBids: winningBids, - winningBidsByBidder: winningBidsByBidder, + winningBids: winningBids, + allBidsByBidder: allBidsByBidder, } } @@ -151,7 +151,7 @@ func isNewWinningBid(bid, wbid *openrtb2.Bid, preferDeals bool) bool { func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, preferDeals bool, accountDefaultBidLimit int) { bidsSnipped := false // sort bids for multibid targeting - for _, topBidsPerBidder := range a.winningBidsByBidder { + for _, topBidsPerBidder := range a.allBidsByBidder { for bidder, topBids := range topBidsPerBidder { sort.Slice(topBids, func(i, j int) bool { return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, preferDeals) @@ -187,7 +187,7 @@ func (a *auction) validateAndUpdateMultiBid(adapterBids map[openrtb_ext.BidderNa func (a *auction) setRoundedPrices(targetingData targetData) { roundedPrices := make(map[*entities.PbsOrtbBid]string, 5*len(a.winningBids)) - for _, topBidsPerImp := range a.winningBidsByBidder { + for _, topBidsPerImp := range a.allBidsByBidder { for _, topBidsPerBidder := range topBidsPerImp { for _, topBid := range topBidsPerBidder { roundedPrices[topBid] = GetPriceBucket(*topBid.Bid, targetingData) @@ -225,7 +225,7 @@ func (a *auction) doCache(ctx context.Context, cache prebid_cache_client.Client, for _, imp := range bidRequest.Imp { expByImp[imp.ID] = imp.Exp } - for impID, topBidsPerImp := range a.winningBidsByBidder { + for impID, topBidsPerImp := range a.allBidsByBidder { for bidderName, topBidsPerBidder := range topBidsPerImp { for _, topBid := range topBidsPerBidder { isOverallWinner := a.winningBids[impID] == topBid @@ -394,8 +394,8 @@ func defTTL(bidType openrtb_ext.BidType, defaultTTLs *config.DefaultTTLs) (ttl i type auction struct { // winningBids is a map from imp.id to the highest overall CPM bid in that imp. winningBids map[string]*entities.PbsOrtbBid - // winningBidsByBidder stores the highest bid on each imp by each bidder. - winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid + // allBidsByBidder is map from ImpID to another map that maps bidderName to all bids from that bidder. + allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid // roundedPrices stores the price strings rounded for each bid according to the price granularity. roundedPrices map[*entities.PbsOrtbBid]string // cacheIds stores the UUIDs from Prebid Cache for fetching the full bid JSON. diff --git a/exchange/auction_test.go b/exchange/auction_test.go index 40b1aba9e98..17224dcd2fd 100644 --- a/exchange/auction_test.go +++ b/exchange/auction_test.go @@ -191,7 +191,7 @@ func loadCacheSpec(filename string) (*cacheSpec, error) { func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { var bid *entities.PbsOrtbBid winningBidsByImp := make(map[string]*entities.PbsOrtbBid) - winningBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid) + allBidsByBidder := make(map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid) roundedPrices := make(map[*entities.PbsOrtbBid]string) bidCategory := make(map[string]string) @@ -210,15 +210,15 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { } // Map this bid if it's the highest we've seen from this bidder so far - if bidMap, ok := winningBidsByBidder[bid.Bid.ImpID]; ok { + if bidMap, ok := allBidsByBidder[bid.Bid.ImpID]; ok { bidMap[pbsBid.Bidder] = append(bidMap[pbsBid.Bidder], bid) } else { - winningBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder[bid.Bid.ImpID] = map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ pbsBid.Bidder: {bid}, } } - for _, topBidsPerBidder := range winningBidsByBidder { + for _, topBidsPerBidder := range allBidsByBidder { for _, topBids := range topBidsPerBidder { sort.Slice(topBids, func(i, j int) bool { return isNewWinningBid(topBids[i].Bid, topBids[j].Bid, true) @@ -263,9 +263,9 @@ func runCacheSpec(t *testing.T, fileDisplayName string, specData *cacheSpec) { } testAuction := &auction{ - winningBids: winningBidsByImp, - winningBidsByBidder: winningBidsByBidder, - roundedPrices: roundedPrices, + winningBids: winningBidsByImp, + allBidsByBidder: allBidsByBidder, + roundedPrices: roundedPrices, } evTracking := &eventTracking{ accountID: "TEST_ACC_ID", @@ -405,7 +405,7 @@ func TestNewAuction(t *testing.T) { winningBids: map[string]*entities.PbsOrtbBid{ "imp1": &bid1p230, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p123}, "rubicon": []*entities.PbsOrtbBid{&bid1p230}, @@ -433,7 +433,7 @@ func TestNewAuction(t *testing.T) { "imp1": &bid1p230, "imp2": &bid2p144, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p230}, "rubicon": []*entities.PbsOrtbBid{&bid1p077}, @@ -462,7 +462,7 @@ func TestNewAuction(t *testing.T) { winningBids: map[string]*entities.PbsOrtbBid{ "imp1": &bid1p123, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p123}, "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, @@ -486,7 +486,7 @@ func TestNewAuction(t *testing.T) { winningBids: map[string]*entities.PbsOrtbBid{ "imp1": &bid1p088d, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p123}, "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, @@ -510,7 +510,7 @@ func TestNewAuction(t *testing.T) { winningBids: map[string]*entities.PbsOrtbBid{ "imp1": &bid1p166d, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d}, "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, @@ -537,7 +537,7 @@ func TestNewAuction(t *testing.T) { winningBids: map[string]*entities.PbsOrtbBid{ "imp1": &bid1p166d, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d}, "rubicon": []*entities.PbsOrtbBid{&bid1p088d}, @@ -563,7 +563,7 @@ func TestNewAuction(t *testing.T) { "imp1": &bid1p166d, "imp2": &bid2p166, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -645,11 +645,11 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { } type fields struct { - winningBids map[string]*entities.PbsOrtbBid - winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid - roundedPrices map[*entities.PbsOrtbBid]string - cacheIds map[*openrtb2.Bid]string - vastCacheIds map[*openrtb2.Bid]string + winningBids map[string]*entities.PbsOrtbBid + allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid + roundedPrices map[*entities.PbsOrtbBid]string + cacheIds map[*openrtb2.Bid]string + vastCacheIds map[*openrtb2.Bid]string } type args struct { adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid @@ -657,8 +657,8 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { accountDefaultBidLimit int } type want struct { - winningBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid - adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid + allBidsByBidder map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid + adapterBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid } tests := []struct { description string @@ -673,7 +673,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { "imp1": &bid1p166d, "imp2": &bid2p166, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -697,7 +697,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { preferDeals: true, }, want: want{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -724,7 +724,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { "imp1": &bid1p166d, "imp2": &bid2p166, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -748,7 +748,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { preferDeals: true, }, want: want{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077, &bid1p001}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -775,7 +775,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { "imp1": &bid1p166d, "imp2": &bid2p166, }, - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p001, &bid1p166d, &bid1p077}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -799,7 +799,7 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { preferDeals: true, }, want: want{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp1": { "appnexus": []*entities.PbsOrtbBid{&bid1p166d, &bid1p077}, "pubmatic": []*entities.PbsOrtbBid{&bid1p088d, &bid1p123}, @@ -823,14 +823,14 @@ func TestValidateAndUpdateMultiBid(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { a := &auction{ - winningBids: tt.fields.winningBids, - winningBidsByBidder: tt.fields.winningBidsByBidder, - roundedPrices: tt.fields.roundedPrices, - cacheIds: tt.fields.cacheIds, - vastCacheIds: tt.fields.vastCacheIds, + winningBids: tt.fields.winningBids, + allBidsByBidder: tt.fields.allBidsByBidder, + roundedPrices: tt.fields.roundedPrices, + cacheIds: tt.fields.cacheIds, + vastCacheIds: tt.fields.vastCacheIds, } a.validateAndUpdateMultiBid(tt.args.adapterBids, tt.args.preferDeals, tt.args.accountDefaultBidLimit) - assert.Equal(t, tt.want.winningBidsByBidder, tt.fields.winningBidsByBidder, tt.description) + assert.Equal(t, tt.want.allBidsByBidder, tt.fields.allBidsByBidder, tt.description) assert.Equal(t, tt.want.adapterBids, tt.args.adapterBids, tt.description) }) } diff --git a/exchange/exchange.go b/exchange/exchange.go index 7c604886633..003ce559dc2 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -574,7 +574,7 @@ func applyDealSupport(bidRequest *openrtb2.BidRequest, auc *auction, bidCategory errs := []error{} impDealMap := getDealTiers(bidRequest) - for impID, topBidsPerImp := range auc.winningBidsByBidder { + for impID, topBidsPerImp := range auc.allBidsByBidder { impDeal := impDealMap[impID] for bidder, topBidsPerBidder := range topBidsPerImp { bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder.String()) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index ee2e0f6ed6d..6cd48198267 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -3542,7 +3542,7 @@ func TestApplyDealSupport(t *testing.T) { } auc := &auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { test.in.bidderName: {&bid}, }, @@ -3551,8 +3551,8 @@ func TestApplyDealSupport(t *testing.T) { dealErrs := applyDealSupport(bidRequest, auc, bidCategory, nil) - assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description) - assert.Equal(t, test.expected.dealTierSatisfied, auc.winningBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description) + assert.Equal(t, test.expected.hbPbCatDur, bidCategory[auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].Bid.ID], test.description) + assert.Equal(t, test.expected.dealTierSatisfied, auc.allBidsByBidder["imp_id1"][test.in.bidderName][0].DealTierSatisfied, "expected.dealTierSatisfied=%v when %v", test.expected.dealTierSatisfied, test.description) if len(test.expected.dealErr) > 0 { assert.Containsf(t, dealErrs, errors.New(test.expected.dealErr), "Expected error message not found in deal errors") } @@ -3593,7 +3593,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) { }, }, auc: &auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, @@ -3639,7 +3639,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) { }, }, auc: &auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, @@ -3690,7 +3690,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) { }, }, auc: &auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, @@ -3729,7 +3729,7 @@ func TestApplyDealSupportMultiBid(t *testing.T) { errs := applyDealSupport(tt.args.bidRequest, tt.args.auc, tt.args.bidCategory, tt.args.multiBid) assert.Equal(t, tt.want.errs, errs) - for impID, topBidsPerImp := range tt.args.auc.winningBidsByBidder { + for impID, topBidsPerImp := range tt.args.auc.allBidsByBidder { for bidder, topBidsPerBidder := range topBidsPerImp { for i, topBid := range topBidsPerBidder { assert.Equal(t, tt.want.expectedHbPbCatDur[impID][bidder.String()][i], tt.args.bidCategory[topBid.Bid.ID], tt.name) diff --git a/exchange/exchangetest/targeting-always-include-deals.json b/exchange/exchangetest/targeting-always-include-deals.json new file mode 100644 index 00000000000..1ffb808841b --- /dev/null +++ b/exchange/exchangetest/targeting-always-include-deals.json @@ -0,0 +1,296 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + } + } + }, + { + "id": "imp-id-2", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 2 + }, + "audienceNetwork": { + "placementId": "some-other-placement" + } + } + } + } + } + ], + "ext": { + "prebid": { + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": false, + "alwaysincludedeals": true + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.71, + "w": 200, + "h": 250, + "crid": "creative-1", + "dealid": "deal-1" + }, + "bidType": "video" + }, + { + "ortbBid": { + "id": "losing-bid", + "impid": "my-imp-id", + "price": 0.21, + "w": 200, + "h": 250, + "crid": "creative-2", + "dealid": "deal-2" + }, + "bidType": "video" + }, + { + "ortbBid": { + "id": "other-bid", + "impid": "imp-id-2", + "price": 0.61, + "w": 300, + "h": 500, + "crid": "creative-3", + "dealid": "deal-3" + }, + "bidType": "video" + } + ], + "seat": "appnexus" + } + ] + } + }, + "audienceNetwork": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "contending-bid", + "impid": "my-imp-id", + "price": 0.51, + "w": 200, + "h": 250, + "crid": "creative-4", + "dealid": "deal-4" + }, + "bidType": "video" + }, + { + "ortbBid": { + "id": "losing-bid-aN", + "impid": "imp-id-2", + "price": 0.40, + "w": 200, + "h": 250, + "crid": "creative-5" + }, + "bidType": "video" + } + ], + "seat": "audienceNetwork" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "audienceNetwork", + "bid": [ + { + "id": "contending-bid", + "impid": "my-imp-id", + "price": 0.51, + "w": 200, + "h": 250, + "crid": "creative-4", + "dealid": "deal-4", + "ext": { + "origbidcpm": 0.51, + "prebid": { + "meta": { + "adaptercode": "audienceNetwork" + }, + "type": "video", + "targeting": { + "hb_bidder_audienceNe": "audienceNetwork", + "hb_cache_host_audien": "www.pbcserver.com", + "hb_cache_path_audien": "/pbcache/endpoint", + "hb_deal_audienceNetw": "deal-4", + "hb_pb_audienceNetwor": "0.50", + "hb_size_audienceNetw": "200x250" + } + } + } + }, + { + "id": "losing-bid-aN", + "impid": "imp-id-2", + "price": 0.40, + "w": 200, + "h": 250, + "crid": "creative-5", + "ext": { + "origbidcpm": 0.40, + "prebid": { + "meta": { + "adaptercode": "audienceNetwork" + }, + "type": "video" + } + } + } + ] + }, + { + "seat": "appnexus", + "bid": [ + { + "id": "winning-bid", + "impid": "my-imp-id", + "price": 0.71, + "w": 200, + "h": 250, + "crid": "creative-1", + "dealid": "deal-1", + "ext": { + "origbidcpm": 0.71, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.70", + "hb_pb_appnexus": "0.70", + "hb_deal":"deal-1", + "hb_deal_appnexus":"deal-1", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + }, + { + "id": "losing-bid", + "impid": "my-imp-id", + "price": 0.21, + "w": 200, + "h": 250, + "crid": "creative-2", + "dealid": "deal-2", + "ext": { + "origbidcpm": 0.21, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video" + } + } + }, + { + "id": "other-bid", + "impid": "imp-id-2", + "price": 0.61, + "w": 300, + "h": 500, + "crid": "creative-3", + "dealid": "deal-3", + "ext": { + "origbidcpm": 0.61, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_deal":"deal-3", + "hb_deal_appnexus":"deal-3", + "hb_pb": "0.60", + "hb_pb_appnexus": "0.60", + "hb_size": "300x500", + "hb_size_appnexus": "300x500" + } + } + } + } + ] + } + ] + } + } +} diff --git a/exchange/targeting.go b/exchange/targeting.go index 0047ba8b06e..d5ce7152a77 100644 --- a/exchange/targeting.go +++ b/exchange/targeting.go @@ -26,6 +26,7 @@ type targetData struct { includeCacheVast bool includeFormat bool preferDeals bool + alwaysIncludeDeals bool // cacheHost and cachePath exist to supply cache host and path as targeting parameters cacheHost string cachePath string @@ -38,7 +39,7 @@ type targetData struct { // it's ok if those stay in the auction. For now, this method implements a very naive cache strategy. // In the future, we should implement a more clever retry & backoff strategy to balance the success rate & performance. func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMapping map[string]string, truncateTargetAttr *int, multiBidMap map[string]openrtb_ext.ExtMultiBid) { - for impId, topBidsPerImp := range auc.winningBidsByBidder { + for impId, topBidsPerImp := range auc.allBidsByBidder { overallWinner := auc.winningBids[impId] for originalBidderName, topBidsPerBidder := range topBidsPerImp { targetingBidderCode := originalBidderName @@ -61,40 +62,42 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi isOverallWinner := overallWinner == topBid + bidHasDeal := len(topBid.Bid.DealID) > 0 + targets := make(map[string]string, 10) if cpm, ok := auc.roundedPrices[topBid]; ok { - targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbpbConstantKey, cpm, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } - targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(targetingBidderCode), targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbBidderConstantKey, string(targetingBidderCode), targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) if hbSize := makeHbSize(topBid.Bid); hbSize != "" { - targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbSizeConstantKey, hbSize, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if cacheID, ok := auc.cacheIds[topBid.Bid]; ok { - targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbCacheKey, cacheID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if vastID, ok := auc.vastCacheIds[topBid.Bid]; ok { - targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if targData.includeFormat { - targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBid.BidType), targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBid.BidType), targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if targData.cacheHost != "" { - targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if targData.cachePath != "" { - targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbConstantCachePathKey, targData.cachePath, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } - if deal := topBid.Bid.DealID; len(deal) > 0 { - targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, deal, targetingBidderCode, isOverallWinner, truncateTargetAttr) + if bidHasDeal { + targData.addKeys(targets, openrtb_ext.HbDealIDConstantKey, topBid.Bid.DealID, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if isApp { - targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbEnvKey, openrtb_ext.HbEnvKeyApp, targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } if len(categoryMapping) > 0 { - targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBid.Bid.ID], targetingBidderCode, isOverallWinner, truncateTargetAttr) + targData.addKeys(targets, openrtb_ext.HbCategoryDurationKey, categoryMapping[topBid.Bid.ID], targetingBidderCode, isOverallWinner, truncateTargetAttr, bidHasDeal) } topBid.BidTargets = targets } @@ -102,7 +105,7 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi } } -func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool, truncateTargetAttr *int) { +func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.TargetingKey, value string, bidderName openrtb_ext.BidderName, overallWinner bool, truncateTargetAttr *int, bidHasDeal bool) { var maxLength int if truncateTargetAttr != nil { maxLength = *truncateTargetAttr @@ -112,7 +115,7 @@ func (targData *targetData) addKeys(keys map[string]string, key openrtb_ext.Targ } else { maxLength = MaxKeyLength } - if targData.includeBidderKeys { + if targData.includeBidderKeys || (targData.alwaysIncludeDeals && bidHasDeal) { keys[key.BidderKey(bidderName, maxLength)] = value } if targData.includeWinners && overallWinner { diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index c124178e9c8..e6cc429e8ea 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -319,6 +319,11 @@ var bid2p166 *openrtb2.Bid = &openrtb2.Bid{ Price: 1.66, } +var bid175 *openrtb2.Bid = &openrtb2.Bid{ + Price: 1.75, + DealID: "mydeal2", +} + var ( truncateTargetAttrValue10 int = 10 truncateTargetAttrValue5 int = 5 @@ -339,7 +344,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeWinners: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -374,7 +379,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeBidderKeys: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -409,6 +414,54 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ }, TruncateTargetAttr: nil, }, + { + Description: "Targeting with alwaysIncludeDeals", + TargetData: targetData{ + priceGranularity: lookupPriceGranularity("med"), + alwaysIncludeDeals: true, + }, + Auction: auction{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: {{ + Bid: bid111, + BidType: openrtb_ext.BidTypeBanner, + }}, + openrtb_ext.BidderRubicon: {{ + Bid: bid175, + BidType: openrtb_ext.BidTypeBanner, + }}, + openrtb_ext.BidderPubmatic: {{ + Bid: bid123, + BidType: openrtb_ext.BidTypeBanner, + }}, + }, + }, + }, + ExpectedPbsBids: map[string]map[openrtb_ext.BidderName][]ExpectedPbsBid{ + "ImpId-1": { + openrtb_ext.BidderAppnexus: []ExpectedPbsBid{ + { + BidTargets: map[string]string{ + "hb_bidder_appnexus": "appnexus", + "hb_pb_appnexus": "1.10", + "hb_deal_appnexus": "mydeal", + }, + }, + }, + openrtb_ext.BidderRubicon: []ExpectedPbsBid{ + { + BidTargets: map[string]string{ + "hb_bidder_rubicon": "rubicon", + "hb_pb_rubicon": "1.70", + "hb_deal_rubicon": "mydeal2", + }, + }, + }, + }, + }, + TruncateTargetAttr: nil, + }, { Description: "Full basic targeting with hd_format", TargetData: targetData{ @@ -418,7 +471,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeFormat: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -467,7 +520,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ cachePath: "cache", }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -520,7 +573,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeBidderKeys: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -550,7 +603,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeBidderKeys: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -592,7 +645,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeBidderKeys: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -634,7 +687,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeBidderKeys: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -676,7 +729,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeWinners: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -711,7 +764,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeWinners: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -746,7 +799,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeWinners: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: {{ Bid: bid123, @@ -783,7 +836,7 @@ var TargetingTests []TargetingTestData = []TargetingTestData{ includeFormat: true, }, Auction: auction{ - winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ + allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "ImpId-1": { openrtb_ext.BidderAppnexus: { { @@ -918,7 +971,7 @@ func TestSetTargeting(t *testing.T) { auc.setRoundedPrices(test.TargetData) winningBids := make(map[string]*entities.PbsOrtbBid) // Set winning bids from the auction data - for imp, bidsByBidder := range auc.winningBidsByBidder { + for imp, bidsByBidder := range auc.allBidsByBidder { for _, bids := range bidsByBidder { for _, bid := range bids { if winningBid, ok := winningBids[imp]; ok { @@ -939,12 +992,12 @@ func TestSetTargeting(t *testing.T) { for i, expected := range expectedTargets { assert.Equal(t, expected.BidTargets, - auc.winningBidsByBidder[imp][bidder][i].BidTargets, + auc.allBidsByBidder[imp][bidder][i].BidTargets, "Test: %s\nTargeting failed for bidder %s on imp %s.", test.Description, string(bidder), imp) - assert.Equal(t, expected.TargetBidderCode, auc.winningBidsByBidder[imp][bidder][i].TargetBidderCode) + assert.Equal(t, expected.TargetBidderCode, auc.allBidsByBidder[imp][bidder][i].TargetBidderCode) } } } diff --git a/exchange/utils.go b/exchange/utils.go index 8735806887f..676c015ae0e 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -879,6 +879,7 @@ func getExtTargetData(requestExtPrebid *openrtb_ext.ExtRequestPrebid, cacheInstr priceGranularity: *requestExtPrebid.Targeting.PriceGranularity, mediaTypePriceGranularity: requestExtPrebid.Targeting.MediaTypePriceGranularity, preferDeals: requestExtPrebid.Targeting.PreferDeals, + alwaysIncludeDeals: requestExtPrebid.Targeting.AlwaysIncludeDeals, } } diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index 90a95a1b16e..f5b403d6a1e 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -199,6 +199,7 @@ type ExtRequestTargeting struct { DurationRangeSec []int `json:"durationrangesec,omitempty"` PreferDeals bool `json:"preferdeals,omitempty"` AppendBidderNames bool `json:"appendbiddernames,omitempty"` + AlwaysIncludeDeals bool `json:"alwaysincludedeals,omitempty"` } type ExtIncludeBrandCategory struct { From 8e5a7857e2397eb2e8a73820af605301adb7fac8 Mon Sep 17 00:00:00 2001 From: bold-win <157734532+bold-win@users.noreply.github.com> Date: Mon, 19 Feb 2024 09:54:00 +0100 Subject: [PATCH 15/69] New adapter: BoldwinX (#3430) Co-authored-by: boldwin --- adapters/bwx/bwx.go | 158 ++++++++++ adapters/bwx/bwx_test.go | 34 +++ adapters/bwx/bwxtest/exemplary/banner.json | 281 ++++++++++++++++++ adapters/bwx/bwxtest/exemplary/native.json | 164 ++++++++++ adapters/bwx/bwxtest/exemplary/video.json | 204 +++++++++++++ .../bwxtest/supplemental/bad-response.json | 106 +++++++ .../bwxtest/supplemental/empty-mediatype.json | 190 ++++++++++++ .../supplemental/empty-seatbid-0-bid.json | 111 +++++++ .../bwxtest/supplemental/empty-seatbid.json | 111 +++++++ .../invalid-ext-bidder-object.json | 49 +++ .../supplemental/invalid-ext-object.json | 47 +++ .../supplemental/invalid-mediatype.json | 187 ++++++++++++ .../bwx/bwxtest/supplemental/status-204.json | 100 +++++++ .../bwx/bwxtest/supplemental/status-400.json | 106 +++++++ .../bwx/bwxtest/supplemental/status-503.json | 105 +++++++ .../supplemental/unexpected-status.json | 106 +++++++ adapters/bwx/params_test.go | 54 ++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_bwx.go | 6 + static/bidder-info/bwx.yaml | 19 ++ static/bidder-params/bwx.json | 21 ++ 22 files changed, 2163 insertions(+) create mode 100644 adapters/bwx/bwx.go create mode 100644 adapters/bwx/bwx_test.go create mode 100644 adapters/bwx/bwxtest/exemplary/banner.json create mode 100644 adapters/bwx/bwxtest/exemplary/native.json create mode 100644 adapters/bwx/bwxtest/exemplary/video.json create mode 100644 adapters/bwx/bwxtest/supplemental/bad-response.json create mode 100644 adapters/bwx/bwxtest/supplemental/empty-mediatype.json create mode 100644 adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json create mode 100644 adapters/bwx/bwxtest/supplemental/empty-seatbid.json create mode 100644 adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json create mode 100644 adapters/bwx/bwxtest/supplemental/invalid-ext-object.json create mode 100644 adapters/bwx/bwxtest/supplemental/invalid-mediatype.json create mode 100644 adapters/bwx/bwxtest/supplemental/status-204.json create mode 100644 adapters/bwx/bwxtest/supplemental/status-400.json create mode 100644 adapters/bwx/bwxtest/supplemental/status-503.json create mode 100644 adapters/bwx/bwxtest/supplemental/unexpected-status.json create mode 100644 adapters/bwx/params_test.go create mode 100644 openrtb_ext/imp_bwx.go create mode 100644 static/bidder-info/bwx.yaml create mode 100644 static/bidder-params/bwx.json diff --git a/adapters/bwx/bwx.go b/adapters/bwx/bwx.go new file mode 100644 index 00000000000..bab1fb7d6a2 --- /dev/null +++ b/adapters/bwx/bwx.go @@ -0,0 +1,158 @@ +package bwx + +import ( + "encoding/json" + "fmt" + "net/http" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type bidType struct { + Type string `json:"type"` +} + +type bidExt struct { + Prebid bidType `json:"prebid"` +} + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + tmpl, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint URL template: %v", err) + } + + bidder := &adapter{ + endpoint: tmpl, + } + + return bidder, nil +} + +func (a *adapter) buildEndpointFromRequest(imp *openrtb2.Imp) (string, error) { + var impExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &impExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize bidder impression extension: %v", err), + } + } + + var boldwinxExt openrtb_ext.ExtBWX + if err := json.Unmarshal(impExt.Bidder, &boldwinxExt); err != nil { + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Failed to deserialize BoldwinX extension: %v", err), + } + } + + endpointParams := macros.EndpointTemplateParams{ + Host: boldwinxExt.Env, + SourceId: boldwinxExt.Pid, + } + + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + var errs []error + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestCopy := *request + for _, imp := range request.Imp { + requestCopy.Imp = []openrtb2.Imp{imp} + + endpoint, err := a.buildEndpointFromRequest(&imp) + if err != nil { + errs = append(errs, err) + continue + } + + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + errs = append(errs, err) + continue + } + + request := &adapters.RequestData{ + Method: http.MethodPost, + Body: requestJSON, + Uri: endpoint, + Headers: headers, + } + + requests = append(requests, request) + } + + return requests, errs +} + +func (a *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(bidderRawResponse) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(bidderRawResponse); err != nil { + return nil, []error{err} + } + + var bidResp openrtb2.BidResponse + if err := json.Unmarshal(bidderRawResponse.Body, &bidResp); err != nil { + return nil, []error{err} + } + + if len(bidResp.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Array SeatBid cannot be empty", + }} + } + + return prepareBidResponse(bidResp.SeatBid) +} + +func prepareBidResponse(seats []openrtb2.SeatBid) (*adapters.BidderResponse, []error) { + var errs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(seats)) + + for _, seatBid := range seats { + for idx, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + errs = append(errs, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[idx], + BidType: bidType, + }) + } + } + + return bidResponse, errs +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("failed to parse bid mtype (%d) for impression id \"%s\"", bid.MType, bid.ImpID) + } +} diff --git a/adapters/bwx/bwx_test.go b/adapters/bwx/bwx_test.go new file mode 100644 index 00000000000..4e984008cec --- /dev/null +++ b/adapters/bwx/bwx_test.go @@ -0,0 +1,34 @@ +package bwx + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderBWX, + config.Adapter{ + Endpoint: "http://rtb.boldwin.live/?pid={{.SourceId}}&host={{.Host}}&pbs=1", + }, + config.Server{ + ExternalUrl: "http://hosturl.com", + GvlID: 1, + DataCenter: "2", + }, + ) + + assert.NoError(t, buildErr) + adapterstest.RunJSONBidderTest(t, "bwxtest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderBWX, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/bwx/bwxtest/exemplary/banner.json b/adapters/bwx/bwxtest/exemplary/banner.json new file mode 100644 index 00000000000..af82099d24f --- /dev/null +++ b/adapters/bwx/bwxtest/exemplary/banner.json @@ -0,0 +1,281 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + }, + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "1", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 0 + }, + "ext": { + "bidder": { + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + }, + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "2", + "secure": 1, + "bidfloor": 0.2, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250, + "pos": 4 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": ["IAB12"], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "1", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid1", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "2", + "price": 2.4, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test3", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bwx/bwxtest/exemplary/native.json b/adapters/bwx/bwxtest/exemplary/native.json new file mode 100644 index 00000000000..0bba948285e --- /dev/null +++ b/adapters/bwx/bwxtest/exemplary/native.json @@ -0,0 +1,164 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "native": { + "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}", + "ver": "1.1" + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 4, + "ext": { + "prebid": { + "type": "native" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "{}", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 4, + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/bwx/bwxtest/exemplary/video.json b/adapters/bwx/bwxtest/exemplary/video.json new file mode 100644 index 00000000000..ffe74ee7cc4 --- /dev/null +++ b/adapters/bwx/bwxtest/exemplary/video.json @@ -0,0 +1,204 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "video": { + "mimes": [ + "video/mp4", + "video/ogg", + "video/webm" + ], + "minduration": 3, + "maxduration": 3000, + "protocols": [ + 2, + 3, + 5, + 6, + 7, + 8 + ], + "w": 480, + "h": 320, + "linearity": 1, + "playbackmethod": [ + 2 + ], + "pos": 0 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "id", + "impid": "id", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/bwx/bwxtest/supplemental/bad-response.json b/adapters/bwx/bwxtest/supplemental/bad-response.json new file mode 100644 index 00000000000..8a32dab14b6 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/bad-response.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/bwxtest/supplemental/empty-mediatype.json b/adapters/bwx/bwxtest/supplemental/empty-mediatype.json new file mode 100644 index 00000000000..ad319e84f20 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/empty-mediatype.json @@ -0,0 +1,190 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "mtype": 1, + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "ext": { + "some": "value" + } + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "failed to parse bid mtype (0) for impression id \"2\"", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json b/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json new file mode 100644 index 00000000000..ce9d7875857 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/empty-seatbid-0-bid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [], + "expectedBidResponses": [{"currency":"USD","bids":[]}] +} diff --git a/adapters/bwx/bwxtest/supplemental/empty-seatbid.json b/adapters/bwx/bwxtest/supplemental/empty-seatbid.json new file mode 100644 index 00000000000..2bb3290f8aa --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/empty-seatbid.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "a1580f2f-be6d-11eb-a150-d094662c1c35", + "bidid": "359da97d0384d8a14767029c18fd840d", + "seatbid": [], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Array SeatBid cannot be empty", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json b/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json new file mode 100644 index 00000000000..ed081b7912b --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/invalid-ext-bidder-object.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": [] + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize BoldwinX extension: json: cannot unmarshal array into Go value of type openrtb_ext.ExtBWX", + "comparison": "literal" + } + ] +} diff --git a/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json b/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json new file mode 100644 index 00000000000..aa215eb3e34 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/invalid-ext-object.json @@ -0,0 +1,47 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": "" + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [], + "expectedMakeRequestsErrors": [ + { + "value": "Failed to deserialize bidder impression extension: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder", + "comparison": "literal" + } + ] +} diff --git a/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json b/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json new file mode 100644 index 00000000000..aff08f53786 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/invalid-mediatype.json @@ -0,0 +1,187 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "id", + "bidid": "id", + "seatbid": [ + { + "bid": [ + { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, { + "id": "id", + "impid": "2", + "price": 1.2, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test2", + "adomain": [ + "test.com" + ], + "cat": [ + "IAB1" + ], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250 + } + ], + "seat": "seat" + } + ], + "cur": "USD" + } + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "failed to parse bid mtype (0) for impression id \"2\"", + "comparison": "literal" + } + ], + "expectedBidResponses": [ + { + "currency":"USD", + "bids":[ + { + "bid": { + "id": "id", + "impid": "1", + "price": 0.1, + "nurl": "http://test.com/nurl", + "burl": "http://test.com/burl", + "adm": "Test1", + "adomain": ["test.com"], + "cat": ["IAB1"], + "cid": "cid", + "crid": "crid", + "w": 300, + "h": 250, + "mtype": 1, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/bwx/bwxtest/supplemental/status-204.json b/adapters/bwx/bwxtest/supplemental/status-204.json new file mode 100644 index 00000000000..e89b6e19dc8 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/status-204.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 204, + "body": {} + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/bwxtest/supplemental/status-400.json b/adapters/bwx/bwxtest/supplemental/status-400.json new file mode 100644 index 00000000000..2094204026e --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/status-400.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 400, + "body": "The Key has a different ad format" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/bwxtest/supplemental/status-503.json b/adapters/bwx/bwxtest/supplemental/status-503.json new file mode 100644 index 00000000000..3d5a6170345 --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/status-503.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 503 + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 503. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/bwxtest/supplemental/unexpected-status.json b/adapters/bwx/bwxtest/supplemental/unexpected-status.json new file mode 100644 index 00000000000..f3d7ecc740f --- /dev/null +++ b/adapters/bwx/bwxtest/supplemental/unexpected-status.json @@ -0,0 +1,106 @@ +{ + "mockBidRequest": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://rtb.boldwin.live/?pid=3163e2c9e034770c0daaa98c7613b573&host=boldwinx-stage&pbs=1", + "body": { + "id": "id", + "imp": [ + { + "id": "id", + "secure": 1, + "bidfloor": 0.01, + "bidfloorcur": "USD", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "env": "boldwinx-stage", + "pid": "3163e2c9e034770c0daaa98c7613b573" + } + } + } + ], + "device": { + "ua": "UA", + "ip": "123.3.4.123" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "id": "userid" + }, + "site": { + "id": "id", + "domain": "test,com", + "cat": [ + "IAB12" + ], + "publisher": { + "id": "pubid" + } + } + } + }, + "mockResponse": { + "status": 403, + "body": "Access is denied" + } + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 403. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/bwx/params_test.go b/adapters/bwx/params_test.go new file mode 100644 index 00000000000..f51d863033f --- /dev/null +++ b/adapters/bwx/params_test.go @@ -0,0 +1,54 @@ +package bwx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +var validParams = []string{ + `{"env":"boldwinx-stage", "pid":"123456"}`, + `{"pid":"123456"}`, +} + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderBWX, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected boldwinx params: %s", validParam) + } + } +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `[]`, + `{}`, + `{"some": "param"}`, + `{"env":"boldwinx-stage"}`, + `{"othervalue":"Lorem ipsum"}`, + `{"env":"boldwinx-stage", pid:""}`, + `{pid:101010}`, + `{pid:"valid-pid", env: 0}`, +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderBWX, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 639ea93ab2e..e188d2936e2 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -59,6 +59,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/bmtm" "github.com/prebid/prebid-server/v2/adapters/boldwin" "github.com/prebid/prebid-server/v2/adapters/brave" + "github.com/prebid/prebid-server/v2/adapters/bwx" cadentaperturemx "github.com/prebid/prebid-server/v2/adapters/cadent_aperture_mx" "github.com/prebid/prebid-server/v2/adapters/ccx" "github.com/prebid/prebid-server/v2/adapters/coinzilla" @@ -260,6 +261,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderBmtm: bmtm.Builder, openrtb_ext.BidderBoldwin: boldwin.Builder, openrtb_ext.BidderBrave: brave.Builder, + openrtb_ext.BidderBWX: bwx.Builder, openrtb_ext.BidderCadentApertureMX: cadentaperturemx.Builder, openrtb_ext.BidderCcx: ccx.Builder, openrtb_ext.BidderCoinzilla: coinzilla.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 91217f18ce2..173120f7301 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -75,6 +75,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderBmtm, BidderBoldwin, BidderBrave, + BidderBWX, BidderCadentApertureMX, BidderCcx, BidderCoinzilla, @@ -354,6 +355,7 @@ const ( BidderBmtm BidderName = "bmtm" BidderBoldwin BidderName = "boldwin" BidderBrave BidderName = "brave" + BidderBWX BidderName = "bwx" BidderCadentApertureMX BidderName = "cadent_aperture_mx" BidderCcx BidderName = "ccx" BidderCoinzilla BidderName = "coinzilla" diff --git a/openrtb_ext/imp_bwx.go b/openrtb_ext/imp_bwx.go new file mode 100644 index 00000000000..9181beb05b0 --- /dev/null +++ b/openrtb_ext/imp_bwx.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtBWX struct { + Env string `json:"env"` + Pid string `json:"pid"` +} diff --git a/static/bidder-info/bwx.yaml b/static/bidder-info/bwx.yaml new file mode 100644 index 00000000000..cf716c3890d --- /dev/null +++ b/static/bidder-info/bwx.yaml @@ -0,0 +1,19 @@ +endpoint: "http://rtb.boldwin.live?pid={{.SourceId}}&host={{.Host}}&pbs=1" +maintainer: + email: "prebid@bold-win.com" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + # BoldwinX supports user syncing, but requires configuration by the host. contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect diff --git a/static/bidder-params/bwx.json b/static/bidder-params/bwx.json new file mode 100644 index 00000000000..873aadad40c --- /dev/null +++ b/static/bidder-params/bwx.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "BoldwinX Adapter Params", + "description": "A schema which validates params accepted by the BoldwinX adapter", + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "BoldwinX environment", + "minLength": 1 + }, + "pid": { + "type": "string", + "description": "Unique placement ID", + "minLength": 1 + } + }, + "required": [ + "pid" + ] + } From a4f5c119a87e706edd8d8ff3fce368d226973a24 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Tue, 20 Feb 2024 18:45:21 +0100 Subject: [PATCH 16/69] Fix go vet composite literals with unkeyed fields (#3507) --- amp/parse.go | 4 +- amp/parse_test.go | 198 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 152 insertions(+), 50 deletions(-) diff --git a/amp/parse.go b/amp/parse.go index 54dcc8e0df3..12663ee93bd 100644 --- a/amp/parse.go +++ b/amp/parse.go @@ -73,7 +73,7 @@ func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWrit warningMsg = validateTCf2ConsentString(ampParams.Consent) } case ConsentUSPrivacy: - rv = ccpa.ConsentWriter{ampParams.Consent} + rv = ccpa.ConsentWriter{Consent: ampParams.Consent} if ccpa.ValidateConsent(ampParams.Consent) { if parseGdprApplies(ampParams.GdprApplies) == 1 { // Log warning because AMP request comes with both a valid CCPA string and gdpr_applies set to true @@ -85,7 +85,7 @@ func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWrit } default: if ccpa.ValidateConsent(ampParams.Consent) { - rv = ccpa.ConsentWriter{ampParams.Consent} + rv = ccpa.ConsentWriter{Consent: ampParams.Consent} if parseGdprApplies(ampParams.GdprApplies) == 1 { warningMsg = "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string" } diff --git a/amp/parse_test.go b/amp/parse_test.go index a377c4711fb..f2f097284c5 100644 --- a/amp/parse_test.go +++ b/amp/parse_test.go @@ -284,18 +284,24 @@ func TestPrivacyReader(t *testing.T) { ampParams: Params{Consent: "1YYY"}, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, warning: nil, }, }, { desc: "No consent type, valid CCPA consent string and gdpr_applies set to true: expect a CCPA consent writer and a warning", in: testInput{ - ampParams: Params{Consent: "1YYY", GdprApplies: &boolTrue}, + ampParams: Params{ + Consent: "1YYY", + GdprApplies: &boolTrue, + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, - warning: &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, + warning: &errortypes.Warning{ + Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { @@ -304,8 +310,11 @@ func TestPrivacyReader(t *testing.T) { ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA"}, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One}, - warning: nil, + policyWriter: gdpr.ConsentWriter{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + RegExtGDPR: &int8One, + }, + warning: nil, }, }, }, @@ -316,41 +325,63 @@ func TestPrivacyReader(t *testing.T) { { desc: "Unrecognized consent type was specified and invalid consent string provided: expect nil policy writer and a warning", in: testInput{ - ampParams: Params{ConsentType: 101, Consent: "NOT_CCPA_NOR_GDPR_TCF2"}, + ampParams: Params{ + ConsentType: 101, + Consent: "NOT_CCPA_NOR_GDPR_TCF2", + }, }, expected: expectedResults{ policyWriter: privacy.NilPolicyWriter{}, - warning: &errortypes.Warning{Message: "Consent string 'NOT_CCPA_NOR_GDPR_TCF2' is not recognized as one of the supported formats CCPA or TCF2.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + warning: &errortypes.Warning{ + Message: "Consent string 'NOT_CCPA_NOR_GDPR_TCF2' is not recognized as one of the supported formats CCPA or TCF2.", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "Unrecognized consent type specified but query params come with a valid CCPA consent string: expect a CCPA consent writer and no error nor warning", in: testInput{ - ampParams: Params{ConsentType: 101, Consent: "1YYY"}, + ampParams: Params{ + ConsentType: 101, + Consent: "1YYY", + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, warning: nil, }, }, { desc: "Unrecognized consent type, valid CCPA consent string and gdpr_applies set to true: expect a CCPA consent writer and a warning", in: testInput{ - ampParams: Params{ConsentType: 101, Consent: "1YYY", GdprApplies: &boolTrue}, + ampParams: Params{ + ConsentType: 101, + Consent: "1YYY", + GdprApplies: &boolTrue, + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, - warning: &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, + warning: &errortypes.Warning{ + Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "Unrecognized consent type, valid TCF2 consent string and gdpr_applies not set: expect GDPR consent writer and no error nor warning", in: testInput{ - ampParams: Params{ConsentType: 101, Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA"}, + ampParams: Params{ + ConsentType: 101, + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One}, - warning: nil, + policyWriter: gdpr.ConsentWriter{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + RegExtGDPR: &int8One, + }, + warning: nil, }, }, }, @@ -361,51 +392,91 @@ func TestPrivacyReader(t *testing.T) { { desc: "GDPR consent string is invalid, but consent type is TCF2: return a valid GDPR writer and warn about the GDPR string being invalid", in: testInput{ - ampParams: Params{Consent: "INVALID_GDPR", ConsentType: ConsentTCF2, GdprApplies: nil}, + ampParams: Params{ + Consent: "INVALID_GDPR", + ConsentType: ConsentTCF2, + GdprApplies: nil, + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"INVALID_GDPR", &int8One}, - warning: &errortypes.Warning{Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: gdpr.ConsentWriter{ + Consent: "INVALID_GDPR", + RegExtGDPR: &int8One, + }, + warning: &errortypes.Warning{ + Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "GDPR consent string is invalid, consent type is TCF2, gdpr_applies is set to true: return a valid GDPR writer and warn about the GDPR string being invalid", in: testInput{ - ampParams: Params{Consent: "INVALID_GDPR", ConsentType: ConsentTCF2, GdprApplies: &boolFalse}, + ampParams: Params{ + Consent: "INVALID_GDPR", + ConsentType: ConsentTCF2, + GdprApplies: &boolFalse, + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"INVALID_GDPR", &int8Zero}, - warning: &errortypes.Warning{Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: gdpr.ConsentWriter{ + Consent: "INVALID_GDPR", + RegExtGDPR: &int8Zero, + }, + warning: &errortypes.Warning{ + Message: "Consent string 'INVALID_GDPR' is not a valid TCF2 consent string.", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "Valid GDPR consent string, gdpr_applies is set to false, return a valid GDPR writer, no warning", in: testInput{ - ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2, GdprApplies: &boolFalse}, + ampParams: Params{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + ConsentType: ConsentTCF2, + GdprApplies: &boolFalse, + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8Zero}, - warning: nil, + policyWriter: gdpr.ConsentWriter{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + RegExtGDPR: &int8Zero, + }, + warning: nil, }, }, { desc: "Valid GDPR consent string, gdpr_applies is set to true, return a valid GDPR writer and no warning", in: testInput{ - ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2, GdprApplies: &boolTrue}, + ampParams: Params{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + ConsentType: ConsentTCF2, + GdprApplies: &boolTrue, + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One}, - warning: nil, + policyWriter: gdpr.ConsentWriter{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + RegExtGDPR: &int8One, + }, + warning: nil, }, }, { desc: "Valid GDPR consent string, return a valid GDPR writer and no warning", in: testInput{ - ampParams: Params{Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", ConsentType: ConsentTCF2}, + ampParams: Params{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + ConsentType: ConsentTCF2, + }, }, expected: expectedResults{ - policyWriter: gdpr.ConsentWriter{"CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", &int8One}, - warning: nil, + policyWriter: gdpr.ConsentWriter{ + Consent: "CPdiPIJPdiPIJACABBENAzCv_____3___wAAAQNd_X9cAAAAAAAA", + RegExtGDPR: &int8One, + }, + warning: nil, }, }, }, @@ -416,30 +487,46 @@ func TestPrivacyReader(t *testing.T) { { desc: "CCPA consent string is invalid: return a valid writer a warning about the string being invalid", in: testInput{ - ampParams: Params{Consent: "XXXX", ConsentType: ConsentUSPrivacy}, + ampParams: Params{ + Consent: "XXXX", + ConsentType: ConsentUSPrivacy, + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"XXXX"}, - warning: &errortypes.Warning{Message: "Consent string 'XXXX' is not a valid CCPA consent string.", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: ccpa.ConsentWriter{Consent: "XXXX"}, + warning: &errortypes.Warning{ + Message: "Consent string 'XXXX' is not a valid CCPA consent string.", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "Valid CCPA consent string, gdpr_applies is set to true: return a valid GDPR writer and warn about the gdpr_applies value.", in: testInput{ - ampParams: Params{Consent: "1YYY", ConsentType: ConsentUSPrivacy, GdprApplies: &boolTrue}, + ampParams: Params{ + Consent: "1YYY", + ConsentType: ConsentUSPrivacy, + GdprApplies: &boolTrue, + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, - warning: &errortypes.Warning{Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", WarningCode: errortypes.InvalidPrivacyConsentWarningCode}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, + warning: &errortypes.Warning{ + Message: "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string", + WarningCode: errortypes.InvalidPrivacyConsentWarningCode, + }, }, }, { desc: "Valid CCPA consent string, return a valid GDPR writer and no warning", in: testInput{ - ampParams: Params{Consent: "1YYY", ConsentType: ConsentUSPrivacy}, + ampParams: Params{ + Consent: "1YYY", + ConsentType: ConsentUSPrivacy, + }, }, expected: expectedResults{ - policyWriter: ccpa.ConsentWriter{"1YYY"}, + policyWriter: ccpa.ConsentWriter{Consent: "1YYY"}, warning: nil, }, }, @@ -469,19 +556,34 @@ func TestBuildGdprTCF2ConsentWriter(t *testing.T) { expectedWriter gdpr.ConsentWriter }{ { - desc: "gdpr_applies not set", - inParams: Params{Consent: consentString}, - expectedWriter: gdpr.ConsentWriter{consentString, &int8One}, + desc: "gdpr_applies not set", + inParams: Params{Consent: consentString}, + expectedWriter: gdpr.ConsentWriter{ + Consent: consentString, + RegExtGDPR: &int8One, + }, }, { - desc: "gdpr_applies set to false", - inParams: Params{Consent: consentString, GdprApplies: &boolFalse}, - expectedWriter: gdpr.ConsentWriter{consentString, &int8Zero}, + desc: "gdpr_applies set to false", + inParams: Params{ + Consent: consentString, + GdprApplies: &boolFalse, + }, + expectedWriter: gdpr.ConsentWriter{ + Consent: consentString, + RegExtGDPR: &int8Zero, + }, }, { - desc: "gdpr_applies set to true", - inParams: Params{Consent: consentString, GdprApplies: &boolTrue}, - expectedWriter: gdpr.ConsentWriter{consentString, &int8One}, + desc: "gdpr_applies set to true", + inParams: Params{ + Consent: consentString, + GdprApplies: &boolTrue, + }, + expectedWriter: gdpr.ConsentWriter{ + Consent: consentString, + RegExtGDPR: &int8One, + }, }, } for _, tc := range testCases { From a06a2ebb13cae1e45dba87db057f0ebb73f1864a Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:22:18 -0500 Subject: [PATCH 17/69] DSA: Remove bids missing DSA object when required (#3487) --- dsa/validate.go | 51 ++++ dsa/validate_test.go | 178 ++++++++++++ exchange/exchange.go | 23 +- exchange/exchange_test.go | 228 +++++++++------ exchange/exchangetest/dsa-not-required.json | 134 +++++++++ exchange/exchangetest/dsa-required.json | 219 +++++++++++++++ exchange/non_bid_reason.go | 1 + openrtb_ext/regs.go | 8 + openrtb_ext/request_wrapper.go | 48 +++- openrtb_ext/request_wrapper_test.go | 289 ++++++++++++++++++++ 10 files changed, 1080 insertions(+), 99 deletions(-) create mode 100644 dsa/validate.go create mode 100644 dsa/validate_test.go create mode 100644 exchange/exchangetest/dsa-not-required.json create mode 100644 exchange/exchangetest/dsa-required.json diff --git a/dsa/validate.go b/dsa/validate.go new file mode 100644 index 00000000000..f6ece66e132 --- /dev/null +++ b/dsa/validate.go @@ -0,0 +1,51 @@ +package dsa + +import ( + "github.com/prebid/prebid-server/v2/exchange/entities" + "github.com/prebid/prebid-server/v2/openrtb_ext" + + "github.com/buger/jsonparser" +) + +const ( + // Required - bid responses without DSA object will not be accepted + Required = 2 + // RequiredOnlinePlatform - bid responses without DSA object will not be accepted, Publisher is an Online Platform + RequiredOnlinePlatform = 3 +) + +// Validate determines whether a given bid is valid from a DSA perspective. +// A bid is considered valid unless the bid request indicates that a DSA object is required +// in bid responses and the object happens to be missing from the specified bid. +func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) (valid bool) { + if !dsaRequired(req) { + return true + } + if bid == nil || bid.Bid == nil { + return false + } + _, dataType, _, err := jsonparser.Get(bid.Bid.Ext, "dsa") + if dataType == jsonparser.Object && err == nil { + return true + } else if err != nil && err != jsonparser.KeyPathNotFoundError { + return true + } + return false +} + +// dsaRequired examines the bid request to determine if the dsarequired field indicates +// that bid responses include a dsa object +func dsaRequired(req *openrtb_ext.RequestWrapper) bool { + regExt, err := req.GetRegExt() + if regExt == nil || err != nil { + return false + } + regsDSA := regExt.GetDSA() + if regsDSA == nil { + return false + } + if regsDSA.Required == Required || regsDSA.Required == RequiredOnlinePlatform { + return true + } + return false +} diff --git a/dsa/validate_test.go b/dsa/validate_test.go new file mode 100644 index 00000000000..74d428c3e9e --- /dev/null +++ b/dsa/validate_test.go @@ -0,0 +1,178 @@ +package dsa + +import ( + "encoding/json" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/exchange/entities" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + giveRequest *openrtb_ext.RequestWrapper + giveBid *entities.PbsOrtbBid + wantValid bool + }{ + { + name: "not_required", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`), + }, + }, + }, + giveBid: nil, + wantValid: true, + }, + { + name: "required_and_bid_is_nil", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + }, + }, + }, + giveBid: nil, + wantValid: false, + }, + { + name: "required_and_bid.bid_is_nil", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{}, + wantValid: false, + }, + { + name: "required_and_bid.ext.dsa_not_present", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{}`), + }, + }, + wantValid: false, + }, + { + name: "required_and_bid.ext.dsa_present", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa": {}}`), + }, + }, + wantValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + valid := Validate(tt.giveRequest, tt.giveBid) + assert.Equal(t, tt.wantValid, valid) + }) + } +} + +func TestDSARequired(t *testing.T) { + tests := []struct { + name string + giveRequest *openrtb_ext.RequestWrapper + wantRequired bool + }{ + { + name: "not_required_and_reg.ext.dsa_is_nil", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{}`), + }, + }, + }, + wantRequired: false, + }, + { + name: "not_required_and_reg.ext.dsa_is_empty", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {}}`), + }, + }, + }, + wantRequired: false, + }, + { + name: "required_and_reg.ext.dsa_is_0", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`), + }, + }, + }, + wantRequired: false, + }, + { + name: "required_and_reg.ext.dsa_is_1", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 1}}`), + }, + }, + }, + wantRequired: false, + }, + { + name: "required_and_reg.ext.dsa_is_2", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + }, + }, + }, + wantRequired: true, + }, + { + name: "required_and_reg.ext.dsa_is_3", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 3}}`), + }, + }, + }, + wantRequired: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + required := dsaRequired(tt.giveRequest) + assert.Equal(t, tt.wantRequired, required) + }) + } +} diff --git a/exchange/exchange.go b/exchange/exchange.go index 003ce559dc2..ffd3b2e8f4b 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -21,6 +21,7 @@ import ( "github.com/prebid/prebid-server/v2/bidadjustment" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/currency" + "github.com/prebid/prebid-server/v2/dsa" "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/experiment/adscert" @@ -494,7 +495,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog e.bidValidationEnforcement.SetBannerCreativeMaxSize(r.Account.Validations) // Build the response - bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids) + bidResponse := e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs, &seatNonBids) bidResponse = adservertargeting.Apply(r.BidRequestWrapper, r.ResolvedBidRequest, bidResponse, r.QueryParams, bidResponseExt, r.Account.TruncateTargetAttribute) bidResponse.Ext, err = encodeBidResponseExt(bidResponseExt) @@ -890,7 +891,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { } // This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester -func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse { +func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, bidRequest *openrtb_ext.RequestWrapper, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error, seatNonBids *nonBids) *openrtb2.BidResponse { bidResponse := new(openrtb2.BidResponse) bidResponse.ID = bidRequest.ID @@ -905,7 +906,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ for a, adapterSeatBids := range adapterSeatBids { //while processing every single bib, do we need to handle categories here? if adapterSeatBids != nil && len(adapterSeatBids.Bids) > 0 { - sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidResponseExt, pubID, seatNonBids) + sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, pubID, seatNonBids) seatBids = append(seatBids, *sb) bidResponse.Cur = adapterSeatBids.Currency } @@ -1224,14 +1225,14 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*en // Return an openrtb seatBid for a bidder // buildBidResponse is responsible for ensuring nil bid seatbids are not included -func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid { +func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string, seatNonBids *nonBids) *openrtb2.SeatBid { seatBid := &openrtb2.SeatBid{ Seat: adapter.String(), Group: 0, // Prebid cannot support roadblocking } var errList []error - seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidResponseExt, adapter, pubID, seatNonBids) + seatBid.Bid, errList = e.makeBid(adapterBid.Bids, auc, returnCreative, impExtInfoMap, bidRequest, bidResponseExt, adapter, pubID, seatNonBids) if len(errList) > 0 { adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) } @@ -1239,11 +1240,21 @@ func (e *exchange) makeSeatBid(adapterBid *entities.PbsOrtbSeatBid, adapter open return seatBid } -func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) { +func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidRequest *openrtb_ext.RequestWrapper, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string, seatNonBids *nonBids) ([]openrtb2.Bid, []error) { result := make([]openrtb2.Bid, 0, len(bids)) errs := make([]error, 0, 1) for _, bid := range bids { + if !dsa.Validate(bidRequest, bid) { + RequiredError := openrtb_ext.ExtBidderMessage{ + Code: errortypes.BadServerResponseErrorCode, + Message: "bidResponse rejected: DSA object missing when required", + } + bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError) + + seatNonBids.addBid(bid, int(ResponseRejectedPrivacy), adapter.String()) + continue // Don't add bid to result + } if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID, e.bidValidationEnforcement.BannerCreativeMaxSize) { seatNonBids.addBid(bid, int(ResponseRejectedCreativeSizeNotAllowed), adapter.String()) diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 6cd48198267..44baaf8d070 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -144,18 +144,20 @@ func TestCharacterEscape(t *testing.T) { adapterBids["appnexus"] = &entities.PbsOrtbSeatBid{Currency: "USD"} //An openrtb2.BidRequest struct as specified in https://github.com/prebid/prebid-server/issues/465 - bidRequest := &openrtb2.BidRequest{ - ID: "some-request-id", - Imp: []openrtb2.Imp{{ - ID: "some-impression-id", - Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, - Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), - }}, - Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, - Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, - AT: 1, - TMax: 500, - Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`), + bidRequest := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 1}}`), + }}, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 1}}}],"tmax": 500}`), + }, } //adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, @@ -1303,34 +1305,36 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) { }, }, } - bidRequest := &openrtb2.BidRequest{ - ID: "some-request-id", - TMax: 1000, - Imp: []openrtb2.Imp{ - { - ID: "test-div", - Secure: openrtb2.Int8Ptr(0), - Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, - Ext: json.RawMessage(` { - "rubicon": { - "accountId": 1001, - "siteId": 113932, - "zoneId": 535510 - }, - "appnexus": { "placementId": 1 }, - "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" }, - "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 }, - "conversant": { "site_id": "108060" }, - "ix": { "siteId": "287415" } -}`), + bidRequest := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + TMax: 1000, + Imp: []openrtb2.Imp{ + { + ID: "test-div", + Secure: openrtb2.Int8Ptr(0), + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, + Ext: json.RawMessage(` { + "rubicon": { + "accountId": 1001, + "siteId": 113932, + "zoneId": 535510 + }, + "appnexus": { "placementId": 1 }, + "pubmatic": { "publisherId": "156209", "adSlot": "pubmatic_test2@300x250" }, + "pulsepoint": { "cf": "300X250", "cp": 512379, "ct": 486653 }, + "conversant": { "site_id": "108060" }, + "ix": { "siteId": "287415" } + }`), + }, }, + Site: &openrtb2.Site{ + Page: "http://rubitest.com/index.html", + Publisher: &openrtb2.Publisher{ID: "1001"}, + }, + Test: 1, + Ext: json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`), }, - Site: &openrtb2.Site{ - Page: "http://rubitest.com/index.html", - Publisher: &openrtb2.Publisher{ID: "1001"}, - }, - Test: 1, - Ext: json.RawMessage(`{"prebid": { "cache": { "bids": {}, "vastxml": {} }, "targeting": { "pricegranularity": "med", "includewinners": true, "includebidderkeys": false } }}`), } var errList []error @@ -1426,7 +1430,7 @@ func TestBidReturnsCreative(t *testing.T) { //Run tests for _, test := range testCases { - resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, nil, "", "", &nonBids{}) + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, test.inReturnCreative, nil, &openrtb_ext.RequestWrapper{}, nil, "", "", &nonBids{}) assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) assert.Equal(t, test.expectedCreativeMarkup, resultingBids[0].AdM, "%s. Ad markup string doesn't match expected \n", test.description) @@ -1594,18 +1598,20 @@ func TestBidResponseCurrency(t *testing.T) { liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" - bidRequest := &openrtb2.BidRequest{ - ID: "some-request-id", - Imp: []openrtb2.Imp{{ - ID: "some-impression-id", - Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, - Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), - }}, - Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, - Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, - AT: 1, - TMax: 500, - Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`), + bidRequest := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id", + Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), + }}, + Site: &openrtb2.Site{Page: "prebid.org", Ext: json.RawMessage(`{"amp":0}`)}, + Device: &openrtb2.Device{UA: "curl/7.54.0", IP: "::1"}, + AT: 1, + TMax: 500, + Ext: json.RawMessage(`{"id": "some-request-id","site": {"page": "prebid.org"},"imp": [{"id": "some-impression-id","banner": {"format": [{"w": 300,"h": 250},{"w": 300,"h": 600}]},"ext": {"appnexus": {"placementId": 10433394}}}],"tmax": 500}`), + }, } adapterExtra := map[openrtb_ext.BidderName]*seatResponseExtra{ @@ -1740,14 +1746,16 @@ func TestBidResponseImpExtInfo(t *testing.T) { liveAdapters := make([]openrtb_ext.BidderName, 1) liveAdapters[0] = "appnexus" - bidRequest := &openrtb2.BidRequest{ - ID: "some-request-id", - Imp: []openrtb2.Imp{{ - ID: "some-impression-id", - Video: &openrtb2.Video{}, - Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), - }}, - Ext: json.RawMessage(``), + bidRequest := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-request-id", + Imp: []openrtb2.Imp{{ + ID: "some-impression-id", + Video: &openrtb2.Video{}, + Ext: json.RawMessage(`{"appnexus": {"placementId": 10433394}}`), + }}, + Ext: json.RawMessage(``), + }, } var errList []error @@ -4735,17 +4743,42 @@ func TestMakeBidWithValidation(t *testing.T) { sampleAd := "" sampleOpenrtbBid := &openrtb2.Bid{ID: "some-bid-id", AdM: sampleAd} - // Define test cases testCases := []struct { - description string - givenValidations config.Validations - givenBids []*entities.PbsOrtbBid - givenSeat openrtb_ext.BidderName - expectedNumOfBids int - expectedNonBids *nonBids + name string + givenBidRequestExt json.RawMessage + givenValidations config.Validations + givenBids []*entities.PbsOrtbBid + givenSeat openrtb_ext.BidderName + expectedNumOfBids int + expectedNonBids *nonBids + expectedNumDebugErrors int + expectedNumDebugWarnings int }{ { - description: "Validation is enforced, and one bid out of the two is invalid based on dimensions", + name: "One_of_two_bids_is_invalid_based_on_DSA_object_presence", + givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), + givenValidations: config.Validations{}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {}}`)}}, {Bid: &openrtb2.Bid{}}}, + givenSeat: "pubmatic", + expectedNumOfBids: 1, + expectedNonBids: &nonBids{ + seatNonBidsMap: map[string][]openrtb_ext.NonBid{ + "pubmatic": { + { + StatusCode: 305, + Ext: openrtb_ext.NonBidExt{ + Prebid: openrtb_ext.ExtResponseNonBidPrebid{ + Bid: openrtb_ext.NonBidObject{}, + }, + }, + }, + }, + }, + }, + expectedNumDebugWarnings: 1, + }, + { + name: "Creative_size_validation_enforced,_one_of_two_bids_has_invalid_dimensions", givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationEnforce, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", @@ -4767,17 +4800,19 @@ func TestMakeBidWithValidation(t *testing.T) { }, }, }, + expectedNumDebugErrors: 1, }, { - description: "Validation is warned, so no bids should be removed (Validating CreativeMaxSize) ", - givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, - givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, - givenSeat: "pubmatic", - expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + name: "Creative_size_validation_warned,_one_of_two_bids_has_invalid_dimensions", + givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, + givenSeat: "pubmatic", + expectedNumOfBids: 2, + expectedNonBids: &nonBids{}, + expectedNumDebugErrors: 1, }, { - description: "Validation is enforced, and one bid out of the two is invalid based on AdM", + name: "AdM_validation_enforced,_one_of_two_bids_has_invalid_AdM", givenValidations: config.Validations{SecureMarkup: config.ValidationEnforce}, givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", @@ -4792,17 +4827,19 @@ func TestMakeBidWithValidation(t *testing.T) { }, }, }, + expectedNumDebugErrors: 1, }, { - description: "Validation is warned so no bids should be removed (Validating SecureMarkup)", - givenValidations: config.Validations{SecureMarkup: config.ValidationWarn}, - givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, - givenSeat: "pubmatic", - expectedNumOfBids: 2, - expectedNonBids: &nonBids{}, + name: "AdM_validation_warned,_one_of_two_bids_has_invalid_AdM", + givenValidations: config.Validations{SecureMarkup: config.ValidationWarn}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid", ImpID: "1"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid", ImpID: "2"}, BidType: openrtb_ext.BidTypeBanner}}, + givenSeat: "pubmatic", + expectedNumOfBids: 2, + expectedNonBids: &nonBids{}, + expectedNumDebugErrors: 1, }, { - description: "Adm validation is skipped, creative size validation is enforced, one Adm is invalid, but because we skip, no bids should be removed", + name: "Adm_validation_skipped,_creative_size_validation_enforced,_one_of_two_bids_has_invalid_AdM", givenValidations: config.Validations{SecureMarkup: config.ValidationSkip, BannerCreativeMaxSize: config.ValidationEnforce}, givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{AdM: "http://domain.com/invalid"}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{AdM: "https://domain.com/valid"}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", @@ -4810,8 +4847,8 @@ func TestMakeBidWithValidation(t *testing.T) { expectedNonBids: &nonBids{}, }, { - description: "Creative Size Validation is skipped, Adm Validation is enforced, one Creative Size is invalid, but because we skip, no bids should be removed", - givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationWarn, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, + name: "Creative_size_validation_skipped,_Adm_Validation_enforced,_one_of_two_bids_has_invalid_dimensions", + givenValidations: config.Validations{BannerCreativeMaxSize: config.ValidationSkip, MaxCreativeWidth: 100, MaxCreativeHeight: 100}, givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{W: 200, H: 200}, BidType: openrtb_ext.BidTypeBanner}, {Bid: &openrtb2.Bid{W: 50, H: 50}, BidType: openrtb_ext.BidTypeBanner}}, givenSeat: "pubmatic", expectedNumOfBids: 2, @@ -4844,23 +4881,34 @@ func TestMakeBidWithValidation(t *testing.T) { e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) - bidExtResponse := &openrtb_ext.ExtBidResponse{Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage)} - ImpExtInfoMap := make(map[string]ImpExtInfo) ImpExtInfoMap["1"] = ImpExtInfo{} ImpExtInfoMap["2"] = ImpExtInfo{} //Run tests for _, test := range testCases { - t.Run(test.description, func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { + bidExtResponse := &openrtb_ext.ExtBidResponse{ + Errors: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + Warnings: make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage), + } + bidRequest := &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: test.givenBidRequestExt, + }, + }, + } e.bidValidationEnforcement = test.givenValidations sampleBids := test.givenBids nonBids := &nonBids{} - resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidExtResponse, test.givenSeat, "", nonBids) + resultingBids, resultingErrs := e.makeBid(sampleBids, sampleAuction, true, ImpExtInfoMap, bidRequest, bidExtResponse, test.givenSeat, "", nonBids) - assert.Equal(t, 0, len(resultingErrs), "%s. Test should not return errors \n", test.description) - assert.Equal(t, test.expectedNumOfBids, len(resultingBids), "%s. Test returns more valid bids than expected\n", test.description) - assert.Equal(t, test.expectedNonBids, nonBids, "%s. Test returns incorrect nonBids\n", test.description) + assert.Equal(t, 0, len(resultingErrs)) + assert.Equal(t, test.expectedNumOfBids, len(resultingBids)) + assert.Equal(t, test.expectedNonBids, nonBids) + assert.Equal(t, test.expectedNumDebugErrors, len(bidExtResponse.Errors)) + assert.Equal(t, test.expectedNumDebugWarnings, len(bidExtResponse.Warnings)) }) } } diff --git a/exchange/exchangetest/dsa-not-required.json b/exchange/exchangetest/dsa-not-required.json new file mode 100644 index 00000000000..f2a1f821e2f --- /dev/null +++ b/exchange/exchangetest/dsa-not-required.json @@ -0,0 +1,134 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 1, + "pubrender": 0, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "mockResponse": { + "pbsSeatBids": [{ + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3 + } + }, + "bidType": "video" + }], + "seat": "appnexus" + }] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video" + } + } + }] + }] + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/dsa-required.json b/exchange/exchangetest/dsa-required.json new file mode 100644 index 00000000000..d25efdc192e --- /dev/null +++ b/exchange/exchangetest/dsa-required.json @@ -0,0 +1,219 @@ +{ + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + }, + "audienceNetwork": { + "placementId": "some-placement" + } + } + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 2, + "pubrender": 0, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "outgoingRequests": { + "appnexus": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": 1 + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 2, + "pubrender": 0, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "mockResponse": { + "pbsSeatBids": [{ + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": [{ + "domain": "dsp1domain.com", + "dsaparams": [1, 2] + }], + "adrender": 1 + } + } + }, + "bidType": "video" + }], + "seat": "appnexus" + }] + } + }, + "audienceNetwork": { + "expectRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [{ + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "placementId": "some-placement" + } + } + }], + "regs": { + "ext": { + "dsa": { + "dsarequired": 2, + "pubrender": 0, + "datatopub": 2, + "transparency": [{ + "domain": "platform1domain.com", + "dsaparams": [1] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [1, 2] + } + ] + } + } + } + } + }, + "mockResponse": { + "pbsSeatBids": [{ + "pbsBids": [{ + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3 + } + }, + "bidType": "video" + }], + "seat": "audienceNetwork" + }] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [{ + "seat": "appnexus", + "bid": [{ + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "ext": { + "someField": "someValue", + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video" + }, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": [{ + "domain": "dsp1domain.com", + "dsaparams": [1, 2] + }], + "adrender": 1 + } + } + }] + },{ + "seat": "audienceNetwork", + "bid": [] + }] + } + } +} \ No newline at end of file diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go index 4d817a6709e..f105d0adc9d 100644 --- a/exchange/non_bid_reason.go +++ b/exchange/non_bid_reason.go @@ -8,6 +8,7 @@ type NonBidReason int const ( NoBidUnknownError NonBidReason = 0 // No Bid - General ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid + ResponseRejectedPrivacy NonBidReason = 305 ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed) ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) ) diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go index 56a5179d051..8057ce2ccd7 100644 --- a/openrtb_ext/regs.go +++ b/openrtb_ext/regs.go @@ -2,6 +2,8 @@ package openrtb_ext // ExtRegs defines the contract for bidrequest.regs.ext type ExtRegs struct { + // DSA is an object containing DSA transparency information, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + DSA *ExtRegsDSA `json:"dsa,omitempty"` // GDPR should be "1" if the caller believes the user is subject to GDPR laws, "0" if not, and undefined // if it's unknown. For more info on this parameter, see: https://iabtechlab.com/wp-content/uploads/2018/02/OpenRTB_Advisory_GDPR_2018-02.pdf @@ -10,3 +12,9 @@ type ExtRegs struct { // USPrivacy should be a four character string, see: https://iabtechlab.com/wp-content/uploads/2019/11/OpenRTB-Extension-U.S.-Privacy-IAB-Tech-Lab.pdf USPrivacy string `json:"us_privacy,omitempty"` } + +// ExtRegsDSA defines the contract for bidrequest.regs.ext.dsa +type ExtRegsDSA struct { + // Required should be a between 0 and 3 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + Required int8 `json:"dsarequired,omitempty"` +} diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index d8fe02eadfa..d7ff5acc021 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -53,6 +53,7 @@ const ( consentedProvidersSettingsListKey = "consented_providers_settings" consentKey = "consent" ampKey = "amp" + dsaKey = "dsa" eidsKey = "eids" gdprKey = "gdpr" prebidKey = "prebid" @@ -1165,6 +1166,8 @@ func (de *DOOHExt) Clone() *DOOHExt { type RegExt struct { ext map[string]json.RawMessage extDirty bool + dsa *ExtRegsDSA + dsaDirty bool gdpr *int8 gdprDirty bool usPrivacy string @@ -1186,6 +1189,16 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error { return err } + dsaJson, hasDSA := re.ext[dsaKey] + if hasDSA { + re.dsa = &ExtRegsDSA{} + } + if dsaJson != nil { + if err := jsonutil.Unmarshal(dsaJson, re.dsa); err != nil { + return err + } + } + gdprJson, hasGDPR := re.ext[gdprKey] if hasGDPR && gdprJson != nil { if err := jsonutil.Unmarshal(gdprJson, &re.gdpr); err != nil { @@ -1204,6 +1217,19 @@ func (re *RegExt) unmarshal(extJson json.RawMessage) error { } func (re *RegExt) marshal() (json.RawMessage, error) { + if re.dsaDirty { + if re.dsa != nil { + rawjson, err := jsonutil.Marshal(re.dsa) + if err != nil { + return nil, err + } + re.ext[dsaKey] = rawjson + } else { + delete(re.ext, dsaKey) + } + re.dsaDirty = false + } + if re.gdprDirty { if re.gdpr != nil { rawjson, err := jsonutil.Marshal(re.gdpr) @@ -1238,7 +1264,7 @@ func (re *RegExt) marshal() (json.RawMessage, error) { } func (re *RegExt) Dirty() bool { - return re.extDirty || re.gdprDirty || re.usPrivacyDirty + return re.extDirty || re.dsaDirty || re.gdprDirty || re.usPrivacyDirty } func (re *RegExt) GetExt() map[string]json.RawMessage { @@ -1254,9 +1280,25 @@ func (re *RegExt) SetExt(ext map[string]json.RawMessage) { re.extDirty = true } +func (re *RegExt) GetDSA() *ExtRegsDSA { + if re.dsa == nil { + return nil + } + dsa := *re.dsa + return &dsa +} + +func (re *RegExt) SetDSA(dsa *ExtRegsDSA) { + re.dsa = dsa + re.dsaDirty = true +} + func (re *RegExt) GetGDPR() *int8 { - gdpr := re.gdpr - return gdpr + if re.gdpr == nil { + return nil + } + gdpr := *re.gdpr + return &gdpr } func (re *RegExt) SetGDPR(gdpr *int8) { diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index 6c318e37eb7..f04a51a4bdc 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2049,3 +2049,292 @@ func TestCloneImpExt(t *testing.T) { }) } } + +func TestRebuildRegExt(t *testing.T) { + strA := "a" + strB := "b" + + tests := []struct { + name string + request openrtb2.BidRequest + regExt RegExt + expectedRequest openrtb2.BidRequest + }{ + { + name: "req_regs_nil_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{}, + }, + { + name: "req_regs_nil_-_dirty_and_different_-_change", + request: openrtb2.BidRequest{}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + expectedRequest: openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), + }, + }, + }, + { + name: "req_regs_ext_nil_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, + { + name: "req_regs_ext_nil_-_dirty_and_different_-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + expectedRequest: openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), + }, + }, + }, + { + name: "req_regs_dsa_populated_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, + }, + { + name: "req_regs_dsa_populated_-_dirty_and_different-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: 2}, dsaDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":2}}`)}}, + }, + { + name: "req_regs_dsa_populated_-_dirty_and_same_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, + }, + { + name: "req_regs_dsa_populated_-_dirty_and_nil_-_cleared", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{}`)}}, + regExt: RegExt{dsa: nil, dsaDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, + { + name: "req_regs_gdpr_populated_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}}, + }, + { + name: "req_regs_gdpr_populated_-_dirty_and_different-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}}, + regExt: RegExt{gdpr: ptrutil.ToPtr[int8](0), gdprDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":0}`)}}, + }, + { + name: "req_regs_gdpr_populated_-_dirty_and_same_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}}, + regExt: RegExt{gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"gdpr":1}`)}}, + }, + { + name: "req_regs_gdpr_populated_-_dirty_and_nil_-_cleared", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{}`)}}, + regExt: RegExt{gdpr: nil, gdprDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, + { + name: "req_regs_usprivacy_populated_-_not_dirty_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + regExt: RegExt{}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + }, + { + name: "req_regs_usprivacy_populated_-_dirty_and_different-_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + regExt: RegExt{usPrivacy: strB, usPrivacyDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"b"}`)}}, + }, + { + name: "req_regs_usprivacy_populated_-_dirty_and_same_-_no_change", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + regExt: RegExt{usPrivacy: strA, usPrivacyDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + }, + { + name: "req_regs_usprivacy_populated_-_dirty_and_nil_-_cleared", + request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"a"}`)}}, + regExt: RegExt{usPrivacy: "", usPrivacyDirty: true}, + expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.regExt.ext = make(map[string]json.RawMessage) + + w := RequestWrapper{BidRequest: &tt.request, regExt: &tt.regExt} + w.RebuildRequest() + assert.Equal(t, tt.expectedRequest, *w.BidRequest) + }) + } +} + +func TestRegExtUnmarshal(t *testing.T) { + tests := []struct { + name string + regExt *RegExt + extJson json.RawMessage + expectDSA *ExtRegsDSA + expectGDPR *int8 + expectUSPrivacy string + expectError bool + }{ + { + name: "RegExt.ext_not_empty_and_not_dirtyr", + regExt: &RegExt{ + ext: map[string]json.RawMessage{"dsa": json.RawMessage(`{}`)}, + }, + extJson: json.RawMessage{}, + expectError: false, + }, + { + name: "RegExt.ext_empty_and_dirty", + regExt: &RegExt{extDirty: true}, + extJson: json.RawMessage(`{"dsa":{"dsarequired":1}}`), + expectError: false, + }, + { + name: "nothing_to_unmarshal", + regExt: &RegExt{ + ext: map[string]json.RawMessage{}, + }, + extJson: json.RawMessage{}, + expectError: false, + }, + // DSA + { + name: "valid_dsa_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"dsa":{"dsarequired":1}}`), + expectDSA: &ExtRegsDSA{ + Required: 1, + }, + expectError: false, + }, + { + name: "malformed_dsa_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"dsa":{"dsarequired":""}}`), + expectDSA: &ExtRegsDSA{ + Required: 0, + }, + expectError: true, + }, + // GDPR + { + name: "valid_gdpr_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gdpr":1}`), + expectGDPR: ptrutil.ToPtr[int8](1), + expectError: false, + }, + { + name: "malformed_gdpr_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"gdpr":""}`), + expectGDPR: ptrutil.ToPtr[int8](0), + expectError: true, + }, + // us_privacy + { + name: "valid_usprivacy_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"us_privacy":"consent"}`), + expectUSPrivacy: "consent", + expectError: false, + }, + { + name: "malformed_usprivacy_json", + regExt: &RegExt{}, + extJson: json.RawMessage(`{"us_privacy":1}`), + expectError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.regExt.unmarshal(tt.extJson) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.expectDSA, tt.regExt.dsa) + assert.Equal(t, tt.expectGDPR, tt.regExt.gdpr) + assert.Equal(t, tt.expectUSPrivacy, tt.regExt.usPrivacy) + }) + } +} + +func TestRegExtGetExtSetExt(t *testing.T) { + regExt := &RegExt{} + regExtJSON := regExt.GetExt() + assert.Equal(t, regExtJSON, map[string]json.RawMessage{}) + assert.False(t, regExt.Dirty()) + + rawJSON := map[string]json.RawMessage{ + "dsa": json.RawMessage(`{}`), + "gdpr": json.RawMessage(`1`), + "usprivacy": json.RawMessage(`"consent"`), + } + regExt.SetExt(rawJSON) + assert.True(t, regExt.Dirty()) + + regExtJSON = regExt.GetExt() + assert.Equal(t, regExtJSON, rawJSON) + assert.NotSame(t, regExtJSON, rawJSON) +} + +func TestRegExtGetDSASetDSA(t *testing.T) { + regExt := &RegExt{} + regExtDSA := regExt.GetDSA() + assert.Nil(t, regExtDSA) + assert.False(t, regExt.Dirty()) + + dsa := &ExtRegsDSA{ + Required: 2, + } + regExt.SetDSA(dsa) + assert.True(t, regExt.Dirty()) + + regExtDSA = regExt.GetDSA() + assert.Equal(t, regExtDSA, dsa) + assert.NotSame(t, regExtDSA, dsa) +} + +func TestRegExtGetUSPrivacySetUSPrivacy(t *testing.T) { + regExt := &RegExt{} + regExtUSPrivacy := regExt.GetUSPrivacy() + assert.Equal(t, regExtUSPrivacy, "") + assert.False(t, regExt.Dirty()) + + usprivacy := "consent" + regExt.SetUSPrivacy(usprivacy) + assert.True(t, regExt.Dirty()) + + regExtUSPrivacy = regExt.GetUSPrivacy() + assert.Equal(t, regExtUSPrivacy, usprivacy) + assert.NotSame(t, regExtUSPrivacy, usprivacy) +} + +func TestRegExtGetGDPRSetGDPR(t *testing.T) { + regExt := &RegExt{} + regExtGDPR := regExt.GetGDPR() + assert.Nil(t, regExtGDPR) + assert.False(t, regExt.Dirty()) + + gdpr := ptrutil.ToPtr[int8](1) + regExt.SetGDPR(gdpr) + assert.True(t, regExt.Dirty()) + + regExtGDPR = regExt.GetGDPR() + assert.Equal(t, regExtGDPR, gdpr) + assert.NotSame(t, regExtGDPR, gdpr) +} From 21bf61171c2fa6529ac2a326b5c0043fcdde313b Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Wed, 21 Feb 2024 05:54:33 +0100 Subject: [PATCH 18/69] Fix go vet 'composite literals with unkeyed fields' (#3522) --- adapters/adoppler/adoppler.go | 20 ++++++++++---------- adapters/adquery/adquery.go | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/adapters/adoppler/adoppler.go b/adapters/adoppler/adoppler.go index 07cfce53c74..1af0b72017d 100644 --- a/adapters/adoppler/adoppler.go +++ b/adapters/adoppler/adoppler.go @@ -65,7 +65,7 @@ func (ads *AdopplerAdapter) MakeRequests( for _, imp := range req.Imp { ext, err := unmarshalExt(imp.Ext) if err != nil { - errs = append(errs, &errortypes.BadInput{err.Error()}) + errs = append(errs, &errortypes.BadInput{Message: err.Error()}) continue } @@ -83,7 +83,7 @@ func (ads *AdopplerAdapter) MakeRequests( if err != nil { e := fmt.Sprintf("Unable to build bid URI: %s", err.Error()) - errs = append(errs, &errortypes.BadInput{e}) + errs = append(errs, &errortypes.BadInput{Message: e}) continue } data := &adapters.RequestData{ @@ -110,11 +110,11 @@ func (ads *AdopplerAdapter) MakeBids( return nil, nil } if resp.StatusCode == http.StatusBadRequest { - return nil, []error{&errortypes.BadInput{"bad request"}} + return nil, []error{&errortypes.BadInput{Message: "bad request"}} } if resp.StatusCode != http.StatusOK { err := &errortypes.BadServerResponse{ - fmt.Sprintf("unexpected status: %d", resp.StatusCode), + Message: fmt.Sprintf("unexpected status: %d", resp.StatusCode), } return nil, []error{err} } @@ -123,7 +123,7 @@ func (ads *AdopplerAdapter) MakeBids( err := json.Unmarshal(resp.Body, &bidResp) if err != nil { err := &errortypes.BadServerResponse{ - fmt.Sprintf("invalid body: %s", err.Error()), + Message: fmt.Sprintf("invalid body: %s", err.Error()), } return nil, []error{err} } @@ -132,7 +132,7 @@ func (ads *AdopplerAdapter) MakeBids( for _, imp := range intReq.Imp { if _, ok := impTypes[imp.ID]; ok { return nil, []error{&errortypes.BadInput{ - fmt.Sprintf("duplicate $.imp.id %s", imp.ID), + Message: fmt.Sprintf("duplicate $.imp.id %s", imp.ID), }} } if imp.Banner != nil { @@ -145,7 +145,7 @@ func (ads *AdopplerAdapter) MakeBids( impTypes[imp.ID] = openrtb_ext.BidTypeNative } else { return nil, []error{&errortypes.BadInput{ - "one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required", + Message: "one of $.imp.banner, $.imp.video, $.imp.audio and $.imp.native field required", }} } } @@ -156,7 +156,7 @@ func (ads *AdopplerAdapter) MakeBids( tp, ok := impTypes[bid.ImpID] if !ok { err := &errortypes.BadServerResponse{ - fmt.Sprintf("unknown impid: %s", bid.ImpID), + Message: fmt.Sprintf("unknown impid: %s", bid.ImpID), } return nil, []error{err} } @@ -165,11 +165,11 @@ func (ads *AdopplerAdapter) MakeBids( if tp == openrtb_ext.BidTypeVideo { adsExt, err := unmarshalAdsExt(bid.Ext) if err != nil { - return nil, []error{&errortypes.BadServerResponse{err.Error()}} + return nil, []error{&errortypes.BadServerResponse{Message: err.Error()}} } if adsExt == nil || adsExt.Video == nil { return nil, []error{&errortypes.BadServerResponse{ - "$.seatbid.bid.ext.ads.video required", + Message: "$.seatbid.bid.ext.ads.video required", }} } bidVideo = &openrtb_ext.ExtBidPrebidVideo{ diff --git a/adapters/adquery/adquery.go b/adapters/adquery/adquery.go index 4da3f7a459e..e14c52f183d 100644 --- a/adapters/adquery/adquery.go +++ b/adapters/adquery/adquery.go @@ -41,7 +41,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRe for _, imp := range request.Imp { ext, err := parseExt(imp.Ext) if err != nil { - errs = append(errs, &errortypes.BadInput{err.Error()}) + errs = append(errs, &errortypes.BadInput{Message: err.Error()}) continue } From 18e9b5071dbb011d84ade3662d834d8ddd6f8122 Mon Sep 17 00:00:00 2001 From: ccorbo Date: Wed, 21 Feb 2024 01:32:26 -0500 Subject: [PATCH 19/69] feat: add dsa test [PB-2423] (#3510) Co-authored-by: Chris Corbo --- .../ix/ixtest/supplemental/dsa-request.json | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 adapters/ix/ixtest/supplemental/dsa-request.json diff --git a/adapters/ix/ixtest/supplemental/dsa-request.json b/adapters/ix/ixtest/supplemental/dsa-request.json new file mode 100644 index 00000000000..a51f29278af --- /dev/null +++ b/adapters/ix/ixtest/supplemental/dsa-request.json @@ -0,0 +1,193 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + }, + "site": { + "page": "https://www.example.com/" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://host/endpoint", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + } + ], + "w": 300, + "h": 250 + }, + "ext": { + "bidder": { + "siteId": "569749" + } + } + } + ], + "site": { + "page": "https://www.example.com/", + "publisher": { + "id": "569749" + } + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "platform1domain.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "SSP2domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "ext": { + "ix": {}, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "params": [ + 1, + 2 + ] + }, + "adrender": 1 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "https://advertiser.example.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "ext": { + "ix": {}, + "dsa": { + "behalf": "Advertiser", + "paid": "Advertiser", + "transparency": { + "domain": "dsp1domain.com", + "params": [ + 1, + 2 + ] + }, + "adrender": 1 + } + } + }, + "type": "banner" + } + ] + } + ] + } \ No newline at end of file From 5900d36c7769b7d341648306b76ea6bbd7fad5b3 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:09:24 -0500 Subject: [PATCH 20/69] DSA: Use seat nonbid code 300 (#3531) --- errortypes/code.go | 1 + exchange/exchange.go | 6 +++--- exchange/exchange_test.go | 2 +- exchange/non_bid_reason.go | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/errortypes/code.go b/errortypes/code.go index 1de5e648cef..399dd663498 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -31,6 +31,7 @@ const ( AdServerTargetingWarningCode BidAdjustmentWarningCode FloorBidRejectionWarningCode + InvalidBidResponseDSAWarningCode ) // Coder provides an error or warning code with severity. diff --git a/exchange/exchange.go b/exchange/exchange.go index ffd3b2e8f4b..b9dae6725c6 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -1247,12 +1247,12 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea for _, bid := range bids { if !dsa.Validate(bidRequest, bid) { RequiredError := openrtb_ext.ExtBidderMessage{ - Code: errortypes.BadServerResponseErrorCode, - Message: "bidResponse rejected: DSA object missing when required", + Code: errortypes.InvalidBidResponseDSAWarningCode, + Message: "bid response rejected: DSA object missing when required", } bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError) - seatNonBids.addBid(bid, int(ResponseRejectedPrivacy), adapter.String()) + seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) continue // Don't add bid to result } if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.BidType == openrtb_ext.BidTypeBanner { diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 44baaf8d070..e484e21a42e 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -4765,7 +4765,7 @@ func TestMakeBidWithValidation(t *testing.T) { seatNonBidsMap: map[string][]openrtb_ext.NonBid{ "pubmatic": { { - StatusCode: 305, + StatusCode: 300, Ext: openrtb_ext.NonBidExt{ Prebid: openrtb_ext.ExtResponseNonBidPrebid{ Bid: openrtb_ext.NonBidObject{}, diff --git a/exchange/non_bid_reason.go b/exchange/non_bid_reason.go index f105d0adc9d..2dff2dfbcbf 100644 --- a/exchange/non_bid_reason.go +++ b/exchange/non_bid_reason.go @@ -6,9 +6,9 @@ package exchange type NonBidReason int const ( - NoBidUnknownError NonBidReason = 0 // No Bid - General + NoBidUnknownError NonBidReason = 0 // No Bid - General + ResponseRejectedGeneral NonBidReason = 300 ResponseRejectedCategoryMappingInvalid NonBidReason = 303 // Response Rejected - Category Mapping Invalid - ResponseRejectedPrivacy NonBidReason = 305 ResponseRejectedCreativeSizeNotAllowed NonBidReason = 351 // Response Rejected - Invalid Creative (Size Not Allowed) ResponseRejectedCreativeNotSecure NonBidReason = 352 // Response Rejected - Invalid Creative (Not Secure) ) From 33b466ec1dd9ed9a70220a13e81c42605c61d773 Mon Sep 17 00:00:00 2001 From: Philip Watson Date: Thu, 22 Feb 2024 18:50:09 +1300 Subject: [PATCH 21/69] Stroeercore: support DSA (#3495) --- adapters/stroeerCore/stroeercore.go | 32 ++- .../stroeercoretest/exemplary/dsa.json | 202 ++++++++++++++++++ 2 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 adapters/stroeerCore/stroeercoretest/exemplary/dsa.json diff --git a/adapters/stroeerCore/stroeercore.go b/adapters/stroeerCore/stroeercore.go index 733d549e907..f2ef4923d7b 100644 --- a/adapters/stroeerCore/stroeercore.go +++ b/adapters/stroeerCore/stroeercore.go @@ -23,17 +23,22 @@ type response struct { } type bidResponse struct { - ID string `json:"id"` - BidID string `json:"bidId"` - CPM float64 `json:"cpm"` - Width int64 `json:"width"` - Height int64 `json:"height"` - Ad string `json:"ad"` - CrID string `json:"crid"` - Mtype string `json:"mtype"` + ID string `json:"id"` + BidID string `json:"bidId"` + CPM float64 `json:"cpm"` + Width int64 `json:"width"` + Height int64 `json:"height"` + Ad string `json:"ad"` + CrID string `json:"crid"` + Mtype string `json:"mtype"` + DSA json.RawMessage `json:"dsa"` } -func (b bidResponse) resolveMediaType() (mt openrtb2.MarkupType, bt openrtb_ext.BidType, err error) { +type bidExt struct { + DSA json.RawMessage `json:"dsa,omitempty"` +} + +func (b *bidResponse) resolveMediaType() (mt openrtb2.MarkupType, bt openrtb_ext.BidType, err error) { switch b.Mtype { case "banner": return openrtb2.MarkupBanner, openrtb_ext.BidTypeBanner, nil @@ -82,6 +87,15 @@ func (a *adapter) MakeBids(bidRequest *openrtb2.BidRequest, requestData *adapter MType: markupType, } + if bid.DSA != nil { + dsaJson, err := json.Marshal(bidExt{bid.DSA}) + if err != nil { + errors = append(errors, err) + } else { + openRtbBid.Ext = dsaJson + } + } + bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ Bid: &openRtbBid, BidType: bidType, diff --git a/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json b/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json new file mode 100644 index 00000000000..70baef5fc4b --- /dev/null +++ b/adapters/stroeerCore/stroeercoretest/exemplary/dsa.json @@ -0,0 +1,202 @@ +{ + "mockBidRequest": { + "id": "auction-id", + "cur": [ + "EUR" + ], + "imp": [ + { + "id": "1", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "sid": "123456" + } + } + } + ], + "device": { + "ip": "123.123.123.123" + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path" + }, + "user": { + "buyeruid": "test-buyer-user-id" + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "example-platform.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "example-ssp.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + } + }, + "httpCalls": [ + { + "expectedRequest": { + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "uri": "http://localhost/s2sdsh", + "body": { + "id": "auction-id", + "cur": [ + "EUR" + ], + "imp": [ + { + "id": "1", + "tagid": "123456", + "banner": { + "format": [ + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "sid": "123456" + } + } + } + ], + "device": { + "ip": "123.123.123.123" + }, + "site": { + "domain": "www.publisher.com", + "page": "http://www.publisher.com/some/path" + }, + "user": { + "buyeruid": "test-buyer-user-id" + }, + "regs": { + "ext": { + "dsa": { + "dsarequired": 3, + "pubrender": 0, + "datatopub": 2, + "transparency": [ + { + "domain": "example-platform.com", + "dsaparams": [ + 1 + ] + }, + { + "domain": "example-ssp.com", + "dsaparams": [ + 1, + 2 + ] + } + ] + } + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "bids": [ + { + "id": "1829273982920-01", + "bidId": "1", + "cpm": 2, + "width": 300, + "height": 600, + "ad": "advert", + "crid": "wasd", + "mtype": "banner", + "dsa": { + "behalf": "Advertiser A", + "paid": "Advertiser B", + "transparency": [ + { + "domain": "example-domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ], + "adrender": 1 + } + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "1829273982920-01", + "impid": "1", + "price": 2, + "adm": "advert", + "w": 300, + "h": 600, + "crid": "wasd", + "mtype": 1, + "ext": { + "dsa": { + "behalf": "Advertiser A", + "paid": "Advertiser B", + "transparency": [ + { + "domain": "example-domain.com", + "dsaparams": [ + 1, + 2 + ] + } + ], + "adrender": 1 + } + } + }, + "type": "banner" + } + ] + } + ] +} From 57783356f5f99d3e12478c350da396b56b636a47 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 22 Feb 2024 00:51:22 -0500 Subject: [PATCH 22/69] Update BMTM maintainer address (#3483) --- static/bidder-info/bmtm.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/bmtm.yaml b/static/bidder-info/bmtm.yaml index 3d3f2ca6e22..27452195479 100644 --- a/static/bidder-info/bmtm.yaml +++ b/static/bidder-info/bmtm.yaml @@ -1,6 +1,6 @@ endpoint: "https://one.elitebidder.com/api/pbs" maintainer: - email: dev@brightmountainmedia.com + email: product@brightmountainmedia.com modifyingVastXmlAllowed: false capabilities: app: From 5ae5e05ee6f56eea9e603c1d8e1bc69c099351d0 Mon Sep 17 00:00:00 2001 From: bretg Date: Thu, 22 Feb 2024 00:59:30 -0500 Subject: [PATCH 23/69] adf regional endpoints (#3503) --- static/bidder-info/adf.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/static/bidder-info/adf.yaml b/static/bidder-info/adf.yaml index 2310f346c25..d7d9eccf986 100644 --- a/static/bidder-info/adf.yaml +++ b/static/bidder-info/adf.yaml @@ -1,4 +1,12 @@ +# Please uncomment the appropriate endpoint URL for your datacenter +# Europe endpoint: "https://adx.adform.net/adx/openrtb" +# North/South America +# endpoint: "https://adx2.adform.net/adx/openrtb" +# APAC +# endpoint: "https://adx3.adform.net/adx/openrtb" +geoscope: + - global maintainer: email: "scope.sspp@adform.com" gvlVendorID: 50 From 24a23d3202f26cbf68875578ffa16123775db12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:59:47 +0400 Subject: [PATCH 24/69] Rise: Add GPP macros (#3496) --- static/bidder-info/rise.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/rise.yaml b/static/bidder-info/rise.yaml index 72e56c334c8..79b64d5a4e7 100644 --- a/static/bidder-info/rise.yaml +++ b/static/bidder-info/rise.yaml @@ -14,5 +14,5 @@ capabilities: - video userSync: iframe: - url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: https://pbs-cs.yellowblue.io/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}} userMacro: "[PBS_UID]" From c9131abdbcb18aa620fd80b0978340cb1ddb897e Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Thu, 22 Feb 2024 21:11:21 +0100 Subject: [PATCH 25/69] Add formatcheck Make target (#3480) Signed-off-by: Dmitry S --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index 2d8aae6c78a..cf4ac52b515 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,7 @@ image: # format runs format format: ./scripts/format.sh -f true + +# formatcheck runs format for diagnostics, without modifying the code +formatcheck: + ./scripts/format.sh -f false From b945d09c608ea27cf7bd94e337a1dc775b1cde3a Mon Sep 17 00:00:00 2001 From: Hendrick Musche <107099114+sag-henmus@users.noreply.github.com> Date: Fri, 23 Feb 2024 02:00:35 +0100 Subject: [PATCH 26/69] SeedingAlliance: Deprecate seatId in favor of accountId (#3486) --- adapters/seedingAlliance/seedingAlliance.go | 16 +- .../seedingAlliance/seedingAlliance_test.go | 25 +-- .../exemplary/banner_with_account.json | 142 +++++++++++++++++ .../banner_with_account_and_seat.json | 144 ++++++++++++++++++ openrtb_ext/imp_seedingAlliance.go | 5 +- static/bidder-params/seedingAlliance.json | 6 +- 6 files changed, 318 insertions(+), 20 deletions(-) create mode 100644 adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json create mode 100644 adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json diff --git a/adapters/seedingAlliance/seedingAlliance.go b/adapters/seedingAlliance/seedingAlliance.go index 03b6853b975..af48133d7c2 100644 --- a/adapters/seedingAlliance/seedingAlliance.go +++ b/adapters/seedingAlliance/seedingAlliance.go @@ -33,11 +33,11 @@ func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Serv } func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { - var seatId string + var accountId string var err error for i := range request.Imp { - if seatId, err = getExtInfo(&request.Imp[i]); err != nil { + if accountId, err = getExtInfo(&request.Imp[i]); err != nil { return nil, []error{err} } } @@ -51,7 +51,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, extraRequestInfo *a return nil, []error{err} } - url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: seatId}) + url, err := macros.ResolveMacros(a.endpoint, macros.EndpointTemplateParams{AccountID: accountId}) if err != nil { return nil, []error{err} } @@ -146,7 +146,7 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) { var ext adapters.ExtImpBidder var extSA openrtb_ext.ImpExtSeedingAlliance - seatID := "pbs" + accountId := "pbs" if err := json.Unmarshal(imp.Ext, &ext); err != nil { return "", fmt.Errorf("could not unmarshal adapters.ExtImpBidder: %w", err) @@ -159,8 +159,12 @@ func getExtInfo(imp *openrtb2.Imp) (string, error) { imp.TagID = extSA.AdUnitID if extSA.SeatID != "" { - seatID = extSA.SeatID + accountId = extSA.SeatID } - return seatID, nil + if extSA.AccountID != "" { + accountId = extSA.AccountID + } + + return accountId, nil } diff --git a/adapters/seedingAlliance/seedingAlliance_test.go b/adapters/seedingAlliance/seedingAlliance_test.go index 6d9460b875e..ae9b888e4b1 100644 --- a/adapters/seedingAlliance/seedingAlliance_test.go +++ b/adapters/seedingAlliance/seedingAlliance_test.go @@ -136,24 +136,27 @@ func TestGetMediaTypeForBid(t *testing.T) { func TestGetExtInfo(t *testing.T) { type args struct { - adUnitId string - seatId string + adUnitId string + seatId string + accountId string } tests := []struct { - name string - expectedAdUnitID string - expectedSeatID string - data args - wantErr bool + name string + expectedAdUnitID string + expectedAccountId string + data args + wantErr bool }{ {"regular case", "abc123", "pbs", args{adUnitId: "abc123"}, false}, {"nil case", "", "pbs", args{adUnitId: ""}, false}, {"unmarshal err case", "", "pbs", args{adUnitId: ""}, true}, {"seatId case", "abc123", "seat1", args{adUnitId: "abc123", seatId: "seat1"}, false}, + {"accountId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1"}, false}, + {"accountId and seatId case", "abc123", "account1", args{adUnitId: "abc123", accountId: "account1", seatId: "seat1"}, false}, } for _, test := range tests { - extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId}) + extSA, err := json.Marshal(openrtb_ext.ImpExtSeedingAlliance{AdUnitID: test.data.adUnitId, SeatID: test.data.seatId, AccountID: test.data.accountId}) if err != nil { t.Fatalf("unexpected error %v", err) } @@ -168,7 +171,7 @@ func TestGetExtInfo(t *testing.T) { } ortbImp := openrtb2.Imp{Ext: extBidder} - seatID, err := getExtInfo(&ortbImp) + accountId, err := getExtInfo(&ortbImp) if err != nil { if test.wantErr { continue @@ -180,8 +183,8 @@ func TestGetExtInfo(t *testing.T) { t.Fatalf("want: %v, got: %v", test.expectedAdUnitID, ortbImp.TagID) } - if test.expectedSeatID != seatID { - t.Fatalf("want: %v, got: %v", test.expectedSeatID, seatID) + if test.expectedAccountId != accountId { + t.Fatalf("want: %v, got: %v", test.expectedAccountId, accountId) } } } diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json new file mode 100644 index 00000000000..fc3678ea34b --- /dev/null +++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account.json @@ -0,0 +1,142 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "accountId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://mockup.seeding-alliance.de/?ssp=123", + "body": { + "cur": [ + "EUR" + ], + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "example-tag-id", + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "accountId": "123" + } + } + } + ], + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "123", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "12341234", + "adm": "some-test-ad", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12341234", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json new file mode 100644 index 00000000000..3f010e6075d --- /dev/null +++ b/adapters/seedingAlliance/seedingAlliancetest/exemplary/banner_with_account_and_seat.json @@ -0,0 +1,144 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "seatId": "ignored", + "accountId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://mockup.seeding-alliance.de/?ssp=123", + "body": { + "cur": [ + "EUR" + ], + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "tagid": "example-tag-id", + "ext": { + "bidder": { + "adUnitId": "example-tag-id", + "seatId": "ignored", + "accountId": "123" + } + } + } + ], + "site": { + "publisher": { + "id": "foo", + "name": "foo" + } + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "123", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adid": "12341234", + "adm": "some-test-ad", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "h": 250, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "EUR" + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "EUR", + "bids": [ + { + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "12341234", + "adomain": [ + "domain.com" + ], + "iurl": "http://abc.com/cr?id=12341234", + "cid": "123", + "crid": "12341234", + "w": 300, + "h": 250, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/openrtb_ext/imp_seedingAlliance.go b/openrtb_ext/imp_seedingAlliance.go index 0f594d3c933..d383ad39d6e 100644 --- a/openrtb_ext/imp_seedingAlliance.go +++ b/openrtb_ext/imp_seedingAlliance.go @@ -1,6 +1,7 @@ package openrtb_ext type ImpExtSeedingAlliance struct { - AdUnitID string `json:"adUnitId"` - SeatID string `json:"seatId"` + AdUnitID string `json:"adUnitId"` + SeatID string `json:"seatId"` + AccountID string `json:"accountId"` } diff --git a/static/bidder-params/seedingAlliance.json b/static/bidder-params/seedingAlliance.json index 5fe463b6803..d72086230aa 100644 --- a/static/bidder-params/seedingAlliance.json +++ b/static/bidder-params/seedingAlliance.json @@ -11,7 +11,11 @@ }, "seatId": { "type": "string", - "description": "Seat ID" + "description": "Deprecated, please use accountId" + }, + "accountId": { + "type": "string", + "description": "Account ID of partner" } }, "required": [ From 1d96d891dfd7789dfd87ad37a7610f2adb31bb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zdravko=20Kosanovi=C4=87?= <41286499+zkosanovic@users.noreply.github.com> Date: Fri, 23 Feb 2024 05:23:52 +0400 Subject: [PATCH 27/69] MinuteMedia: Add GPP macros (#3497) --- static/bidder-info/minutemedia.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/minutemedia.yaml b/static/bidder-info/minutemedia.yaml index efe58e57240..55f5a2b7cd3 100644 --- a/static/bidder-info/minutemedia.yaml +++ b/static/bidder-info/minutemedia.yaml @@ -14,5 +14,5 @@ capabilities: - video userSync: iframe: - url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: https://pbs-cs.minutemedia-prebid.com/pbs-iframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}} userMacro: "[PBS_UID]" From 14fcbb780bc649da67d927a834b425872de35ffb Mon Sep 17 00:00:00 2001 From: Aditya Mahendrakar Date: Thu, 22 Feb 2024 17:30:21 -0800 Subject: [PATCH 28/69] Update prebid.org url to https (#3529) --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index a3518ce27ae..ac79079f34f 100644 --- a/static/index.html +++ b/static/index.html @@ -3,7 +3,7 @@ Prebid Server - Prebid Server is a server-to-server proxy for Prebid.js users. + Prebid Server is a server-to-server proxy for Prebid.js users. The host is not responsible for the content or advertising delivered through this proxy. For more information, please contact prebid-server - at - prebid.org. From 17b7082a9d2d8fc2e0a6a15bc836e2e5227eeafe Mon Sep 17 00:00:00 2001 From: ahmadlob <109217988+ahmadlob@users.noreply.github.com> Date: Mon, 26 Feb 2024 20:46:39 +0200 Subject: [PATCH 29/69] Taboola: Fix gpp query param (#3515) --- static/bidder-info/taboola.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/taboola.yaml b/static/bidder-info/taboola.yaml index 436f746959a..0fccee145bc 100644 --- a/static/bidder-info/taboola.yaml +++ b/static/bidder-info/taboola.yaml @@ -13,8 +13,8 @@ capabilities: - native userSync: redirect: - url: https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: "https://trc.taboola.com/sg/ps/1/cm?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}" userMacro: "" iframe: - url: https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}} + url: "https://cdn.taboola.com/scripts/ps-sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redirect={{.RedirectURL}}" userMacro: "" From 030da80c908945f4b23a17778c67c7a18a4ba0e6 Mon Sep 17 00:00:00 2001 From: pm-avinash-kapre <112699665+AvinashKapre@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:00:58 +0530 Subject: [PATCH 30/69] Fix: Pubstack memory leak (#3541) --- analytics/pubstack/eventchannel/sender.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/analytics/pubstack/eventchannel/sender.go b/analytics/pubstack/eventchannel/sender.go index 951de4d414e..fe068b1555f 100644 --- a/analytics/pubstack/eventchannel/sender.go +++ b/analytics/pubstack/eventchannel/sender.go @@ -3,10 +3,11 @@ package eventchannel import ( "bytes" "fmt" - "github.com/golang/glog" "net/http" "net/url" "path" + + "github.com/golang/glog" ) type Sender = func(payload []byte) error @@ -26,6 +27,7 @@ func NewHttpSender(client *http.Client, endpoint string) Sender { if err != nil { return err } + resp.Body.Close() if resp.StatusCode != http.StatusOK { glog.Errorf("[pubstack] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) From d13dfc58e8d0c5e4f9f5412ad433f54f3e558b22 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Tue, 27 Feb 2024 14:27:21 -0500 Subject: [PATCH 31/69] DSA: Bid response adrender, behalf & paid validations (#3523) --- dsa/validate.go | 102 ++++++--- dsa/validate_test.go | 308 ++++++++++++++++++++++++---- exchange/exchange.go | 8 +- exchange/exchange_test.go | 2 +- openrtb_ext/bid.go | 8 + openrtb_ext/regs.go | 4 +- openrtb_ext/request_wrapper_test.go | 14 +- 7 files changed, 363 insertions(+), 83 deletions(-) diff --git a/dsa/validate.go b/dsa/validate.go index f6ece66e132..034dae8b3cf 100644 --- a/dsa/validate.go +++ b/dsa/validate.go @@ -1,51 +1,103 @@ package dsa import ( + "errors" + "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) - "github.com/buger/jsonparser" +// Required values representing whether a DSA object is required +const ( + Required int8 = 2 // bid responses without DSA object will not be accepted + RequiredOnlinePlatform int8 = 3 // bid responses without DSA object will not be accepted, Publisher is Online Platform ) +// PubRender values representing publisher rendering intentions const ( - // Required - bid responses without DSA object will not be accepted - Required = 2 - // RequiredOnlinePlatform - bid responses without DSA object will not be accepted, Publisher is an Online Platform - RequiredOnlinePlatform = 3 + PubRenderCannotRender int8 = 0 // publisher can't render + PubRenderWillRender int8 = 2 // publisher will render +) + +// AdRender values representing buyer/advertiser rendering intentions +const ( + AdRenderWillRender int8 = 1 // buyer/advertiser will render +) + +var ( + ErrDsaMissing = errors.New("DSA object missing when required") + ErrBehalfTooLong = errors.New("DSA behalf exceeds limit of 100 chars") + ErrPaidTooLong = errors.New("DSA paid exceeds limit of 100 chars") + ErrNeitherWillRender = errors.New("DSA publisher and buyer both signal will not render") + ErrBothWillRender = errors.New("DSA publisher and buyer both signal will render") +) + +const ( + behalfMaxLength = 100 + paidMaxLength = 100 ) // Validate determines whether a given bid is valid from a DSA perspective. // A bid is considered valid unless the bid request indicates that a DSA object is required -// in bid responses and the object happens to be missing from the specified bid. -func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) (valid bool) { - if !dsaRequired(req) { - return true +// in bid responses and the object happens to be missing from the specified bid, or if the bid +// DSA object contents are invalid +func Validate(req *openrtb_ext.RequestWrapper, bid *entities.PbsOrtbBid) error { + reqDSA := getReqDSA(req) + bidDSA := getBidDSA(bid) + + if dsaRequired(reqDSA) && bidDSA == nil { + return ErrDsaMissing } - if bid == nil || bid.Bid == nil { - return false + if bidDSA == nil { + return nil + } + if len(bidDSA.Behalf) > behalfMaxLength { + return ErrBehalfTooLong } - _, dataType, _, err := jsonparser.Get(bid.Bid.Ext, "dsa") - if dataType == jsonparser.Object && err == nil { - return true - } else if err != nil && err != jsonparser.KeyPathNotFoundError { - return true + if len(bidDSA.Paid) > paidMaxLength { + return ErrPaidTooLong } - return false + if reqDSA != nil && reqDSA.PubRender != nil && bidDSA.AdRender != nil { + if *reqDSA.PubRender == PubRenderCannotRender && *bidDSA.AdRender != AdRenderWillRender { + return ErrNeitherWillRender + } + if *reqDSA.PubRender == PubRenderWillRender && *bidDSA.AdRender == AdRenderWillRender { + return ErrBothWillRender + } + } + return nil } // dsaRequired examines the bid request to determine if the dsarequired field indicates // that bid responses include a dsa object -func dsaRequired(req *openrtb_ext.RequestWrapper) bool { +func dsaRequired(dsa *openrtb_ext.ExtRegsDSA) bool { + if dsa == nil || dsa.Required == nil { + return false + } + return *dsa.Required == Required || *dsa.Required == RequiredOnlinePlatform +} + +// getReqDSA retrieves the DSA object from the request +func getReqDSA(req *openrtb_ext.RequestWrapper) *openrtb_ext.ExtRegsDSA { + if req == nil { + return nil + } regExt, err := req.GetRegExt() if regExt == nil || err != nil { - return false + return nil } - regsDSA := regExt.GetDSA() - if regsDSA == nil { - return false + return regExt.GetDSA() +} + +// getBidDSA retrieves the DSA object from the bid +func getBidDSA(bid *entities.PbsOrtbBid) *openrtb_ext.ExtBidDSA { + if bid == nil || bid.Bid == nil { + return nil } - if regsDSA.Required == Required || regsDSA.Required == RequiredOnlinePlatform { - return true + var bidExt openrtb_ext.ExtBid + if err := jsonutil.Unmarshal(bid.Bid.Ext, &bidExt); err != nil { + return nil } - return false + return bidExt.DSA } diff --git a/dsa/validate_test.go b/dsa/validate_test.go index 74d428c3e9e..9dfa32c6efe 100644 --- a/dsa/validate_test.go +++ b/dsa/validate_test.go @@ -2,23 +2,48 @@ package dsa import ( "encoding/json" + "strings" "testing" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/exchange/entities" "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" ) func TestValidate(t *testing.T) { + var ( + validBehalf = strings.Repeat("a", 100) + invalidBehalf = strings.Repeat("a", 101) + validPaid = strings.Repeat("a", 100) + invalidPaid = strings.Repeat("a", 101) + ) + tests := []struct { name string giveRequest *openrtb_ext.RequestWrapper giveBid *entities.PbsOrtbBid - wantValid bool + wantError error }{ { - name: "not_required", + name: "nil", + giveRequest: nil, + giveBid: nil, + wantError: nil, + }, + { + name: "request_nil", + giveRequest: nil, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, + }, + { + name: "not_required_and_bid_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -27,7 +52,23 @@ func TestValidate(t *testing.T) { }, }, giveBid: nil, - wantValid: true, + wantError: nil, + }, + { + name: "not_required_and_bid_dsa_is_valid", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 0,"pubrender":0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, }, { name: "required_and_bid_is_nil", @@ -39,10 +80,10 @@ func TestValidate(t *testing.T) { }, }, giveBid: nil, - wantValid: false, + wantError: ErrDsaMissing, }, { - name: "required_and_bid.bid_is_nil", + name: "required_and_bid_dsa_has_invalid_behalf", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -50,11 +91,15 @@ func TestValidate(t *testing.T) { }, }, }, - giveBid: &entities.PbsOrtbBid{}, - wantValid: false, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + invalidBehalf + `"}}`), + }, + }, + wantError: ErrBehalfTooLong, }, { - name: "required_and_bid.ext.dsa_not_present", + name: "required_and_bid_dsa_has_invalid_paid", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -64,13 +109,61 @@ func TestValidate(t *testing.T) { }, giveBid: &entities.PbsOrtbBid{ Bid: &openrtb2.Bid{ - Ext: json.RawMessage(`{}`), + Ext: json.RawMessage(`{"dsa":{"paid":"` + invalidPaid + `"}}`), + }, + }, + wantError: ErrPaidTooLong, + }, + { + name: "required_and_neither_will_render", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"adrender": 0}}`), + }, + }, + wantError: ErrNeitherWillRender, + }, + { + name: "required_and_both_will_render", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 2}}`), + }, }, }, - wantValid: false, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"adrender": 1}}`), + }, + }, + wantError: ErrBothWillRender, }, { - name: "required_and_bid.ext.dsa_present", + name: "required_and_bid_dsa_is_valid", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2,"pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":1}}`), + }, + }, + wantError: nil, + }, + { + name: "required_and_bid_dsa_is_valid_no_pubrender", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -80,17 +173,37 @@ func TestValidate(t *testing.T) { }, giveBid: &entities.PbsOrtbBid{ Bid: &openrtb2.Bid{ - Ext: json.RawMessage(`{"dsa": {}}`), + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `","adrender":2}}`), + }, + }, + wantError: nil, + }, + { + name: "required_and_bid_dsa_is_valid_no_adrender", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Regs: &openrtb2.Regs{ + Ext: json.RawMessage(`{"dsa": {"dsarequired": 2, "pubrender": 0}}`), + }, + }, + }, + giveBid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa":{"behalf":"` + validBehalf + `","paid":"` + validPaid + `"}}`), }, }, - wantValid: true, + wantError: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - valid := Validate(tt.giveRequest, tt.giveBid) - assert.Equal(t, tt.wantValid, valid) + err := Validate(tt.giveRequest, tt.giveBid) + if tt.wantError != nil { + assert.Equal(t, err, tt.wantError) + } else { + assert.NoError(t, err) + } }) } } @@ -98,55 +211,110 @@ func TestValidate(t *testing.T) { func TestDSARequired(t *testing.T) { tests := []struct { name string - giveRequest *openrtb_ext.RequestWrapper + giveReqDSA *openrtb_ext.ExtRegsDSA wantRequired bool }{ { - name: "not_required_and_reg.ext.dsa_is_nil", - giveRequest: &openrtb_ext.RequestWrapper{ - BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{}`), - }, - }, + name: "nil", + giveReqDSA: nil, + wantRequired: false, + }, + { + name: "nil_required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: nil, + }, + wantRequired: false, + }, + { + name: "not_required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](0), + }, + wantRequired: false, + }, + { + name: "not_required_supported", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](1), }, wantRequired: false, }, { - name: "not_required_and_reg.ext.dsa_is_empty", + name: "required", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](2), + }, + wantRequired: true, + }, + { + name: "required_online_platform", + giveReqDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](3), + }, + wantRequired: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + required := dsaRequired(tt.giveReqDSA) + assert.Equal(t, tt.wantRequired, required) + }) + } +} + +func TestGetReqDSA(t *testing.T) { + tests := []struct { + name string + giveRequest *openrtb_ext.RequestWrapper + expectedDSA *openrtb_ext.ExtRegsDSA + }{ + { + name: "req_is_nil", + giveRequest: nil, + expectedDSA: nil, + }, + { + name: "bidrequest_is_nil", + giveRequest: &openrtb_ext.RequestWrapper{ + BidRequest: nil, + }, + expectedDSA: nil, + }, + { + name: "req.regs_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {}}`), - }, + Regs: nil, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_0", + name: "req.regs.ext_is_nil", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 0}}`), + Ext: nil, }, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_1", + name: "req.regs.ext_is_empty", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 1}}`), + Ext: json.RawMessage(`{}`), }, }, }, - wantRequired: false, + expectedDSA: nil, }, { - name: "required_and_reg.ext.dsa_is_2", + name: "req.regs.ext_dsa_is_populated", giveRequest: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{ Regs: &openrtb2.Regs{ @@ -154,25 +322,75 @@ func TestDSARequired(t *testing.T) { }, }, }, - wantRequired: true, + expectedDSA: &openrtb_ext.ExtRegsDSA{ + Required: ptrutil.ToPtr[int8](2), + }, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dsa := getReqDSA(tt.giveRequest) + assert.Equal(t, tt.expectedDSA, dsa) + }) + } +} + +func TestGetBidDSA(t *testing.T) { + tests := []struct { + name string + bid *entities.PbsOrtbBid + expectedDSA *openrtb_ext.ExtBidDSA + }{ { - name: "required_and_reg.ext.dsa_is_3", - giveRequest: &openrtb_ext.RequestWrapper{ - BidRequest: &openrtb2.BidRequest{ - Regs: &openrtb2.Regs{ - Ext: json.RawMessage(`{"dsa": {"dsarequired": 3}}`), - }, + name: "bid_is_nil", + bid: nil, + expectedDSA: nil, + }, + { + name: "bid.bid_is_nil", + bid: &entities.PbsOrtbBid{ + Bid: nil, + }, + expectedDSA: nil, + }, + { + name: "bid.bid.ext_is_nil", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: nil, }, }, - wantRequired: true, + expectedDSA: nil, + }, + { + name: "bid.bid.ext_is_empty", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{}`), + }, + }, + expectedDSA: nil, + }, + { + name: "bid.bid.ext.dsa_is_populated", + bid: &entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + Ext: json.RawMessage(`{"dsa": {"behalf":"test1","paid":"test2","adrender":1}}`), + }, + }, + expectedDSA: &openrtb_ext.ExtBidDSA{ + Behalf: "test1", + Paid: "test2", + AdRender: ptrutil.ToPtr[int8](1), + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - required := dsaRequired(tt.giveRequest) - assert.Equal(t, tt.wantRequired, required) + dsa := getBidDSA(tt.bid) + assert.Equal(t, tt.expectedDSA, dsa) }) } } diff --git a/exchange/exchange.go b/exchange/exchange.go index b9dae6725c6..41134104f37 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -1245,12 +1245,12 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea errs := make([]error, 0, 1) for _, bid := range bids { - if !dsa.Validate(bidRequest, bid) { - RequiredError := openrtb_ext.ExtBidderMessage{ + if err := dsa.Validate(bidRequest, bid); err != nil { + dsaMessage := openrtb_ext.ExtBidderMessage{ Code: errortypes.InvalidBidResponseDSAWarningCode, - Message: "bid response rejected: DSA object missing when required", + Message: fmt.Sprintf("bid rejected: %s", err.Error()), } - bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], RequiredError) + bidResponseExt.Warnings[adapter] = append(bidResponseExt.Warnings[adapter], dsaMessage) seatNonBids.addBid(bid, int(ResponseRejectedGeneral), adapter.String()) continue // Don't add bid to result diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index e484e21a42e..19c1eb67267 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -4758,7 +4758,7 @@ func TestMakeBidWithValidation(t *testing.T) { name: "One_of_two_bids_is_invalid_based_on_DSA_object_presence", givenBidRequestExt: json.RawMessage(`{"dsa": {"dsarequired": 2}}`), givenValidations: config.Validations{}, - givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {}}`)}}, {Bid: &openrtb2.Bid{}}}, + givenBids: []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{Ext: json.RawMessage(`{"dsa": {"adrender":1}}`)}}, {Bid: &openrtb2.Bid{}}}, givenSeat: "pubmatic", expectedNumOfBids: 1, expectedNonBids: &nonBids{ diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go index 2e190389212..7d3dcbd70bf 100644 --- a/openrtb_ext/bid.go +++ b/openrtb_ext/bid.go @@ -7,6 +7,7 @@ import ( // ExtBid defines the contract for bidresponse.seatbid.bid[i].ext type ExtBid struct { + DSA *ExtBidDSA `json:"dsa,omitempty"` Prebid *ExtBidPrebid `json:"prebid,omitempty"` } @@ -83,6 +84,13 @@ type ExtBidPrebidEvents struct { Imp string `json:"imp,omitempty"` } +// ExtBidDSA defines the contract for bidresponse.seatbid.bid[i].ext.dsa +type ExtBidDSA struct { + AdRender *int8 `json:"adrender,omitempty"` + Behalf string `json:"behalf,omitempty"` + Paid string `json:"paid,omitempty"` +} + // BidType describes the allowed values for bidresponse.seatbid.bid[i].ext.prebid.type type BidType string diff --git a/openrtb_ext/regs.go b/openrtb_ext/regs.go index 8057ce2ccd7..eca5ff98e55 100644 --- a/openrtb_ext/regs.go +++ b/openrtb_ext/regs.go @@ -16,5 +16,7 @@ type ExtRegs struct { // ExtRegsDSA defines the contract for bidrequest.regs.ext.dsa type ExtRegsDSA struct { // Required should be a between 0 and 3 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md - Required int8 `json:"dsarequired,omitempty"` + Required *int8 `json:"dsarequired,omitempty"` + // PubRender should be between 0 and 2 inclusive, see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md + PubRender *int8 `json:"pubrender,omitempty"` } diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index f04a51a4bdc..425229e54e9 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -2069,7 +2069,7 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_nil_-_dirty_and_different_-_change", request: openrtb2.BidRequest{}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{ Regs: &openrtb2.Regs{ Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), @@ -2085,7 +2085,7 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_ext_nil_-_dirty_and_different_-_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true, gdpr: ptrutil.ToPtr[int8](1), gdprDirty: true, usPrivacy: strA, usPrivacyDirty: true}, expectedRequest: openrtb2.BidRequest{ Regs: &openrtb2.Regs{ Ext: json.RawMessage(`{"dsa":{"dsarequired":1},"gdpr":1,"us_privacy":"a"}`), @@ -2101,13 +2101,13 @@ func TestRebuildRegExt(t *testing.T) { { name: "req_regs_dsa_populated_-_dirty_and_different-_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 2}, dsaDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](2)}, dsaDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":2}}`)}}, }, { name: "req_regs_dsa_populated_-_dirty_and_same_-_no_change", request: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, - regExt: RegExt{dsa: &ExtRegsDSA{Required: 1}, dsaDirty: true}, + regExt: RegExt{dsa: &ExtRegsDSA{Required: ptrutil.ToPtr[int8](1)}, dsaDirty: true}, expectedRequest: openrtb2.BidRequest{Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"dsa":{"dsarequired":1}}`)}}, }, { @@ -2215,7 +2215,7 @@ func TestRegExtUnmarshal(t *testing.T) { regExt: &RegExt{}, extJson: json.RawMessage(`{"dsa":{"dsarequired":1}}`), expectDSA: &ExtRegsDSA{ - Required: 1, + Required: ptrutil.ToPtr[int8](1), }, expectError: false, }, @@ -2224,7 +2224,7 @@ func TestRegExtUnmarshal(t *testing.T) { regExt: &RegExt{}, extJson: json.RawMessage(`{"dsa":{"dsarequired":""}}`), expectDSA: &ExtRegsDSA{ - Required: 0, + Required: ptrutil.ToPtr[int8](0), }, expectError: true, }, @@ -2299,7 +2299,7 @@ func TestRegExtGetDSASetDSA(t *testing.T) { assert.False(t, regExt.Dirty()) dsa := &ExtRegsDSA{ - Required: 2, + Required: ptrutil.ToPtr[int8](2), } regExt.SetDSA(dsa) assert.True(t, regExt.Dirty()) From 11decc200677979abc6f36763a38519dd8a626ae Mon Sep 17 00:00:00 2001 From: linux019 Date: Tue, 27 Feb 2024 21:51:35 +0200 Subject: [PATCH 32/69] Fix modules template and builder (#3534) Co-authored-by: oaleksieiev --- modules/generator/builder.tmpl | 17 +++++++++-------- modules/generator/buildergen.go | 21 +++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/modules/generator/builder.tmpl b/modules/generator/builder.tmpl index b7b78103dbe..db0cd1a4fb4 100644 --- a/modules/generator/builder.tmpl +++ b/modules/generator/builder.tmpl @@ -1,20 +1,21 @@ package modules - -{{if .}} import ( - {{- range .}} - {{.Vendor}}{{.Module | Title}} "github.com/prebid/prebid-server/v2/modules/{{.Vendor}}/{{.Module}}" +{{- range $vendor, $modules := .}} + {{- range $module := $modules}} + {{$vendor}}{{$module | Title}} "github.com/prebid/prebid-server/v2/modules/{{$vendor}}/{{$module}}" {{- end}} +{{- end}} ) -{{end}} // builders returns mapping between module name and its builder // vendor and module names are chosen based on the module directory name func builders() ModuleBuilders { return ModuleBuilders{ - {{- range .}} - "{{.Vendor}}": { - "{{.Module}}": {{.Vendor}}{{.Module | Title}}.Builder, + {{- range $vendor, $modules := .}} + "{{$vendor}}": { + {{- range $module := $modules}} + "{{$module}}": {{$vendor}}{{$module | Title}}.Builder, + {{- end}} }, {{- end}} } diff --git a/modules/generator/buildergen.go b/modules/generator/buildergen.go index b906682b7a3..219932420bd 100644 --- a/modules/generator/buildergen.go +++ b/modules/generator/buildergen.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strings" "text/template" ) @@ -20,26 +21,26 @@ var ( outName = "builder.go" ) -type Module struct { - Vendor string - Module string -} - func main() { - var modules []Module + modules := make(map[string][]string) filepath.WalkDir("./", func(path string, d fs.DirEntry, err error) error { if !r.MatchString(path) { return nil } match := r.FindStringSubmatch(path) - modules = append(modules, Module{ - Vendor: match[1], - Module: match[2], - }) + vendorModules := modules[match[1]] + vendorModules = append(vendorModules, match[2]) + modules[match[1]] = vendorModules + return nil }) + for vendorName, names := range modules { + sort.Strings(names) + modules[vendorName] = names + } + funcMap := template.FuncMap{"Title": strings.Title} t, err := template.New(tmplName).Funcs(funcMap).ParseFiles(fmt.Sprintf("generator/%s", tmplName)) if err != nil { From 9f8a9c45b00701964826000331de0eb86394a469 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Tue, 27 Feb 2024 23:24:19 +0100 Subject: [PATCH 33/69] Reformat structures to use key names (#3524) Signed-off-by: Dmitry S --- endpoints/info/bidders_detail_test.go | 5 +- exchange/exchange_test.go | 82 +++++++++---------- hooks/hookexecution/execution_test.go | 2 +- .../aspects/request_timeout_handler_test.go | 2 +- .../backends/db_fetcher/fetcher.go | 2 +- .../backends/empty_fetcher/fetcher.go | 2 +- .../backends/file_fetcher/fetcher_test.go | 2 +- util/stringutil/stringutil_test.go | 6 +- 8 files changed, 53 insertions(+), 50 deletions(-) diff --git a/endpoints/info/bidders_detail_test.go b/endpoints/info/bidders_detail_test.go index 2911aa8e0e9..4965decef40 100644 --- a/endpoints/info/bidders_detail_test.go +++ b/endpoints/info/bidders_detail_test.go @@ -453,7 +453,10 @@ func TestBiddersDetailHandler(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { responseRecorder := httptest.NewRecorder() - handler(responseRecorder, nil, httprouter.Params{{"bidderName", test.givenBidder}}) + handler(responseRecorder, nil, httprouter.Params{{ + Key: "bidderName", + Value: test.givenBidder, + }}) result := responseRecorder.Result() assert.Equal(t, result.StatusCode, test.expectedStatus, test.description+":statuscode") diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 19c1eb67267..9156fc06c78 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -2566,10 +2566,10 @@ func TestCategoryMapping(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2621,10 +2621,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) { bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 40.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 40.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2675,9 +2675,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2757,9 +2757,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) { bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1} bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 30.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids := []*entities.PbsOrtbBid{ &bid1_1, @@ -2885,11 +2885,11 @@ func TestNoCategoryDedupe(t *testing.T) { bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1} bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""} - bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 14.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_3 := entities.PbsOrtbBid{Bid: &bid3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_4 := entities.PbsOrtbBid{Bid: &bid4, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_5 := entities.PbsOrtbBid{Bid: &bid5, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} selectedBids := make(map[string]int) expectedCategories := map[string]string{ @@ -2965,8 +2965,8 @@ func TestCategoryMappingBidderName(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -3019,8 +3019,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) { bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1} - bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, nil, nil, 0, false, "", 12.0000, "USD", ""} + bid1_1 := entities.PbsOrtbBid{Bid: &bid1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 17}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_2 := entities.PbsOrtbBid{Bid: &bid2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 8}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 12.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids1 := []*entities.PbsOrtbBid{ &bid1_1, @@ -3131,7 +3131,7 @@ func TestBidRejectionErrors(t *testing.T) { innerBids := []*entities.PbsOrtbBid{} for _, bid := range test.bids { currentBid := entities.PbsOrtbBid{ - bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, nil, 0, false, "", 10.0000, "USD", ""} + Bid: bid, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBids = append(innerBids, ¤tBid) } @@ -3179,8 +3179,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) { bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1} bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1 := entities.PbsOrtbBid{Bid: &bidApn1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn2 := entities.PbsOrtbBid{Bid: &bidApn2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1, @@ -3259,11 +3259,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1} bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} - bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} + bid1_Apn2_1 := entities.PbsOrtbBid{Bid: &bidApn2_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn2_2 := entities.PbsOrtbBid{Bid: &bidApn2_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} innerBidsApn1 := []*entities.PbsOrtbBid{ &bid1_Apn1_1, @@ -3341,9 +3341,9 @@ func TestRemoveBidById(t *testing.T) { bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1} bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1} - bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} - bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""} - bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""} + bid1_Apn1_1 := entities.PbsOrtbBid{Bid: &bidApn1_1, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_2 := entities.PbsOrtbBid{Bid: &bidApn1_2, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 20.0000, OriginalBidCur: "USD", TargetBidderCode: ""} + bid1_Apn1_3 := entities.PbsOrtbBid{Bid: &bidApn1_3, BidMeta: nil, BidType: "video", BidTargets: nil, BidVideo: &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, BidEvents: nil, BidFloors: nil, DealPriority: 0, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 10.0000, OriginalBidCur: "USD", TargetBidderCode: ""} type aTest struct { desc string @@ -3544,7 +3544,7 @@ func TestApplyDealSupport(t *testing.T) { }, } - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.in.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.in.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} bidCategory := map[string]string{ bid.Bid.ID: test.in.targ["hb_pb_cat_dur"], } @@ -3604,8 +3604,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3650,8 +3650,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3701,8 +3701,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) { allBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{ "imp_id1": { openrtb_ext.BidderName("appnexus"): { - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, - &entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, + &entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "789101"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: 5, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""}, }, }, }, @@ -3893,7 +3893,7 @@ func TestUpdateHbPbCatDur(t *testing.T) { } for _, test := range testCases { - bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""} + bid := entities.PbsOrtbBid{Bid: &openrtb2.Bid{ID: "123456"}, BidMeta: nil, BidType: "video", BidTargets: map[string]string{}, BidVideo: &openrtb_ext.ExtBidPrebidVideo{}, BidEvents: nil, BidFloors: nil, DealPriority: test.dealPriority, DealTierSatisfied: false, GeneratedBidID: "", OriginalBidCPM: 0, OriginalBidCur: "USD", TargetBidderCode: ""} bidCategory := map[string]string{ bid.Bid.ID: test.targ["hb_pb_cat_dur"], } diff --git a/hooks/hookexecution/execution_test.go b/hooks/hookexecution/execution_test.go index c7d17a15e9f..33f652faa2f 100644 --- a/hooks/hookexecution/execution_test.go +++ b/hooks/hookexecution/execution_test.go @@ -176,7 +176,7 @@ func TestHandleModuleActivitiesProcessedAuctionRequestPayload(t *testing.T) { //check input payload didn't change origInPayloadData := test.inPayloadData activityControl := privacy.NewActivityControl(test.privacyConfig) - account := &config.Account{Privacy: config.AccountPrivacy{IPv6Config: config.IPv6{testIPv6ScrubBytes}}} + account := &config.Account{Privacy: config.AccountPrivacy{IPv6Config: config.IPv6{AnonKeepBits: testIPv6ScrubBytes}}} newPayload := handleModuleActivities(test.hookCode, activityControl, test.inPayloadData, account) assert.Equal(t, test.expectedPayloadData.Request.BidRequest, newPayload.Request.BidRequest) assert.Equal(t, origInPayloadData, test.inPayloadData) diff --git a/router/aspects/request_timeout_handler_test.go b/router/aspects/request_timeout_handler_test.go index 4ece14208e8..f77d48aa6e6 100644 --- a/router/aspects/request_timeout_handler_test.go +++ b/router/aspects/request_timeout_handler_test.go @@ -101,7 +101,7 @@ func ExecuteAspectRequest(t *testing.T, timeInQueue string, reqTimeout string, s req.Header.Set(reqTimeoutHeaderName, reqTimeout) } - customHeaders := config.RequestTimeoutHeaders{reqTimeInQueueHeaderName, reqTimeoutHeaderName} + customHeaders := config.RequestTimeoutHeaders{RequestTimeInQueue: reqTimeInQueueHeaderName, RequestTimeoutInQueue: reqTimeoutHeaderName} metrics := &metrics.MetricsEngineMock{} diff --git a/stored_requests/backends/db_fetcher/fetcher.go b/stored_requests/backends/db_fetcher/fetcher.go index 3a9b83f7779..d6fade0e866 100644 --- a/stored_requests/backends/db_fetcher/fetcher.go +++ b/stored_requests/backends/db_fetcher/fetcher.go @@ -152,7 +152,7 @@ func (fetcher *dbFetcher) FetchResponses(ctx context.Context, ids []string) (dat } func (fetcher *dbFetcher) FetchAccount(ctx context.Context, accountDefaultsJSON json.RawMessage, accountID string) (json.RawMessage, []error) { - return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} + return nil, []error{stored_requests.NotFoundError{ID: accountID, DataType: "Account"}} } func (fetcher *dbFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { diff --git a/stored_requests/backends/empty_fetcher/fetcher.go b/stored_requests/backends/empty_fetcher/fetcher.go index c851a997df9..65e5de90480 100644 --- a/stored_requests/backends/empty_fetcher/fetcher.go +++ b/stored_requests/backends/empty_fetcher/fetcher.go @@ -33,7 +33,7 @@ func (fetcher EmptyFetcher) FetchResponses(ctx context.Context, ids []string) (d } func (fetcher EmptyFetcher) FetchAccount(ctx context.Context, accountDefaultJSON json.RawMessage, accountID string) (json.RawMessage, []error) { - return nil, []error{stored_requests.NotFoundError{accountID, "Account"}} + return nil, []error{stored_requests.NotFoundError{ID: accountID, DataType: "Account"}} } func (fetcher EmptyFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { diff --git a/stored_requests/backends/file_fetcher/fetcher_test.go b/stored_requests/backends/file_fetcher/fetcher_test.go index 0155c1aa82c..b4a73870392 100644 --- a/stored_requests/backends/file_fetcher/fetcher_test.go +++ b/stored_requests/backends/file_fetcher/fetcher_test.go @@ -36,7 +36,7 @@ func TestAccountFetcher(t *testing.T) { _, errs = fetcher.FetchAccount(context.Background(), json.RawMessage(`{"events_enabled":true}`), "nonexistent") assertErrorCount(t, 1, errs) assert.Error(t, errs[0]) - assert.Equal(t, stored_requests.NotFoundError{"nonexistent", "Account"}, errs[0]) + assert.Equal(t, stored_requests.NotFoundError{ID: "nonexistent", DataType: "Account"}, errs[0]) _, errs = fetcher.FetchAccount(context.Background(), json.RawMessage(`{"events_enabled"}`), "valid") assertErrorCount(t, 1, errs) diff --git a/util/stringutil/stringutil_test.go b/util/stringutil/stringutil_test.go index 94988ee41c9..a7aa0010995 100644 --- a/util/stringutil/stringutil_test.go +++ b/util/stringutil/stringutil_test.go @@ -30,7 +30,7 @@ func TestStrToInt8Slice(t *testing.T) { in: "malformed", expected: testOutput{ arr: nil, - err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax}, + err: &strconv.NumError{Func: "ParseInt", Num: "malformed", Err: strconv.ErrSyntax}, }, }, { @@ -38,7 +38,7 @@ func TestStrToInt8Slice(t *testing.T) { in: "malformed,2,malformed", expected: testOutput{ arr: nil, - err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax}, + err: &strconv.NumError{Func: "ParseInt", Num: "malformed", Err: strconv.ErrSyntax}, }, }, { @@ -46,7 +46,7 @@ func TestStrToInt8Slice(t *testing.T) { in: "128", expected: testOutput{ arr: nil, - err: &strconv.NumError{"ParseInt", "128", strconv.ErrRange}, + err: &strconv.NumError{Func: "ParseInt", Num: "128", Err: strconv.ErrRange}, }, }, { From fd920153517a33a37271c623ce5ea5ecf8f234c4 Mon Sep 17 00:00:00 2001 From: dzhang-criteo <87757739+dzhang-criteo@users.noreply.github.com> Date: Wed, 28 Feb 2024 14:49:22 +0100 Subject: [PATCH 34/69] Criteo: add GPP macros (#3544) --- static/bidder-info/criteo.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/criteo.yaml b/static/bidder-info/criteo.yaml index 7d58c68d198..d6419d463de 100644 --- a/static/bidder-info/criteo.yaml +++ b/static/bidder-info/criteo.yaml @@ -15,8 +15,8 @@ userSync: # criteo supports user syncing, but requires configuration by the host. contact this # bidder directly at the email address in this file to ask about enabling user sync. redirect: - url: "https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230" + url: "https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}&profile=230" userMacro: "${CRITEO_USER_ID}" iframe: - url: "https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230" + url: "https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&redir={{.RedirectURL}}&profile=230" userMacro: "${CRITEO_USER_ID}" \ No newline at end of file From e8267b8cc8376b63ff9c8498b775a56c00b83de4 Mon Sep 17 00:00:00 2001 From: guscarreon Date: Wed, 28 Feb 2024 14:48:36 -0500 Subject: [PATCH 35/69] Use Json compacter in the bidders/params endpoint (#3395) --- endpoints/openrtb2/auction_test.go | 6 ++ main.go | 3 + openrtb_ext/bidders.go | 1 + router/bidder_params_tests/appnexus.json | 27 ++++++++ router/router.go | 2 +- router/router_test.go | 26 ++++++++ util/jsonutil/jsonutil.go | 37 +++++++++++ util/jsonutil/jsonutil_test.go | 82 ++++++++++++++++++++++++ 8 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 router/bidder_params_tests/appnexus.json diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index a9e16c9490b..9df8b540ab5 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/buger/jsonparser" + jsoniter "github.com/json-iterator/go" "github.com/julienschmidt/httprouter" "github.com/prebid/openrtb/v20/native1" nativeRequests "github.com/prebid/openrtb/v20/native1/request" @@ -43,6 +44,11 @@ import ( const jsonFileExtension string = ".json" +func TestMain(m *testing.M) { + jsoniter.RegisterExtension(&jsonutil.RawMessageExtension{}) + os.Exit(m.Run()) +} + func TestJsonSampleRequests(t *testing.T) { testSuites := []struct { description string diff --git a/main.go b/main.go index e72f02f1f0e..1909199d1e9 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,13 @@ import ( "runtime" "time" + jsoniter "github.com/json-iterator/go" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/currency" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/router" "github.com/prebid/prebid-server/v2/server" + "github.com/prebid/prebid-server/v2/util/jsonutil" "github.com/prebid/prebid-server/v2/util/task" "github.com/golang/glog" @@ -21,6 +23,7 @@ import ( func init() { rand.Seed(time.Now().UnixNano()) + jsoniter.RegisterExtension(&jsonutil.RawMessageExtension{}) } func main() { diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 173120f7301..fcd2b15ce22 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -612,6 +612,7 @@ func NewBidderParamsValidator(schemaDirectory string) (BidderParamValidator, err if _, ok := bidderMap[bidderName]; !ok { return nil, fmt.Errorf("File %s/%s does not match a valid BidderName.", schemaDirectory, fileInfo.Name()) } + toOpen, err := paramsValidator.abs(filepath.Join(schemaDirectory, fileInfo.Name())) if err != nil { return nil, fmt.Errorf("Failed to get an absolute representation of the path: %s, %v", toOpen, err) diff --git a/router/bidder_params_tests/appnexus.json b/router/bidder_params_tests/appnexus.json new file mode 100644 index 00000000000..11dedd41e49 --- /dev/null +++ b/router/bidder_params_tests/appnexus.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Sample schema", + "description": "A sample schema to test the bidder/params endpoint", + "type": "object", + "properties": { + "integer_param": { + "type": "integer", + "minimum": 1, + "description": "A customer id" + }, + "string_param_1": { + "type": "string", + "minLength": 1, + "description": "Text with blanks in between" + }, + "string_param_2": { + "type": "string", + "minLength": 1, + "description": "Text_with_no_blanks_in_between" + } + }, + "required": [ + "integer_param", + "string_param_2" + ] +} diff --git a/router/router.go b/router/router.go index d89d1f59ca2..c285bbdab1b 100644 --- a/router/router.go +++ b/router/router.go @@ -93,7 +93,7 @@ func newJsonDirectoryServer(schemaDirectory string, validator openrtb_ext.Bidder data[aliasName] = bidderData } - response, err := json.Marshal(data) + response, err := jsonutil.Marshal(data) if err != nil { glog.Fatalf("Failed to marshal bidder param JSON-schema: %v", err) } diff --git a/router/router_test.go b/router/router_test.go index cc2f077e5e6..866a8440f3f 100644 --- a/router/router_test.go +++ b/router/router_test.go @@ -7,6 +7,7 @@ import ( "os" "testing" + jsoniter "github.com/json-iterator/go" "github.com/prebid/prebid-server/v2/config" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/jsonutil" @@ -18,6 +19,11 @@ const adapterDirectory = "../adapters" type testValidator struct{} +func TestMain(m *testing.M) { + jsoniter.RegisterExtension(&jsonutil.RawMessageExtension{}) + os.Exit(m.Run()) +} + func (validator *testValidator) Validate(name openrtb_ext.BidderName, ext json.RawMessage) error { return nil } @@ -275,3 +281,23 @@ func TestValidateDefaultAliases(t *testing.T) { } } } + +func TestBidderParamsCompactedOutput(t *testing.T) { + expectedFormattedResponse := `{"appnexus":{"$schema":"http://json-schema.org/draft-04/schema#","title":"Sample schema","description":"A sample schema to test the bidder/params endpoint","type":"object","properties":{"integer_param":{"type":"integer","minimum":1,"description":"A customer id"},"string_param_1":{"type":"string","minLength":1,"description":"Text with blanks in between"},"string_param_2":{"type":"string","minLength":1,"description":"Text_with_no_blanks_in_between"}},"required":["integer_param","string_param_2"]}}` + + // Setup + inSchemaDirectory := "bidder_params_tests" + paramsValidator, err := openrtb_ext.NewBidderParamsValidator(inSchemaDirectory) + assert.NoError(t, err, "Error initialing validator") + + handler := newJsonDirectoryServer(inSchemaDirectory, paramsValidator, nil, nil) + recorder := httptest.NewRecorder() + request, err := http.NewRequest("GET", "/bidder/params", nil) + assert.NoError(t, err, "Error creating request") + + // Run + handler(recorder, request, nil) + + // Assertions + assert.Equal(t, expectedFormattedResponse, recorder.Body.String()) +} diff --git a/util/jsonutil/jsonutil.go b/util/jsonutil/jsonutil.go index b5bb47cca9a..695ccd8a5c1 100644 --- a/util/jsonutil/jsonutil.go +++ b/util/jsonutil/jsonutil.go @@ -5,8 +5,10 @@ import ( "encoding/json" "io" "strings" + "unsafe" jsoniter "github.com/json-iterator/go" + "github.com/modern-go/reflect2" "github.com/prebid/prebid-server/v2/errortypes" ) @@ -211,3 +213,38 @@ func tryExtractErrorMessage(err error) string { func isLikelyDetailedErrorMessage(msg string) bool { return !strings.HasPrefix(msg, "request.") } + +// RawMessageExtension will call json.Compact() on every json.RawMessage field when getting marshalled. +type RawMessageExtension struct { + jsoniter.DummyExtension +} + +// CreateEncoder substitutes the default jsoniter encoder of the json.RawMessage type with ours, that +// calls json.Compact() before writting to the stream +func (e *RawMessageExtension) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder { + if typ == jsonRawMessageType { + return &rawMessageCodec{} + } + return nil +} + +var jsonRawMessageType = reflect2.TypeOfPtr(&json.RawMessage{}).Elem() + +// rawMessageCodec implements jsoniter.ValEncoder interface so we can override the default json.RawMessage Encode() +// function with our implementation +type rawMessageCodec struct{} + +func (codec *rawMessageCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { + if ptr != nil { + jsonRawMsg := *(*[]byte)(ptr) + + dst := bytes.NewBuffer(make([]byte, 0, len(jsonRawMsg))) + if err := json.Compact(dst, jsonRawMsg); err == nil { + stream.Write(dst.Bytes()) + } + } +} + +func (codec *rawMessageCodec) IsEmpty(ptr unsafe.Pointer) bool { + return ptr == nil || len(*((*json.RawMessage)(ptr))) == 0 +} diff --git a/util/jsonutil/jsonutil_test.go b/util/jsonutil/jsonutil_test.go index 09fb6727309..96632d62548 100644 --- a/util/jsonutil/jsonutil_test.go +++ b/util/jsonutil/jsonutil_test.go @@ -1,10 +1,15 @@ package jsonutil import ( + "bytes" + "encoding/json" "errors" "strings" "testing" + "unsafe" + jsoniter "github.com/json-iterator/go" + "github.com/modern-go/reflect2" "github.com/stretchr/testify/assert" ) @@ -240,3 +245,80 @@ func TestTryExtractErrorMessage(t *testing.T) { }) } } + +func TestCreateEncoder(t *testing.T) { + testCases := []struct { + desc string + inType reflect2.Type + expectedValEncoder jsoniter.ValEncoder + }{ + { + desc: "With_extension", + inType: reflect2.TypeOfPtr((*jsoniter.Any)(nil)).Elem(), + expectedValEncoder: nil, + }, + { + desc: "No_extension", + inType: reflect2.TypeOfPtr(&json.RawMessage{}).Elem(), + expectedValEncoder: &rawMessageCodec{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + extension := &RawMessageExtension{} + encoder := extension.CreateEncoder(tc.inType) + assert.IsType(t, encoder, tc.expectedValEncoder) + }) + } +} + +func TestEncode(t *testing.T) { + jsonBlob := json.RawMessage(`{ + "properties": { + "string": "Blanks spaces in between words to not be removed if compacted", + "integer": 5, + "string_array": [ + "string array elem one", + "string array elem two" + ] + } +}`) + + t.Run( + "Nil_pointer", + func(t *testing.T) { + // set test + encoder := &rawMessageCodec{} + output := bytes.NewBuffer([]byte{}) + stream := jsoniter.NewStream(jsonConfigValidationOn, output, len(jsonBlob)) + + // run + encoder.Encode(nil, stream) + + // assertions + assert.Equal(t, "", output.String()) + assert.Equal(t, true, encoder.IsEmpty(nil)) + }, + ) + t.Run( + "json.RawMessage_compact_JSON", + func(t *testing.T) { + // set test + encoder := &rawMessageCodec{} + output := bytes.NewBuffer([]byte{}) + stream := jsoniter.NewStream(jsonConfigValidationOn, output, len(jsonBlob)) + + // run + encoder.Encode(unsafe.Pointer(&jsonBlob), stream) + + // assertions + assert.Equal( + t, + `{"properties":{"string":"Blanks spaces in between words to not be removed if compacted","integer":5,"string_array":["string array elem one","string array elem two"]}}`, + output.String(), + ) + assert.Equal(t, false, encoder.IsEmpty(unsafe.Pointer(&jsonBlob))) + }, + ) +} From 7db3d9f8be2316513c09492a40569cc3c5e89ca2 Mon Sep 17 00:00:00 2001 From: dzhang-criteo <87757739+dzhang-criteo@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:11:09 +0100 Subject: [PATCH 36/69] TheMediaGrid: Add GPP macros (#3545) authored by @dzhang-criteo --- static/bidder-info/grid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/grid.yaml b/static/bidder-info/grid.yaml index bf1832c9590..9d9e7aa58f5 100644 --- a/static/bidder-info/grid.yaml +++ b/static/bidder-info/grid.yaml @@ -15,5 +15,5 @@ capabilities: - native userSync: redirect: - url: "https://x.bidswitch.net/check_uuid/{{.RedirectURL}}?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}" + url: "https://x.bidswitch.net/check_uuid/{{.RedirectURL}}?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&us_privacy={{.USPrivacy}}" userMacro: "${BSW_UUID}" From 4fb7be0e12a85d7e8867086b696097aa5404b9b7 Mon Sep 17 00:00:00 2001 From: SerhiiNahornyi Date: Fri, 1 Mar 2024 07:55:07 +0200 Subject: [PATCH 37/69] Rubicon: Remove api validation (#3493) --- adapters/rubicon/rubicon.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go index 01b1e79799e..6cffa1b2abf 100644 --- a/adapters/rubicon/rubicon.go +++ b/adapters/rubicon/rubicon.go @@ -938,7 +938,7 @@ func isVideo(imp openrtb2.Imp) bool { func isFullyPopulatedVideo(video *openrtb2.Video) bool { // These are just recommended video fields for XAPI - return video.MIMEs != nil && video.Protocols != nil && video.MaxDuration != 0 && video.Linearity != 0 && video.API != nil + return video.MIMEs != nil && video.Protocols != nil && video.MaxDuration != 0 && video.Linearity != 0 } func resolveNativeObject(native *openrtb2.Native, target map[string]interface{}) (*openrtb2.Native, error) { From ecdff687b32728cbc801e0e6f5ae64dcb4d20018 Mon Sep 17 00:00:00 2001 From: Aman Jain <34913883+amanpatniajmer@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:55:45 +0530 Subject: [PATCH 38/69] Medianet: Upgrades to OpenRTB 2.6 (#3548) Co-authored-by: Aman Jain Co-authored-by: rajatgoyal2510 --- .../valid-req-200-bid-response-from-mnet.json | 8 ++------ static/bidder-info/medianet.yaml | 2 ++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json b/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json index e6a9b654a3a..7925ab3f345 100644 --- a/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json +++ b/adapters/medianet/medianettest/supplemental/valid-req-200-bid-response-from-mnet.json @@ -36,9 +36,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } }, "httpCalls": [{ @@ -81,9 +79,7 @@ "USD" ], "regs": { - "ext": { - "gdpr": 0 - } + "gdpr": 0 } } }, diff --git a/static/bidder-info/medianet.yaml b/static/bidder-info/medianet.yaml index ad741e8fb4a..1b23d5aec96 100644 --- a/static/bidder-info/medianet.yaml +++ b/static/bidder-info/medianet.yaml @@ -4,6 +4,8 @@ maintainer: email: "prebid-support@media.net" gvlVendorID: 142 endpointCompression: gzip +openrtb: + version: 2.6 modifyingVastXmlAllowed: true capabilities: app: From 82caa3781e21728450b605a8b73381f6daafeaf7 Mon Sep 17 00:00:00 2001 From: Mikael Lundin Date: Mon, 4 Mar 2024 07:30:03 +0100 Subject: [PATCH 39/69] Send site ext as key values to allow targeting and pick the first eid to solve eids for Schibsted. (#3530) --- adapters/adnuntius/adnuntius.go | 45 ++++++- .../adnuntiustest/supplemental/site-ext.json | 113 ++++++++++++++++++ .../adnuntiustest/supplemental/user-ext.json | 112 +++++++++++++++++ 3 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/site-ext.json create mode 100644 adapters/adnuntius/adnuntiustest/supplemental/user-ext.json diff --git a/adapters/adnuntius/adnuntius.go b/adapters/adnuntius/adnuntius.go index 4b823455815..c1d71cc1ab4 100644 --- a/adapters/adnuntius/adnuntius.go +++ b/adapters/adnuntius/adnuntius.go @@ -33,6 +33,9 @@ type adnAdunit struct { type extDeviceAdnuntius struct { NoCookies bool `json:"noCookies,omitempty"` } +type siteExt struct { + Data interface{} `json:"data"` +} type Ad struct { Bid struct { @@ -71,9 +74,10 @@ type adnMetaData struct { Usi string `json:"usi,omitempty"` } type adnRequest struct { - AdUnits []adnAdunit `json:"adUnits"` - MetaData adnMetaData `json:"metaData,omitempty"` - Context string `json:"context,omitempty"` + AdUnits []adnAdunit `json:"adUnits"` + MetaData adnMetaData `json:"metaData,omitempty"` + Context string `json:"context,omitempty"` + KeyValues interface{} `json:"kv,omitempty"` } type RequestExt struct { @@ -138,7 +142,6 @@ func makeEndpointUrl(ortbRequest openrtb2.BidRequest, a *adapter, noCookies bool if deviceExt.NoCookies { noCookies = true } - } _, offset := a.time.Now().Zone() @@ -249,11 +252,31 @@ func (a *adapter) generateRequests(ortbRequest openrtb2.BidRequest) ([]*adapters site = ortbRequest.Site.Page } + extSite, erro := getSiteExtAsKv(&ortbRequest) + if erro != nil { + return nil, []error{fmt.Errorf("failed to parse site Ext: %v", err)} + } + for _, networkAdunits := range networkAdunitMap { adnuntiusRequest := adnRequest{ - AdUnits: networkAdunits, - Context: site, + AdUnits: networkAdunits, + Context: site, + KeyValues: extSite.Data, + } + + var extUser openrtb_ext.ExtUser + if ortbRequest.User != nil && ortbRequest.User.Ext != nil { + if err := json.Unmarshal(ortbRequest.User.Ext, &extUser); err != nil { + return nil, []error{fmt.Errorf("failed to parse Ext User: %v", err)} + } + } + + // Will change when our adserver can accept multiple user IDS + if extUser.Eids != nil && len(extUser.Eids) > 0 { + if len(extUser.Eids[0].UIDs) > 0 { + adnuntiusRequest.MetaData.Usi = extUser.Eids[0].UIDs[0].ID + } } ortbUser := ortbRequest.User @@ -310,6 +333,16 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, externalRequest *adapte return bidResponse, nil } +func getSiteExtAsKv(request *openrtb2.BidRequest) (siteExt, error) { + var extSite siteExt + if request.Site != nil && request.Site.Ext != nil { + if err := json.Unmarshal(request.Site.Ext, &extSite); err != nil { + return extSite, fmt.Errorf("failed to parse ExtSite in Adnuntius: %v", err) + } + } + return extSite, nil +} + func getGDPR(request *openrtb2.BidRequest) (string, string, error) { gdpr := "" diff --git a/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json new file mode 100644 index 00000000000..1d1ef3d3586 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/site-ext.json @@ -0,0 +1,113 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "id": "1kjh3429kjh295jkl" + }, + "site": { + "ext":{ + "data" : { + "key": ["value"] + } + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=json&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "kv": { + "key": ["value"] + }, + "metaData": { + "usi": "1kjh3429kjh295jkl" + }, + "context": "unknown" + } + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file diff --git a/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json new file mode 100644 index 00000000000..7d5f89377b7 --- /dev/null +++ b/adapters/adnuntius/adnuntiustest/supplemental/user-ext.json @@ -0,0 +1,112 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "user": { + "ext":{ + "eids" : [ + { + "source": "idProvider", + "uids": [ + { "id": "userId", "atype": 1, "ext": { "stype": "ppuid" } } + ] + } + ] + } + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "bidder": { + "auId": "123" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://whatever.url?format=json&tzo=0", + "body": { + "adUnits": [ + { + "auId": "123", + "targetId": "123-test-imp-id", + "dimensions": [[300,250],[300,600]] + } + ], + "metaData": { + "usi": "userId" + }, + "context": "unknown" + } + }, + "mockResponse": { + "status": 200, + "body": { + "adUnits": [ + { + "auId": "0000000000000123", + "targetId": "123-test-imp-id", + "html": "", + "responseId": "adn-rsp-900646517", + "ads": [ + { + "destinationUrls": { + "url": "http://www.google.com" + }, + "bid": { + "amount": 20.0, + "currency": "NOK" + }, + "adId": "adn-id-1559784094", + "creativeWidth": "980", + "creativeHeight": "240", + "creativeId": "jn9hpzvlsf8cpdmm", + "lineItemId": "q7y9qm5b0xt9htrv" + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "adn-id-1559784094", + "impid": "test-imp-id", + "price": 20000, + "adm": "", + "adid": "adn-id-1559784094", + "adomain": [ + "google.com" + ], + "cid": "q7y9qm5b0xt9htrv", + "crid": "jn9hpzvlsf8cpdmm", + "w": 980, + "h": 240 + }, + "type": "banner" + } + ], + "currency": "NOK" + } + ] +} \ No newline at end of file From 93137cdccb52a5e6cc58d2d773e921e09e1e9436 Mon Sep 17 00:00:00 2001 From: Brian Sardo <1168933+bsardo@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:59:23 -0500 Subject: [PATCH 40/69] Convert GDPR bidder name types to string to accommodate analytics (#3554) --- account/account.go | 2 +- account/account_test.go | 8 +-- config/account.go | 6 +- config/account_test.go | 42 ++++++------- config/config.go | 38 ++++++------ config/config_test.go | 104 ++++++++++++++++----------------- gdpr/aggregated_config.go | 6 +- gdpr/aggregated_config_test.go | 38 ++++++------ gdpr/basic_enforcement.go | 13 ++--- gdpr/basic_enforcement_test.go | 15 +++-- gdpr/full_enforcement.go | 9 ++- gdpr/full_enforcement_test.go | 63 ++++++++++++-------- gdpr/gdpr_test.go | 2 +- gdpr/impl.go | 14 ++--- gdpr/impl_test.go | 26 +++++---- gdpr/purpose_config.go | 15 +++-- gdpr/purpose_config_test.go | 90 ++++++++++++++++------------ gdpr/purpose_enforcer.go | 8 +-- gdpr/purpose_enforcer_test.go | 46 ++++++++------- 19 files changed, 286 insertions(+), 259 deletions(-) diff --git a/account/account.go b/account/account.go index 2c243e0dd90..37d36761cca 100644 --- a/account/account.go +++ b/account/account.go @@ -111,7 +111,7 @@ func setDerivedConfig(account *config.Account) { if pc.VendorExceptions == nil { continue } - pc.VendorExceptionMap = make(map[openrtb_ext.BidderName]struct{}) + pc.VendorExceptionMap = make(map[string]struct{}) for _, v := range pc.VendorExceptions { pc.VendorExceptionMap[v] = struct{}{} } diff --git a/account/account_test.go b/account/account_test.go index 369c2d2c40d..7788f7c430d 100644 --- a/account/account_test.go +++ b/account/account_test.go @@ -122,7 +122,7 @@ func TestGetAccount(t *testing.T) { func TestSetDerivedConfig(t *testing.T) { tests := []struct { description string - purpose1VendorExceptions []openrtb_ext.BidderName + purpose1VendorExceptions []string feature1VendorExceptions []openrtb_ext.BidderName basicEnforcementVendors []string enforceAlgo string @@ -134,11 +134,11 @@ func TestSetDerivedConfig(t *testing.T) { }, { description: "One purpose 1 vendor exception", - purpose1VendorExceptions: []openrtb_ext.BidderName{"appnexus"}, + purpose1VendorExceptions: []string{"appnexus"}, }, { description: "Multiple purpose 1 vendor exceptions", - purpose1VendorExceptions: []openrtb_ext.BidderName{"appnexus", "rubicon"}, + purpose1VendorExceptions: []string{"appnexus", "rubicon"}, }, { description: "Nil feature 1 vendor exceptions", @@ -192,7 +192,7 @@ func TestSetDerivedConfig(t *testing.T) { setDerivedConfig(&account) - purpose1ExceptionMapKeys := make([]openrtb_ext.BidderName, 0) + purpose1ExceptionMapKeys := make([]string, 0) for k := range account.GDPR.Purpose1.VendorExceptionMap { purpose1ExceptionMapKeys = append(purpose1ExceptionMapKeys, k) } diff --git a/config/account.go b/config/account.go index ee131873e35..1ab6a4c0246 100644 --- a/config/account.go +++ b/config/account.go @@ -227,7 +227,7 @@ func (a *AccountGDPR) PurposeEnforcingVendors(purpose consentconstants.Purpose) } // PurposeVendorExceptions returns the vendor exception map for a given purpose. -func (a *AccountGDPR) PurposeVendorExceptions(purpose consentconstants.Purpose) (value map[openrtb_ext.BidderName]struct{}, exists bool) { +func (a *AccountGDPR) PurposeVendorExceptions(purpose consentconstants.Purpose) (value map[string]struct{}, exists bool) { c, exists := a.PurposeConfigs[purpose] if exists && c.VendorExceptionMap != nil { @@ -262,8 +262,8 @@ type AccountGDPRPurpose struct { EnforcePurpose *bool `mapstructure:"enforce_purpose" json:"enforce_purpose,omitempty"` EnforceVendors *bool `mapstructure:"enforce_vendors" json:"enforce_vendors,omitempty"` // Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed - VendorExceptions []openrtb_ext.BidderName `mapstructure:"vendor_exceptions" json:"vendor_exceptions"` - VendorExceptionMap map[openrtb_ext.BidderName]struct{} + VendorExceptions []string `mapstructure:"vendor_exceptions" json:"vendor_exceptions"` + VendorExceptionMap map[string]struct{} } // AccountGDPRSpecialFeature represents account-specific GDPR special feature configuration diff --git a/config/account_test.go b/config/account_test.go index 65e7f3e8716..c529af09f15 100644 --- a/config/account_test.go +++ b/config/account_test.go @@ -476,48 +476,40 @@ func TestPurposeVendorExceptions(t *testing.T) { tests := []struct { description string givePurposeConfigNil bool - givePurpose1ExceptionMap map[openrtb_ext.BidderName]struct{} - givePurpose2ExceptionMap map[openrtb_ext.BidderName]struct{} + givePurpose1ExceptionMap map[string]struct{} + givePurpose2ExceptionMap map[string]struct{} givePurpose consentconstants.Purpose - wantExceptionMap map[openrtb_ext.BidderName]struct{} - wantExceptionMapSet bool + wantExceptionMap map[string]struct{} }{ { description: "Purpose config is nil", givePurposeConfigNil: true, givePurpose: 1, - // wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - wantExceptionMap: nil, - wantExceptionMapSet: false, + wantExceptionMap: nil, }, { - description: "Nil - exception map not defined for purpose", - givePurpose: 1, - // wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - wantExceptionMap: nil, - wantExceptionMapSet: false, + description: "Nil - exception map not defined for purpose", + givePurpose: 1, + wantExceptionMap: nil, }, { description: "Empty - exception map empty for purpose", givePurpose: 1, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - wantExceptionMapSet: true, + givePurpose1ExceptionMap: map[string]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Nonempty - exception map with multiple entries for purpose", givePurpose: 1, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, - wantExceptionMapSet: true, + givePurpose1ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, + wantExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, }, { description: "Nonempty - exception map with multiple entries for different purpose", givePurpose: 2, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, - givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, - wantExceptionMapSet: true, + givePurpose1ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, + givePurpose2ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, + wantExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, }, } @@ -538,7 +530,11 @@ func TestPurposeVendorExceptions(t *testing.T) { value, present := accountGDPR.PurposeVendorExceptions(tt.givePurpose) assert.Equal(t, tt.wantExceptionMap, value, tt.description) - assert.Equal(t, tt.wantExceptionMapSet, present, tt.description) + if tt.wantExceptionMap == nil { + assert.Equal(t, false, present) + } else { + assert.Equal(t, true, present) + } } } diff --git a/config/config.go b/config/config.go index f0e9f5c7fa7..71b6893ce91 100644 --- a/config/config.go +++ b/config/config.go @@ -375,13 +375,13 @@ func (t *TCF2) PurposeEnforcingVendors(purpose consentconstants.Purpose) (enforc // PurposeVendorExceptions returns the vendor exception map for a given purpose if it exists, otherwise it returns // an empty map of vendor exceptions -func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (vendorExceptions map[openrtb_ext.BidderName]struct{}) { +func (t *TCF2) PurposeVendorExceptions(purpose consentconstants.Purpose) (vendorExceptions map[string]struct{}) { c, exists := t.PurposeConfigs[purpose] if exists && c.VendorExceptionMap != nil { return c.VendorExceptionMap } - return make(map[openrtb_ext.BidderName]struct{}, 0) + return make(map[string]struct{}, 0) } // FeatureOneEnforced checks if special feature one is enforced. If it is enforced, PBS will determine whether geo @@ -417,8 +417,8 @@ type TCF2Purpose struct { EnforcePurpose bool `mapstructure:"enforce_purpose"` EnforceVendors bool `mapstructure:"enforce_vendors"` // Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed - VendorExceptions []openrtb_ext.BidderName `mapstructure:"vendor_exceptions"` - VendorExceptionMap map[openrtb_ext.BidderName]struct{} + VendorExceptions []string `mapstructure:"vendor_exceptions"` + VendorExceptionMap map[string]struct{} } type TCF2SpecialFeature struct { @@ -735,13 +735,13 @@ func New(v *viper.Viper, bidderInfos BidderInfos, normalizeBidderName func(strin } } - // To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table with bidders - // located in the VendorExceptions field of the GDPR.TCF2.PurposeX struct defined in this file + // To look for a purpose's vendor exceptions in O(1) time, for each purpose we fill this hash table with bidders/analytics + // adapters located in the VendorExceptions field of the GDPR.TCF2.PurposeX struct defined in this file for _, pc := range c.GDPR.TCF2.PurposeConfigs { - pc.VendorExceptionMap = make(map[openrtb_ext.BidderName]struct{}) + pc.VendorExceptionMap = make(map[string]struct{}) for v := 0; v < len(pc.VendorExceptions); v++ { - bidderName := pc.VendorExceptions[v] - pc.VendorExceptionMap[bidderName] = struct{}{} + adapterName := pc.VendorExceptions[v] + pc.VendorExceptionMap[adapterName] = struct{}{} } } @@ -1064,16 +1064,16 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("gdpr.tcf2.purpose8.enforce_vendors", true) v.SetDefault("gdpr.tcf2.purpose9.enforce_vendors", true) v.SetDefault("gdpr.tcf2.purpose10.enforce_vendors", true) - v.SetDefault("gdpr.tcf2.purpose1.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose2.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose3.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose4.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose5.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose6.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose7.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose8.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose9.vendor_exceptions", []openrtb_ext.BidderName{}) - v.SetDefault("gdpr.tcf2.purpose10.vendor_exceptions", []openrtb_ext.BidderName{}) + v.SetDefault("gdpr.tcf2.purpose1.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose2.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose3.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose4.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose5.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose6.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose7.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose8.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose9.vendor_exceptions", []string{}) + v.SetDefault("gdpr.tcf2.purpose10.vendor_exceptions", []string{}) v.SetDefault("gdpr.amp_exception", false) v.SetDefault("gdpr.eea_countries", []string{"ALA", "AUT", "BEL", "BGR", "HRV", "CYP", "CZE", "DNK", "EST", "FIN", "FRA", "GUF", "DEU", "GIB", "GRC", "GLP", "GGY", "HUN", "ISL", "IRL", "IMN", "ITA", "JEY", "LVA", diff --git a/config/config_test.go b/config/config_test.go index a551c1be66e..e43628be48d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -229,80 +229,80 @@ func TestDefaults(t *testing.T) { EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose2: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose3: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose4: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose5: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose6: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose7: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose8: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose9: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, Purpose10: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: true, - VendorExceptions: []openrtb_ext.BidderName{}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + VendorExceptions: []string{}, + VendorExceptionMap: map[string]struct{}{}, }, SpecialFeature1: TCF2SpecialFeature{ Enforce: true, @@ -654,80 +654,80 @@ func TestFullConfig(t *testing.T) { EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo1a"), openrtb_ext.BidderName("foo1b")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo1a"): {}, openrtb_ext.BidderName("foo1b"): {}}, + VendorExceptions: []string{"foo1a", "foo1b"}, + VendorExceptionMap: map[string]struct{}{"foo1a": {}, "foo1b": {}}, }, Purpose2: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: false, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo2")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo2"): {}}, + VendorExceptions: []string{"foo2"}, + VendorExceptionMap: map[string]struct{}{"foo2": {}}, }, Purpose3: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoBasic, EnforceAlgoID: TCF2BasicEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo3")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo3"): {}}, + VendorExceptions: []string{"foo3"}, + VendorExceptionMap: map[string]struct{}{"foo3": {}}, }, Purpose4: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo4")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo4"): {}}, + VendorExceptions: []string{"foo4"}, + VendorExceptionMap: map[string]struct{}{"foo4": {}}, }, Purpose5: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo5")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo5"): {}}, + VendorExceptions: []string{"foo5"}, + VendorExceptionMap: map[string]struct{}{"foo5": {}}, }, Purpose6: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo6")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo6"): {}}, + VendorExceptions: []string{"foo6"}, + VendorExceptionMap: map[string]struct{}{"foo6": {}}, }, Purpose7: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo7")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo7"): {}}, + VendorExceptions: []string{"foo7"}, + VendorExceptionMap: map[string]struct{}{"foo7": {}}, }, Purpose8: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo8")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo8"): {}}, + VendorExceptions: []string{"foo8"}, + VendorExceptionMap: map[string]struct{}{"foo8": {}}, }, Purpose9: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo9")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo9"): {}}, + VendorExceptions: []string{"foo9"}, + VendorExceptionMap: map[string]struct{}{"foo9": {}}, }, Purpose10: TCF2Purpose{ EnforceAlgo: TCF2EnforceAlgoFull, EnforceAlgoID: TCF2FullEnforcement, EnforcePurpose: true, EnforceVendors: false, - VendorExceptions: []openrtb_ext.BidderName{openrtb_ext.BidderName("foo10")}, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo10"): {}}, + VendorExceptions: []string{"foo10"}, + VendorExceptionMap: map[string]struct{}{"foo10": {}}, }, SpecialFeature1: TCF2SpecialFeature{ Enforce: true, // true by default @@ -1675,40 +1675,40 @@ func TestTCF2PurposeVendorExceptions(t *testing.T) { tests := []struct { description string givePurposeConfigNil bool - givePurpose1ExceptionMap map[openrtb_ext.BidderName]struct{} - givePurpose2ExceptionMap map[openrtb_ext.BidderName]struct{} + givePurpose1ExceptionMap map[string]struct{} + givePurpose2ExceptionMap map[string]struct{} givePurpose consentconstants.Purpose - wantExceptionMap map[openrtb_ext.BidderName]struct{} + wantExceptionMap map[string]struct{} }{ { description: "Purpose config is nil", givePurposeConfigNil: true, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Nil - exception map not defined for purpose", givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Empty - exception map empty for purpose", givePurpose: 1, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + givePurpose1ExceptionMap: map[string]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Nonempty - exception map with multiple entries for purpose", givePurpose: 1, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, + givePurpose1ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, + wantExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, }, { description: "Nonempty - exception map with multiple entries for different purpose", givePurpose: 2, - givePurpose1ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, - givePurpose2ExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, + givePurpose1ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "index": {}}, + givePurpose2ExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, + wantExceptionMap: map[string]struct{}{"rubicon": {}, "appnexus": {}, "openx": {}}, }, } diff --git a/gdpr/aggregated_config.go b/gdpr/aggregated_config.go index 4bd1533de0f..9ac03143a1c 100644 --- a/gdpr/aggregated_config.go +++ b/gdpr/aggregated_config.go @@ -17,7 +17,7 @@ type TCF2ConfigReader interface { PurposeEnforced(consentconstants.Purpose) bool PurposeEnforcementAlgo(consentconstants.Purpose) config.TCF2EnforcementAlgo PurposeEnforcingVendors(consentconstants.Purpose) bool - PurposeVendorExceptions(consentconstants.Purpose) map[openrtb_ext.BidderName]struct{} + PurposeVendorExceptions(consentconstants.Purpose) map[string]struct{} PurposeOneTreatmentEnabled() bool PurposeOneTreatmentAccessAllowed() bool } @@ -85,9 +85,9 @@ func (tc *tcf2Config) PurposeEnforcingVendors(purpose consentconstants.Purpose) } // PurposeVendorExceptions returns the vendor exception map for the specified purpose if it exists for the account; -// otherwise it returns a nil map. If a bidder is a vendor exception, the GDPR full enforcement algorithm will +// otherwise it returns a nil map. If a bidder/analytics adapter is a vendor exception, the GDPR full enforcement algorithm will // bypass the legal basis calculation assuming the request is valid and there isn't a "deny all" publisher restriction -func (tc *tcf2Config) PurposeVendorExceptions(purpose consentconstants.Purpose) map[openrtb_ext.BidderName]struct{} { +func (tc *tcf2Config) PurposeVendorExceptions(purpose consentconstants.Purpose) map[string]struct{} { if value, exists := tc.AccountConfig.PurposeVendorExceptions(purpose); exists { return value } diff --git a/gdpr/aggregated_config_test.go b/gdpr/aggregated_config_test.go index 54d1c901853..a24e9eff1b1 100644 --- a/gdpr/aggregated_config_test.go +++ b/gdpr/aggregated_config_test.go @@ -317,54 +317,54 @@ func TestPurposeEnforcingVendors(t *testing.T) { func TestPurposeVendorExceptions(t *testing.T) { tests := []struct { description string - givePurpose1HostExceptionMap map[openrtb_ext.BidderName]struct{} - givePurpose1AccountExceptionMap map[openrtb_ext.BidderName]struct{} - givePurpose2HostExceptionMap map[openrtb_ext.BidderName]struct{} - givePurpose2AccountExceptionMap map[openrtb_ext.BidderName]struct{} + givePurpose1HostExceptionMap map[string]struct{} + givePurpose1AccountExceptionMap map[string]struct{} + givePurpose2HostExceptionMap map[string]struct{} + givePurpose2AccountExceptionMap map[string]struct{} givePurpose consentconstants.Purpose - wantExceptionMap map[openrtb_ext.BidderName]struct{} + wantExceptionMap map[string]struct{} }{ { description: "Purpose 1 exception list set at account level - use empty account list", - givePurpose1HostExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - givePurpose1AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + givePurpose1HostExceptionMap: map[string]struct{}{}, + givePurpose1AccountExceptionMap: map[string]struct{}{}, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Purpose 1 exception list set at account level - use nonempty account list", - givePurpose1HostExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - givePurpose1AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + givePurpose1HostExceptionMap: map[string]struct{}{}, + givePurpose1AccountExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + wantExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, }, { description: "Purpose 1 exception list not set at account level - use empty host list", - givePurpose1HostExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + givePurpose1HostExceptionMap: map[string]struct{}{}, givePurpose1AccountExceptionMap: nil, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Purpose 1 exception list not set at account level - use nonempty host list", - givePurpose1HostExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + givePurpose1HostExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, givePurpose1AccountExceptionMap: nil, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + wantExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, }, { description: "Purpose 1 exception list not set at account level or host level", givePurpose1HostExceptionMap: nil, givePurpose1AccountExceptionMap: nil, givePurpose: 1, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + wantExceptionMap: map[string]struct{}{}, }, { description: "Some other purpose exception list set at account level", - givePurpose2HostExceptionMap: map[openrtb_ext.BidderName]struct{}{}, - givePurpose2AccountExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + givePurpose2HostExceptionMap: map[string]struct{}{}, + givePurpose2AccountExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, givePurpose: 2, - wantExceptionMap: map[openrtb_ext.BidderName]struct{}{"appnexus": {}, "rubicon": {}}, + wantExceptionMap: map[string]struct{}{"appnexus": {}, "rubicon": {}}, }, } diff --git a/gdpr/basic_enforcement.go b/gdpr/basic_enforcement.go index 322bb30986f..2bb11857a16 100644 --- a/gdpr/basic_enforcement.go +++ b/gdpr/basic_enforcement.go @@ -2,10 +2,9 @@ package gdpr import ( tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) -// BasicEnforcement determines if legal basis is satisfied for a given purpose and bidder using +// BasicEnforcement determines if legal basis is satisfied for a given purpose and bidder/analytics adapter using // the TCF2 basic enforcement algorithm. The algorithm is a high-level mode of consent confirmation // that looks for a good-faith indication that the user has provided consent or legal basis signals // necessary to perform a privacy-protected activity. The algorithm does not involve the GVL. @@ -14,21 +13,21 @@ type BasicEnforcement struct { cfg purposeConfig } -// LegalBasis determines if legal basis is satisfied for a given purpose and bidder based on user consent +// LegalBasis determines if legal basis is satisfied for a given purpose and bidder/analytics adapter based on user consent // and legal basis signals. -func (be *BasicEnforcement) LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool { +func (be *BasicEnforcement) LegalBasis(vendorInfo VendorInfo, name string, consent tcf2.ConsentMetadata, overrides Overrides) bool { enforcePurpose, enforceVendors := be.applyEnforceOverrides(overrides) if !enforcePurpose && !enforceVendors { return true } - if be.cfg.vendorException(bidder) && !overrides.blockVendorExceptions { + if be.cfg.vendorException(name) && !overrides.blockVendorExceptions { return true } - if !enforcePurpose && be.cfg.basicEnforcementVendor(bidder) { + if !enforcePurpose && be.cfg.basicEnforcementVendor(name) { return true } - if enforcePurpose && consent.PurposeAllowed(be.cfg.PurposeID) && be.cfg.basicEnforcementVendor(bidder) { + if enforcePurpose && consent.PurposeAllowed(be.cfg.PurposeID) && be.cfg.basicEnforcementVendor(name) { return true } if enforcePurpose && consent.PurposeLITransparency(be.cfg.PurposeID) && overrides.allowLITransparency { diff --git a/gdpr/basic_enforcement_test.go b/gdpr/basic_enforcement_test.go index 06472618a83..eb22f59bc0b 100644 --- a/gdpr/basic_enforcement_test.go +++ b/gdpr/basic_enforcement_test.go @@ -12,7 +12,10 @@ import ( ) func TestBasicLegalBasis(t *testing.T) { - appnexusID := uint16(32) + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + appnexusID = uint16(32) + ) noConsents := "CPerMsAPerMsAAAAAAENCfCAAAAAAAAAAAAAAAAAAAAA" purpose2Consent := "CPerMsAPerMsAAAAAAENCfCAAEAAAAAAAAAAAAAAAAAA" @@ -149,7 +152,7 @@ func TestBasicLegalBasis(t *testing.T) { PurposeID: consentconstants.Purpose(2), EnforcePurpose: false, EnforceVendors: true, - BasicEnforcementVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}}, + BasicEnforcementVendorsMap: map[string]struct{}{appnexus: {}}, }, wantResult: true, }, @@ -170,7 +173,7 @@ func TestBasicLegalBasis(t *testing.T) { PurposeID: consentconstants.Purpose(2), EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, wantResult: true, }, @@ -181,7 +184,7 @@ func TestBasicLegalBasis(t *testing.T) { PurposeID: consentconstants.Purpose(2), EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, overrides: Overrides{blockVendorExceptions: true}, wantResult: false, @@ -203,7 +206,7 @@ func TestBasicLegalBasis(t *testing.T) { PurposeID: consentconstants.Purpose(2), EnforcePurpose: true, EnforceVendors: true, - BasicEnforcementVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}}, + BasicEnforcementVendorsMap: map[string]struct{}{appnexus: {}}, }, wantResult: true, }, @@ -233,7 +236,7 @@ func TestBasicLegalBasis(t *testing.T) { enforcer := BasicEnforcement{cfg: tt.config} vendorInfo := VendorInfo{vendorID: appnexusID, vendor: nil} - result := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + result := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantResult, result, tt.description) } diff --git a/gdpr/full_enforcement.go b/gdpr/full_enforcement.go index c872e13e454..9c2b5221385 100644 --- a/gdpr/full_enforcement.go +++ b/gdpr/full_enforcement.go @@ -2,7 +2,6 @@ package gdpr import ( tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) const ( @@ -11,7 +10,7 @@ const ( pubRestrictRequireLegitInterest = 2 ) -// FullEnforcement determines if legal basis is satisfied for a given purpose and bidder using +// FullEnforcement determines if legal basis is satisfied for a given purpose and bidde/analytics adapterr using // the TCF2 full enforcement algorithm. The algorithm is a detailed confirmation that reads the // GVL, interprets the consent string and performs legal basis analysis necessary to perform a // privacy-protected activity. @@ -20,9 +19,9 @@ type FullEnforcement struct { cfg purposeConfig } -// LegalBasis determines if legal basis is satisfied for a given purpose and bidder based on the +// LegalBasis determines if legal basis is satisfied for a given purpose and bidder/analytics adapter based on the // vendor claims in the GVL, publisher restrictions and user consent. -func (fe *FullEnforcement) LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool { +func (fe *FullEnforcement) LegalBasis(vendorInfo VendorInfo, name string, consent tcf2.ConsentMetadata, overrides Overrides) bool { enforcePurpose, enforceVendors := fe.applyEnforceOverrides(overrides) if consent.CheckPubRestriction(uint8(fe.cfg.PurposeID), pubRestrictNotAllowed, vendorInfo.vendorID) { @@ -31,7 +30,7 @@ func (fe *FullEnforcement) LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext. if !enforcePurpose && !enforceVendors { return true } - if fe.cfg.vendorException(bidder) && !overrides.blockVendorExceptions { + if fe.cfg.vendorException(name) && !overrides.blockVendorExceptions { return true } diff --git a/gdpr/full_enforcement_test.go b/gdpr/full_enforcement_test.go index dac9d7ef76a..32ba2e50289 100644 --- a/gdpr/full_enforcement_test.go +++ b/gdpr/full_enforcement_test.go @@ -15,7 +15,10 @@ import ( ) func TestLegalBasisWithPubRestrictionAllowNone(t *testing.T) { - appnexusID := uint16(32) + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + appnexusID = uint16(32) + ) NoConsentsWithP1P2P3V32RestrictionAllowNone := "CPfMKEAPfMKEAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGCAAgAgCAAQAQBgAIAIAAAA" P1P2P3PurposeConsentAndV32VendorConsentWithP1P2P3V32RestrictionAllowNone := "CPfMKEAPfMKEAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAEAIAAAAAAAGCAAgAgCAAQAQBgAIAIAAAA" @@ -44,7 +47,7 @@ func TestLegalBasisWithPubRestrictionAllowNone(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consent: NoConsentsWithP1P2P3V32RestrictionAllowNone, wantConsentPurposeResult: false, @@ -80,21 +83,24 @@ func TestLegalBasisWithPubRestrictionAllowNone(t *testing.T) { enforcer := FullEnforcement{cfg: tt.config} enforcer.cfg.PurposeID = consentconstants.Purpose(1) - consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{}) + consentPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, Overrides{}) assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(2) - LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{}) + LIPurposeresult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, Overrides{}) assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(3) - flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{}) + flexPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, Overrides{}) assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose") } } func TestLegalBasisWithNoPubRestrictionsAndWithPubRestrictionAllowAll(t *testing.T) { - appnexusID := uint16(32) + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + appnexusID = uint16(32) + ) NoConsents := "CPfCRQAPfCRQAAAAAAENCgCAAAAAAAAAAAAAAAAAAAAA" P1P2P3PurposeConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAAAAAAAA" @@ -325,7 +331,7 @@ func TestLegalBasisWithNoPubRestrictionsAndWithPubRestrictionAllowAll(t *testing config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consentNoPubRestriction: NoConsents, consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll, @@ -338,7 +344,7 @@ func TestLegalBasisWithNoPubRestrictionsAndWithPubRestrictionAllowAll(t *testing config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consentNoPubRestriction: NoConsents, consentWithPubRestriction: NoConsentsWithP1P2P3V32RestrictionAllowAll, @@ -370,22 +376,25 @@ func TestLegalBasisWithNoPubRestrictionsAndWithPubRestrictionAllowAll(t *testing enforcer := FullEnforcement{cfg: tt.config} enforcer.cfg.PurposeID = consentconstants.Purpose(1) - consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + consentPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose -- consent string %d of %d", i+1, len(consents)) enforcer.cfg.PurposeID = consentconstants.Purpose(2) - LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + LIPurposeresult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose -- consent string %d of %d", i+1, len(consents)) enforcer.cfg.PurposeID = consentconstants.Purpose(3) - flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + flexPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose -- consent string %d of %d", i+1, len(consents)) } } } func TestLegalBasisWithPubRestrictionRequireConsent(t *testing.T) { - appnexusID := uint16(32) + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + appnexusID = uint16(32) + ) NoConsentsWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA" P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireConsent := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAAAIAAAAAAAGCgAgAgCQAQAQBoAIAIAAAA" @@ -590,7 +599,7 @@ func TestLegalBasisWithPubRestrictionRequireConsent(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consent: NoConsentsWithP1P2P3V32RestrictionRequireConsent, wantConsentPurposeResult: true, @@ -602,7 +611,7 @@ func TestLegalBasisWithPubRestrictionRequireConsent(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consent: NoConsentsWithP1P2P3V32RestrictionRequireConsent, overrides: Overrides{blockVendorExceptions: true}, @@ -628,21 +637,24 @@ func TestLegalBasisWithPubRestrictionRequireConsent(t *testing.T) { enforcer := FullEnforcement{cfg: tt.config} enforcer.cfg.PurposeID = consentconstants.Purpose(1) - consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + consentPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(2) - LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + LIPurposeresult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(3) - flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + flexPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose") } } func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) { - appnexusID := uint16(32) + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + appnexusID = uint16(32) + ) NoConsentsWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAAAAAAAAAAAAAQAAAAAAAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA" P1P2P3PurposeConsentWithP1P2P3V32RestrictionRequireLI := "CPfFkMAPfFkMAAAAAAENCgCAAOAAAAAAAAAAAQAAAAAAAIAAAAAAAGDAAgAgCgAQAQBwAIAIAAAA" @@ -847,7 +859,7 @@ func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consent: NoConsentsWithP1P2P3V32RestrictionRequireLI, wantConsentPurposeResult: true, @@ -859,7 +871,7 @@ func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: true, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, consent: NoConsentsWithP1P2P3V32RestrictionRequireLI, overrides: Overrides{blockVendorExceptions: true}, @@ -885,20 +897,21 @@ func TestLegalBasisWithPubRestrictionRequireLI(t *testing.T) { enforcer := FullEnforcement{cfg: tt.config} enforcer.cfg.PurposeID = consentconstants.Purpose(1) - consentPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + consentPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantConsentPurposeResult, consentPurposeResult, tt.description+" -- GVL consent purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(2) - LIPurposeresult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + LIPurposeresult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantLIPurposeResult, LIPurposeresult, tt.description+" -- GVL LI purpose") enforcer.cfg.PurposeID = consentconstants.Purpose(3) - flexPurposeResult := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, tt.overrides) + flexPurposeResult := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, tt.overrides) assert.Equal(t, tt.wantFlexPurposeResult, flexPurposeResult, tt.description+" -- GVL flex purpose") } } func TestLegalBasisWithoutVendor(t *testing.T) { + appnexus := string(openrtb_ext.BidderAppnexus) P1P2P3PurposeConsent := "CPfCRQAPfCRQAAAAAAENCgCAAOAAAAAAAAAAAAAAAAAA" tests := []struct { name string @@ -918,7 +931,7 @@ func TestLegalBasisWithoutVendor(t *testing.T) { config: purposeConfig{ EnforcePurpose: true, EnforceVendors: false, - VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + VendorExceptionMap: map[string]struct{}{appnexus: {}}, }, wantResult: true, }, @@ -952,7 +965,7 @@ func TestLegalBasisWithoutVendor(t *testing.T) { enforcer := FullEnforcement{cfg: tt.config} enforcer.cfg.PurposeID = consentconstants.Purpose(3) - result := enforcer.LegalBasis(vendorInfo, openrtb_ext.BidderAppnexus, consentMeta, Overrides{}) + result := enforcer.LegalBasis(vendorInfo, appnexus, consentMeta, Overrides{}) assert.Equal(t, tt.wantResult, result) }) } diff --git a/gdpr/gdpr_test.go b/gdpr/gdpr_test.go index 9604e24f4f0..3729eda3d5b 100644 --- a/gdpr/gdpr_test.go +++ b/gdpr/gdpr_test.go @@ -60,6 +60,6 @@ type fakePurposeEnforcerBuilder struct { purposeEnforcer PurposeEnforcer } -func (fpeb fakePurposeEnforcerBuilder) Builder(consentconstants.Purpose, openrtb_ext.BidderName) PurposeEnforcer { +func (fpeb fakePurposeEnforcerBuilder) Builder(consentconstants.Purpose, string) PurposeEnforcer { return fpeb.purposeEnforcer } diff --git a/gdpr/impl.go b/gdpr/impl.go index fd3ad2b2dd9..614c06d9a6a 100644 --- a/gdpr/impl.go +++ b/gdpr/impl.go @@ -48,7 +48,7 @@ func (p *permissionsImpl) BidderSyncAllowed(ctx context.Context, bidder openrtb_ id, ok := p.vendorIDs[bidder] if ok { vendorExceptions := p.cfg.PurposeVendorExceptions(consentconstants.Purpose(1)) - _, vendorException := vendorExceptions[bidder] + _, vendorException := vendorExceptions[string(bidder)] return p.allowSync(ctx, id, bidder, vendorException) } @@ -138,9 +138,9 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, bidder } purpose := consentconstants.Purpose(1) - enforcer := p.purposeEnforcerBuilder(purpose, bidder) + enforcer := p.purposeEnforcerBuilder(purpose, string(bidder)) - if enforcer.LegalBasis(vendorInfo, bidder, pc.consentMeta, Overrides{blockVendorExceptions: !vendorException}) { + if enforcer.LegalBasis(vendorInfo, string(bidder), pc.consentMeta, Overrides{blockVendorExceptions: !vendorException}) { return true, nil } return false, nil @@ -149,13 +149,13 @@ func (p *permissionsImpl) allowSync(ctx context.Context, vendorID uint16, bidder // allowBidRequest computes legal basis for a given bidder using the enforcement algorithms selected // by the purpose enforcer builder func (p *permissionsImpl) allowBidRequest(bidder openrtb_ext.BidderName, consentMeta tcf2.ConsentMetadata, vendorInfo VendorInfo) bool { - enforcer := p.purposeEnforcerBuilder(consentconstants.Purpose(2), bidder) + enforcer := p.purposeEnforcerBuilder(consentconstants.Purpose(2), string(bidder)) overrides := Overrides{} if _, ok := enforcer.(*BasicEnforcement); ok { overrides.allowLITransparency = true } - return enforcer.LegalBasis(vendorInfo, bidder, consentMeta, overrides) + return enforcer.LegalBasis(vendorInfo, string(bidder), consentMeta, overrides) } // allowGeo computes legal basis for a given bidder using the configs, consent and GVL pertaining to @@ -180,13 +180,13 @@ func (p *permissionsImpl) allowGeo(bidder openrtb_ext.BidderName, consentMeta tc func (p *permissionsImpl) allowID(bidder openrtb_ext.BidderName, consentMeta tcf2.ConsentMetadata, vendorInfo VendorInfo) bool { for i := 2; i <= 10; i++ { purpose := consentconstants.Purpose(i) - enforcer := p.purposeEnforcerBuilder(purpose, bidder) + enforcer := p.purposeEnforcerBuilder(purpose, string(bidder)) overrides := Overrides{enforcePurpose: true, enforceVendors: true} if _, ok := enforcer.(*BasicEnforcement); ok && purpose == consentconstants.Purpose(2) { overrides.allowLITransparency = true } - if enforcer.LegalBasis(vendorInfo, bidder, consentMeta, overrides) { + if enforcer.LegalBasis(vendorInfo, string(bidder), consentMeta, overrides) { return true } } diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go index fc3d69d9c57..64fa4434d4d 100644 --- a/gdpr/impl_test.go +++ b/gdpr/impl_test.go @@ -350,7 +350,7 @@ func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { tests := []struct { name string enforceAlgoID config.TCF2EnforcementAlgo - vendorExceptions map[openrtb_ext.BidderName]struct{} + vendorExceptions map[string]struct{} basicEnforcementVendors map[string]struct{} consent string allowBidRequest bool @@ -364,7 +364,7 @@ func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { { name: "full_enforcement_vendor_exception_user_consents_to_purpose_2", enforceAlgoID: config.TCF2FullEnforcement, - vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}}, + vendorExceptions: map[string]struct{}{string(bidderWithoutGVLID): {}}, consent: purpose2Consent, allowBidRequest: true, passID: true, @@ -375,7 +375,7 @@ func TestAllowActivitiesBidderWithoutGVLID(t *testing.T) { }, { name: "basic_enforcement_vendor_exception_user_consents_to_purpose_2", - vendorExceptions: map[openrtb_ext.BidderName]struct{}{bidderWithoutGVLID: {}}, + vendorExceptions: map[string]struct{}{string(bidderWithoutGVLID): {}}, consent: purpose2Consent, allowBidRequest: true, passID: true, @@ -1110,12 +1110,13 @@ func TestAllowActivitiesBidRequests(t *testing.T) { } func TestAllowActivitiesVendorException(t *testing.T) { + appnexus := string(openrtb_ext.BidderAppnexus) noPurposeOrVendorConsentAndPubRestrictsP2 := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAACEAAgAgAA" noPurposeOrVendorConsentAndPubRestrictsNone := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAAA" testDefs := []struct { description string - p2VendorExceptionMap map[openrtb_ext.BidderName]struct{} + p2VendorExceptionMap map[string]struct{} sf1VendorExceptionMap map[openrtb_ext.BidderName]struct{} bidder openrtb_ext.BidderName consent string @@ -1126,7 +1127,7 @@ func TestAllowActivitiesVendorException(t *testing.T) { }{ { description: "Bid/ID blocked by publisher - p2 enabled with p2 vendor exception, pub restricts p2 for vendor", - p2VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + p2VendorExceptionMap: map[string]struct{}{appnexus: {}}, bidder: openrtb_ext.BidderAppnexus, bidderCoreName: openrtb_ext.BidderAppnexus, consent: noPurposeOrVendorConsentAndPubRestrictsP2, @@ -1136,7 +1137,7 @@ func TestAllowActivitiesVendorException(t *testing.T) { }, { description: "Bid/ID allowed by vendor exception - p2 enabled with p2 vendor exception, pub restricts none", - p2VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + p2VendorExceptionMap: map[string]struct{}{appnexus: {}}, sf1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, bidder: openrtb_ext.BidderAppnexus, bidderCoreName: openrtb_ext.BidderAppnexus, @@ -1147,7 +1148,7 @@ func TestAllowActivitiesVendorException(t *testing.T) { }, { description: "Geo blocked - sf1 enabled but no consent", - p2VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + p2VendorExceptionMap: map[string]struct{}{}, sf1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, bidder: openrtb_ext.BidderAppnexus, bidderCoreName: openrtb_ext.BidderAppnexus, @@ -1158,7 +1159,7 @@ func TestAllowActivitiesVendorException(t *testing.T) { }, { description: "Geo allowed by vendor exception - sf1 enabled with sf1 vendor exception", - p2VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + p2VendorExceptionMap: map[string]struct{}{}, sf1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, bidder: openrtb_ext.BidderAppnexus, bidderCoreName: openrtb_ext.BidderAppnexus, @@ -1204,33 +1205,34 @@ func TestAllowActivitiesVendorException(t *testing.T) { } func TestBidderSyncAllowedVendorException(t *testing.T) { + appnexus := string(openrtb_ext.BidderAppnexus) noPurposeOrVendorConsentAndPubRestrictsP1 := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAAQAAAAAAAAAAIIACACA" noPurposeOrVendorConsentAndPubRestrictsNone := "CPF_61ePF_61eFxAAAENAiCAAAAAAAAAAAAAACEAAAAA" testDefs := []struct { description string - p1VendorExceptionMap map[openrtb_ext.BidderName]struct{} + p1VendorExceptionMap map[string]struct{} bidder openrtb_ext.BidderName consent string allowSync bool }{ { description: "Sync blocked by no consent - p1 enabled, no p1 vendor exception, pub restricts none", - p1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + p1VendorExceptionMap: map[string]struct{}{}, bidder: openrtb_ext.BidderAppnexus, consent: noPurposeOrVendorConsentAndPubRestrictsNone, allowSync: false, }, { description: "Sync blocked by publisher - p1 enabled with p1 vendor exception, pub restricts p1 for vendor", - p1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + p1VendorExceptionMap: map[string]struct{}{appnexus: {}}, bidder: openrtb_ext.BidderAppnexus, consent: noPurposeOrVendorConsentAndPubRestrictsP1, allowSync: false, }, { description: "Sync allowed by vendor exception - p1 enabled with p1 vendor exception, pub restricts none", - p1VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderAppnexus: {}}, + p1VendorExceptionMap: map[string]struct{}{appnexus: {}}, bidder: openrtb_ext.BidderAppnexus, consent: noPurposeOrVendorConsentAndPubRestrictsNone, allowSync: true, diff --git a/gdpr/purpose_config.go b/gdpr/purpose_config.go index 09edef94384..ff3a92300ff 100644 --- a/gdpr/purpose_config.go +++ b/gdpr/purpose_config.go @@ -3,7 +3,6 @@ package gdpr import ( "github.com/prebid/go-gdpr/consentconstants" "github.com/prebid/prebid-server/v2/config" - "github.com/prebid/prebid-server/v2/openrtb_ext" ) // purposeConfig represents all of the config info selected from the host and account configs for @@ -13,26 +12,26 @@ type purposeConfig struct { EnforceAlgo config.TCF2EnforcementAlgo EnforcePurpose bool EnforceVendors bool - VendorExceptionMap map[openrtb_ext.BidderName]struct{} + VendorExceptionMap map[string]struct{} BasicEnforcementVendorsMap map[string]struct{} } -// basicEnforcementVendor returns true if a given bidder is configured as a basic enforcement vendor +// basicEnforcementVendor returns true if a given bidder/analytics adapter is configured as a basic enforcement vendor // for the purpose -func (pc *purposeConfig) basicEnforcementVendor(bidder openrtb_ext.BidderName) bool { +func (pc *purposeConfig) basicEnforcementVendor(name string) bool { if pc.BasicEnforcementVendorsMap == nil { return false } - _, found := pc.BasicEnforcementVendorsMap[string(bidder)] + _, found := pc.BasicEnforcementVendorsMap[name] return found } -// vendorException returns true if a given bidder is configured as a vendor exception +// vendorException returns true if a given bidder/analytics adapter is configured as a vendor exception // for the purpose -func (pc *purposeConfig) vendorException(bidder openrtb_ext.BidderName) bool { +func (pc *purposeConfig) vendorException(name string) bool { if pc.VendorExceptionMap == nil { return false } - _, found := pc.VendorExceptionMap[bidder] + _, found := pc.VendorExceptionMap[name] return found } diff --git a/gdpr/purpose_config_test.go b/gdpr/purpose_config_test.go index 4837b62a2aa..c2bd6f56d8f 100644 --- a/gdpr/purpose_config_test.go +++ b/gdpr/purpose_config_test.go @@ -9,59 +9,66 @@ import ( ) func TestPurposeConfigBasicEnforcementVendor(t *testing.T) { + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + ix = string(openrtb_ext.BidderIx) + pubmatic = string(openrtb_ext.BidderPubmatic) + rubicon = string(openrtb_ext.BidderRubicon) + ) + tests := []struct { description string giveBasicVendors map[string]struct{} - giveBidder openrtb_ext.BidderName + giveBidder string wantFound bool }{ { description: "vendor map is nil", giveBasicVendors: nil, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor map is empty", giveBasicVendors: map[string]struct{}{}, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor map has one bidders - bidder not found", giveBasicVendors: map[string]struct{}{ - string(openrtb_ext.BidderPubmatic): {}, + pubmatic: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor map has one bidders - bidder found", giveBasicVendors: map[string]struct{}{ - string(openrtb_ext.BidderAppnexus): {}, + appnexus: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: true, }, { description: "vendor map has many bidderss - bidder not found", giveBasicVendors: map[string]struct{}{ - string(openrtb_ext.BidderIx): {}, - string(openrtb_ext.BidderPubmatic): {}, - string(openrtb_ext.BidderRubicon): {}, + ix: {}, + pubmatic: {}, + rubicon: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor map has many bidderss - bidder found", giveBasicVendors: map[string]struct{}{ - string(openrtb_ext.BidderIx): {}, - string(openrtb_ext.BidderPubmatic): {}, - string(openrtb_ext.BidderAppnexus): {}, - string(openrtb_ext.BidderRubicon): {}, + ix: {}, + pubmatic: {}, + appnexus: {}, + rubicon: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: true, }, } @@ -77,59 +84,66 @@ func TestPurposeConfigBasicEnforcementVendor(t *testing.T) { } func TestPurposeConfigVendorException(t *testing.T) { + var ( + appnexus = string(openrtb_ext.BidderAppnexus) + ix = string(openrtb_ext.BidderIx) + pubmatic = string(openrtb_ext.BidderPubmatic) + rubicon = string(openrtb_ext.BidderRubicon) + ) + tests := []struct { description string - giveExceptions map[openrtb_ext.BidderName]struct{} - giveBidder openrtb_ext.BidderName + giveExceptions map[string]struct{} + giveBidder string wantFound bool }{ { description: "vendor exception map is nil", giveExceptions: nil, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor exception map is empty", - giveExceptions: map[openrtb_ext.BidderName]struct{}{}, - giveBidder: openrtb_ext.BidderAppnexus, + giveExceptions: map[string]struct{}{}, + giveBidder: appnexus, wantFound: false, }, { description: "vendor exception map has one bidders - bidder not found", - giveExceptions: map[openrtb_ext.BidderName]struct{}{ - openrtb_ext.BidderPubmatic: {}, + giveExceptions: map[string]struct{}{ + pubmatic: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor exception map has one bidders - bidder found", - giveExceptions: map[openrtb_ext.BidderName]struct{}{ - openrtb_ext.BidderAppnexus: {}, + giveExceptions: map[string]struct{}{ + appnexus: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: true, }, { description: "vendor exception map has many bidderss - bidder not found", - giveExceptions: map[openrtb_ext.BidderName]struct{}{ - openrtb_ext.BidderIx: {}, - openrtb_ext.BidderPubmatic: {}, - openrtb_ext.BidderRubicon: {}, + giveExceptions: map[string]struct{}{ + ix: {}, + pubmatic: {}, + rubicon: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: false, }, { description: "vendor exception map has many bidderss - bidder found", - giveExceptions: map[openrtb_ext.BidderName]struct{}{ - openrtb_ext.BidderIx: {}, - openrtb_ext.BidderPubmatic: {}, - openrtb_ext.BidderAppnexus: {}, - openrtb_ext.BidderRubicon: {}, + giveExceptions: map[string]struct{}{ + ix: {}, + pubmatic: {}, + appnexus: {}, + rubicon: {}, }, - giveBidder: openrtb_ext.BidderAppnexus, + giveBidder: appnexus, wantFound: true, }, } diff --git a/gdpr/purpose_enforcer.go b/gdpr/purpose_enforcer.go index 4a138e76cf8..ceef0e5f561 100644 --- a/gdpr/purpose_enforcer.go +++ b/gdpr/purpose_enforcer.go @@ -10,11 +10,11 @@ import ( // PurposeEnforcer represents the enforcement strategy for determining if legal basis is achieved for a purpose type PurposeEnforcer interface { - LegalBasis(vendorInfo VendorInfo, bidder openrtb_ext.BidderName, consent tcf2.ConsentMetadata, overrides Overrides) bool + LegalBasis(vendorInfo VendorInfo, name string, consent tcf2.ConsentMetadata, overrides Overrides) bool } // PurposeEnforcerBuilder generates an instance of PurposeEnforcer for a given purpose and bidder -type PurposeEnforcerBuilder func(p consentconstants.Purpose, bidder openrtb_ext.BidderName) PurposeEnforcer +type PurposeEnforcerBuilder func(p consentconstants.Purpose, name string) PurposeEnforcer // Overrides specifies enforcement algorithm rule adjustments type Overrides struct { @@ -45,7 +45,7 @@ type PurposeEnforcers struct { func NewPurposeEnforcerBuilder(cfg TCF2ConfigReader) PurposeEnforcerBuilder { cachedEnforcers := make([]PurposeEnforcers, 10) - return func(purpose consentconstants.Purpose, bidder openrtb_ext.BidderName) PurposeEnforcer { + return func(purpose consentconstants.Purpose, name string) PurposeEnforcer { index := purpose - 1 var basicEnforcementVendor bool @@ -53,7 +53,7 @@ func NewPurposeEnforcerBuilder(cfg TCF2ConfigReader) PurposeEnforcerBuilder { basicEnforcementVendor = false } else { basicEnforcementVendors := cfg.BasicEnforcementVendors() - _, basicEnforcementVendor = basicEnforcementVendors[string(bidder)] + _, basicEnforcementVendor = basicEnforcementVendors[name] } enforceAlgo := cfg.PurposeEnforcementAlgo(purpose) diff --git a/gdpr/purpose_enforcer_test.go b/gdpr/purpose_enforcer_test.go index ed1176b12f3..fa0605702a0 100644 --- a/gdpr/purpose_enforcer_test.go +++ b/gdpr/purpose_enforcer_test.go @@ -11,15 +11,17 @@ import ( ) func TestNewPurposeEnforcerBuilder(t *testing.T) { + appnexus := string(openrtb_ext.BidderAppnexus) + tests := []struct { description string enforceAlgo config.TCF2EnforcementAlgo enforcePurpose bool enforceVendors bool basicVendorsMap map[string]struct{} - vendorExceptionMap map[openrtb_ext.BidderName]struct{} + vendorExceptionMap map[string]struct{} purpose consentconstants.Purpose - bidder openrtb_ext.BidderName + bidder string wantType PurposeEnforcer }{ { @@ -28,9 +30,9 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforcePurpose: true, enforceVendors: true, basicVendorsMap: map[string]struct{}{}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(1), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &FullEnforcement{}, }, { @@ -38,10 +40,10 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforceAlgo: config.TCF2FullEnforcement, enforcePurpose: true, enforceVendors: true, - basicVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + basicVendorsMap: map[string]struct{}{appnexus: {}}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(1), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &FullEnforcement{}, }, { @@ -50,9 +52,9 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforcePurpose: true, enforceVendors: true, basicVendorsMap: map[string]struct{}{}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(1), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &BasicEnforcement{}, }, { @@ -61,9 +63,9 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforcePurpose: true, enforceVendors: true, basicVendorsMap: map[string]struct{}{}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(2), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &FullEnforcement{}, }, { @@ -71,10 +73,10 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforceAlgo: config.TCF2FullEnforcement, enforcePurpose: true, enforceVendors: true, - basicVendorsMap: map[string]struct{}{string(openrtb_ext.BidderAppnexus): {}}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + basicVendorsMap: map[string]struct{}{appnexus: {}}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(2), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &BasicEnforcement{}, }, { @@ -83,9 +85,9 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { enforcePurpose: true, enforceVendors: true, basicVendorsMap: map[string]struct{}{}, - vendorExceptionMap: map[openrtb_ext.BidderName]struct{}{}, + vendorExceptionMap: map[string]struct{}{}, purpose: consentconstants.Purpose(2), - bidder: openrtb_ext.BidderAppnexus, + bidder: appnexus, wantType: &BasicEnforcement{}, }, } @@ -136,13 +138,13 @@ func TestNewPurposeEnforcerBuilder(t *testing.T) { func TestNewPurposeEnforcerBuilderCaching(t *testing.T) { - bidder1 := openrtb_ext.BidderAppnexus + bidder1 := string(openrtb_ext.BidderAppnexus) bidder1Enforcers := make([]PurposeEnforcer, 11) - bidder2 := openrtb_ext.BidderIx + bidder2 := string(openrtb_ext.BidderIx) bidder2Enforcers := make([]PurposeEnforcer, 11) - bidder3 := openrtb_ext.BidderPubmatic + bidder3 := string(openrtb_ext.BidderPubmatic) bidder3Enforcers := make([]PurposeEnforcer, 11) - bidder4 := openrtb_ext.BidderRubicon + bidder4 := string(openrtb_ext.BidderRubicon) bidder4Enforcers := make([]PurposeEnforcer, 11) cfg := fakeTCF2ConfigReader{ @@ -198,7 +200,7 @@ type fakeTCF2ConfigReader struct { enforceAlgo config.TCF2EnforcementAlgo enforcePurpose bool enforceVendors bool - vendorExceptionMap map[openrtb_ext.BidderName]struct{} + vendorExceptionMap map[string]struct{} basicEnforcementVendorsMap map[string]struct{} } @@ -226,7 +228,7 @@ func (fcr *fakeTCF2ConfigReader) PurposeEnforcementAlgo(purpose consentconstants func (fcr *fakeTCF2ConfigReader) PurposeEnforcingVendors(purpose consentconstants.Purpose) bool { return fcr.enforceVendors } -func (fcr *fakeTCF2ConfigReader) PurposeVendorExceptions(purpose consentconstants.Purpose) map[openrtb_ext.BidderName]struct{} { +func (fcr *fakeTCF2ConfigReader) PurposeVendorExceptions(purpose consentconstants.Purpose) map[string]struct{} { return fcr.vendorExceptionMap } func (fcr *fakeTCF2ConfigReader) PurposeOneTreatmentEnabled() bool { From 0b5d04e78c38cdee1a7f9dfcb5958a6647cab338 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Mon, 4 Mar 2024 22:14:42 +0100 Subject: [PATCH 41/69] do normal 'go vet' as it works now (#3550) --- validate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validate.sh b/validate.sh index d263df0ab51..ca4fbc1dfd7 100755 --- a/validate.sh +++ b/validate.sh @@ -36,5 +36,5 @@ fi if $VET; then echo "Running go vet check" - go vet -composites=false ./... + go vet ./... fi From 05a1293ec8a7a3f44f5045b9f09809868f0a477e Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Tue, 5 Mar 2024 02:26:43 -0500 Subject: [PATCH 42/69] Generate Bid ID Test Hardening (#3491) --- exchange/exchange.go | 13 +- exchange/exchange_test.go | 52 ++--- exchange/exchangetest/bid-id-invalid.json | 200 ------------------ exchange/exchangetest/bid-id-valid.json | 193 ----------------- .../events-vast-account-off-request-on.json | 10 +- .../exchangetest/generate-bid-id-error.json | 200 ++++++++++++++++++ .../exchangetest/generate-bid-id-many.json | 163 ++++++++++++++ .../exchangetest/generate-bid-id-one.json | 125 +++++++++++ exchange/targeting_test.go | 2 +- 9 files changed, 529 insertions(+), 429 deletions(-) delete mode 100644 exchange/exchangetest/bid-id-invalid.json delete mode 100644 exchange/exchangetest/bid-id-valid.json create mode 100644 exchange/exchangetest/generate-bid-id-error.json create mode 100644 exchange/exchangetest/generate-bid-id-many.json create mode 100644 exchange/exchangetest/generate-bid-id-one.json diff --git a/exchange/exchange.go b/exchange/exchange.go index 41134104f37..ef38736388d 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -105,7 +105,7 @@ type bidResponseWrapper struct { } type BidIDGenerator interface { - New() (string, error) + New(bidder string) (string, error) Enabled() bool } @@ -117,7 +117,7 @@ func (big *bidIDGenerator) Enabled() bool { return big.enabled } -func (big *bidIDGenerator) New() (string, error) { +func (big *bidIDGenerator) New(bidder string) (string, error) { rawUuid, err := uuid.NewV4() return rawUuid.String(), err } @@ -416,10 +416,11 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog } if e.bidIDGenerator.Enabled() { - for _, seatBid := range adapterBids { - for _, pbsBid := range seatBid.Bids { - pbsBid.GeneratedBidID, err = e.bidIDGenerator.New() - if err != nil { + for bidder, seatBid := range adapterBids { + for i := range seatBid.Bids { + if bidID, err := e.bidIDGenerator.New(bidder.String()); err == nil { + seatBid.Bids[i].GeneratedBidID = bidID + } else { errs = append(errs, errors.New("Error generating bid.ext.prebid.bidid")) } } diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go index 9156fc06c78..78bc167b770 100644 --- a/exchange/exchange_test.go +++ b/exchange/exchange_test.go @@ -663,7 +663,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) { }.Builder e.currencyConverter = mockCurrencyConverter e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -768,7 +768,7 @@ func TestAdapterCurrency(t *testing.T) { }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, - bidIDGenerator: &mockBidIDGenerator{false, false}, + bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, adapterMap: map[openrtb_ext.BidderName]AdaptedBidder{ openrtb_ext.BidderName("appnexus"): AdaptBidder(mockBidder, nil, &config.Configuration{}, &metricsConfig.NilMetricsEngine{}, openrtb_ext.BidderName("appnexus"), nil, ""), }, @@ -846,7 +846,7 @@ func TestFloorsSignalling(t *testing.T) { }.Builder, currencyConverter: currencyConverter, categoriesFetcher: nilCategoryFetcher{}, - bidIDGenerator: &mockBidIDGenerator{false, false}, + bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, priceFloorEnabled: true, priceFloorFetcher: &mockPriceFloorFetcher{}, } @@ -1129,7 +1129,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) { }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -2139,9 +2139,9 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) { }, }, } - bidIdGenerator := &mockBidIDGenerator{} + bidIdGenerator := &fakeBidIDGenerator{} if spec.BidIDGenerator != nil { - *bidIdGenerator = *spec.BidIDGenerator + bidIdGenerator = spec.BidIDGenerator } ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.FloorsEnabled, spec.HostConfigBidValidation, spec.Server) biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest) @@ -2457,31 +2457,35 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string] } } -type mockBidIDGenerator struct { +type fakeBidIDGenerator struct { GenerateBidID bool `json:"generateBidID"` ReturnError bool `json:"returnError"` + bidCount map[string]int } -func (big *mockBidIDGenerator) Enabled() bool { - return big.GenerateBidID +func (f *fakeBidIDGenerator) Enabled() bool { + return f.GenerateBidID } -func (big *mockBidIDGenerator) New() (string, error) { +func (f *fakeBidIDGenerator) New(bidder string) (string, error) { + if f.ReturnError { + return "", errors.New("Test error generating bid.ext.prebid.bidid") + } - if big.ReturnError { - err := errors.New("Test error generating bid.ext.prebid.bidid") - return "", err + if f.bidCount == nil { + f.bidCount = make(map[string]int) } - return "mock_uuid", nil + f.bidCount[bidder] += 1 + return fmt.Sprintf("bid-%v-%v", bidder, f.bidCount[bidder]), nil } -type fakeRandomDeduplicateBidBooleanGenerator struct { - returnValue bool +type fakeBooleanGenerator struct { + value bool } -func (m *fakeRandomDeduplicateBidBooleanGenerator) Generate() bool { - return m.returnValue +func (f *fakeBooleanGenerator) Generate() bool { + return f.value } func newExtRequest() openrtb_ext.ExtRequest { @@ -2849,7 +2853,7 @@ func TestCategoryDedupe(t *testing.T) { Currency: "USD", }, } - deduplicateGenerator := fakeRandomDeduplicateBidBooleanGenerator{returnValue: tt.dedupeGeneratorValue} + deduplicateGenerator := fakeBooleanGenerator{value: tt.dedupeGeneratorValue} bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &deduplicateGenerator, &nonBids{}) assert.Nil(t, err) @@ -3286,7 +3290,7 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) adapterBids[bidderNameApn1] = &seatBidApn1 adapterBids[bidderNameApn2] = &seatBidApn2 - _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true}, &nonBids{}) + _, adapterBids, rejections, err := applyCategoryMapping(nil, *requestExt.Prebid.Targeting, adapterBids, categoriesFetcher, targData, &fakeBooleanGenerator{value: true}, &nonBids{}) assert.NoError(t, err, "Category mapping error should be empty") @@ -4110,7 +4114,7 @@ func TestStoredAuctionResponses(t *testing.T) { e.cache = &wellBehavedCache{} e.me = &metricsConf.NilMetricsEngine{} e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.gdprPermsBuilder = fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -4476,7 +4480,7 @@ func TestAuctionDebugEnabled(t *testing.T) { e.cache = &wellBehavedCache{} e.me = &metricsConf.NilMetricsEngine{} e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.gdprPermsBuilder = fakePermissionsBuilder{ permissions: &permissionsMock{ @@ -5077,7 +5081,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) { }.Builder e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0)) e.categoriesFetcher = categoriesFetcher - e.bidIDGenerator = &mockBidIDGenerator{false, false} + e.bidIDGenerator = &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false} e.requestSplitter = requestSplitter{ me: e.me, gdprPermsBuilder: e.gdprPermsBuilder, @@ -5474,7 +5478,7 @@ type exchangeSpec struct { DebugLog *DebugLog `json:"debuglog,omitempty"` EventsEnabled bool `json:"events_enabled,omitempty"` StartTime int64 `json:"start_time_ms,omitempty"` - BidIDGenerator *mockBidIDGenerator `json:"bidIDGenerator,omitempty"` + BidIDGenerator *fakeBidIDGenerator `json:"bidIDGenerator,omitempty"` RequestType *metrics.RequestType `json:"requestType,omitempty"` PassthroughFlag bool `json:"passthrough_flag,omitempty"` HostSChainFlag bool `json:"host_schain_flag,omitempty"` diff --git a/exchange/exchangetest/bid-id-invalid.json b/exchange/exchangetest/bid-id-invalid.json deleted file mode 100644 index 9b3c5eee245..00000000000 --- a/exchange/exchangetest/bid-id-invalid.json +++ /dev/null @@ -1,200 +0,0 @@ -{ - "bidIDGenerator": { - "generateBidID": true, - "returnError": true - }, - "incomingRequest": { - "ortbRequest": { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "prebid": { - "bidder": { - "appnexus": { - "placementId": 1 - } - } - } - } - } - ], - "test": 1, - "ext": { - "prebid": { - "targeting": { - "includebrandcategory": { - "primaryadserver": 1, - "publisher": "", - "withcategory": true - }, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "min": 0, - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true, - "appendbiddernames": true - } - } - } - }, - "usersyncs": { - "appnexus": "123" - } - }, - "outgoingRequests": { - "appnexus": { - "mockResponse": { - "pbsSeatBids": [ - { - "pbsBids": [ - { - "ortbBid": { - "id": "apn-bid", - "impid": "my-imp-id", - "price": 0.3, - "w": 200, - "h": 250, - "crid": "creative-1", - "cat": [ - "IAB1-1" - ] - }, - "bidType": "video", - "bidVideo": { - "duration": 30, - "PrimaryCategory": "" - } - } - ], - "seat": "appnexus" - } - ] - } - } - }, - "response": { - "bids": { - "id": "some-request-id", - "seatbid": [ - { - "seat": "appnexus", - "bid": [ - { - "id": "apn-bid", - "impid": "my-imp-id", - "price": 0.3, - "w": 200, - "h": 250, - "crid": "creative-1", - "cat": [ - "IAB1-1" - ], - "ext": { - "origbidcpm": 0.3, - "prebid": { - "meta": { - "adaptercode":"appnexus" - }, - "type": "video", - "targeting": { - "hb_bidder": "appnexus", - "hb_bidder_appnexus": "appnexus", - "hb_cache_host": "www.pbcserver.com", - "hb_cache_host_appnex": "www.pbcserver.com", - "hb_cache_path": "/pbcache/endpoint", - "hb_cache_path_appnex": "/pbcache/endpoint", - "hb_pb": "0.20", - "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", - "hb_size": "200x250", - "hb_size_appnexus": "200x250" - } - } - } - } - ] - } - ] - }, - "ext": { - "debug": { - "resolvedrequest": { - "id": "some-request-id", - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "prebid": { - "bidder": { - "appnexus": { - "placementId": 1 - } - } - } - } - } - ], - "site": { - "page": "test.somepage.com" - }, - "test": 1, - "ext": { - "prebid": { - "targeting": { - "includebrandcategory": { - "primaryadserver": 1, - "publisher": "", - "withcategory": true - }, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "min": 0, - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true, - "appendbiddernames": true - } - } - } - } - }, - "errors": { - "prebid": [ - { - "code": 999, - "message": "Error generating bid.ext.prebid.bidid" - } - ] - } - } - } -} \ No newline at end of file diff --git a/exchange/exchangetest/bid-id-valid.json b/exchange/exchangetest/bid-id-valid.json deleted file mode 100644 index caa02384025..00000000000 --- a/exchange/exchangetest/bid-id-valid.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "bidIDGenerator": { - "generateBidID": true, - "returnError": false - }, - "incomingRequest": { - "ortbRequest": { - "id": "some-request-id", - "site": { - "page": "test.somepage.com" - }, - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "prebid": { - "bidder": { - "appnexus": { - "placementId": 1 - } - } - } - } - } - ], - "test": 1, - "ext": { - "prebid": { - "targeting": { - "includebrandcategory": { - "primaryadserver": 1, - "publisher": "", - "withcategory": true - }, - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "min": 0, - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true, - "appendbiddernames": true - } - } - } - }, - "usersyncs": { - "appnexus": "123" - } - }, - "outgoingRequests": { - "appnexus": { - "mockResponse": { - "pbsSeatBids": [ - { - "pbsBids": [ - { - "ortbBid": { - "id": "apn-bid", - "impid": "my-imp-id", - "price": 0.3, - "w": 200, - "h": 250, - "crid": "creative-1", - "cat": [ - "IAB1-1" - ] - }, - "bidType": "video", - "bidVideo": { - "duration": 30, - "PrimaryCategory": "" - } - } - ], - "seat": "appnexus" - } - ] - } - } - }, - "response": { - "bids": { - "id": "some-request-id", - "seatbid": [ - { - "seat": "appnexus", - "bid": [ - { - "id": "apn-bid", - "impid": "my-imp-id", - "price": 0.3, - "w": 200, - "h": 250, - "crid": "creative-1", - "cat": [ - "IAB1-1" - ], - "ext": { - "origbidcpm": 0.3, - "prebid": { - "meta": { - "adaptercode":"appnexus" - }, - "bidid": "mock_uuid", - "type": "video", - "targeting": { - "hb_bidder": "appnexus", - "hb_bidder_appnexus": "appnexus", - "hb_cache_host": "www.pbcserver.com", - "hb_cache_host_appnex": "www.pbcserver.com", - "hb_cache_path": "/pbcache/endpoint", - "hb_cache_path_appnex": "/pbcache/endpoint", - "hb_pb": "0.20", - "hb_pb_appnexus": "0.20", - "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", - "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", - "hb_size": "200x250", - "hb_size_appnexus": "200x250" - } - } - } - } - ] - } - ] - }, - "ext": { - "debug": { - "resolvedrequest": { - "id": "some-request-id", - "imp": [ - { - "id": "my-imp-id", - "video": { - "mimes": [ - "video/mp4" - ] - }, - "ext": { - "prebid": { - "bidder": { - "appnexus": { - "placementId": 1 - } - } - } - } - } - ], - "site": { - "page": "test.somepage.com" - }, - "test": 1, - "ext": { - "prebid": { - "targeting": { - "pricegranularity": { - "precision": 2, - "ranges": [ - { - "min": 0, - "max": 20, - "increment": 0.1 - } - ] - }, - "includewinners": true, - "includebidderkeys": true, - "includebrandcategory": { - "primaryadserver": 1, - "publisher": "", - "withcategory": true - }, - "appendbiddernames": true - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/exchange/exchangetest/events-vast-account-off-request-on.json b/exchange/exchangetest/events-vast-account-off-request-on.json index 1a5aea4217f..93ec6f35d07 100644 --- a/exchange/exchangetest/events-vast-account-off-request-on.json +++ b/exchange/exchangetest/events-vast-account-off-request-on.json @@ -134,7 +134,7 @@ "meta": { "adaptercode": "audienceNetwork" }, - "bidid": "mock_uuid", + "bidid": "bid-audienceNetwork-1", "type": "video" } } @@ -146,7 +146,7 @@ "bid": [ { "id": "winning-bid", - "adm": "prebid.org wrapper", + "adm": "prebid.org wrapper", "nurl": "http://domain.com/winning-bid", "impid": "my-imp-id", "price": 0.71, @@ -156,10 +156,10 @@ "ext": { "origbidcpm": 0.71, "prebid": { - "bidid": "mock_uuid", "meta": { "adaptercode": "appnexus" }, + "bidid": "bid-appnexus-1", "type": "video", "targeting": { "hb_bidder": "appnexus", @@ -174,7 +174,7 @@ }, { "id": "losing-bid", - "adm": "prebid.org wrapper", + "adm": "prebid.org wrapper", "nurl": "http://domain.com/losing-bid", "impid": "my-imp-id", "price": 0.21, @@ -187,7 +187,7 @@ "meta": { "adaptercode": "appnexus" }, - "bidid": "mock_uuid", + "bidid": "bid-appnexus-2", "type": "video" } } diff --git a/exchange/exchangetest/generate-bid-id-error.json b/exchange/exchangetest/generate-bid-id-error.json new file mode 100644 index 00000000000..9c13ef38895 --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-error.json @@ -0,0 +1,200 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": true + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + }, + "usersyncs": { + "appnexus": "123" + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30, + "PrimaryCategory": "" + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "my-imp-id", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "type": "video", + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_pb": "0.20", + "hb_pb_appnexus": "0.20", + "hb_pb_cat_dur": "0.20_VideoGames_0s_appnexus", + "hb_pb_cat_dur_appnex": "0.20_VideoGames_0s_appnexus", + "hb_size": "200x250", + "hb_size_appnexus": "200x250" + } + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "my-imp-id", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + }, + "test": 1, + "ext": { + "prebid": { + "targeting": { + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "", + "withcategory": true + }, + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "min": 0, + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true, + "appendbiddernames": true + } + } + } + } + }, + "errors": { + "prebid": [ + { + "code": 999, + "message": "Error generating bid.ext.prebid.bidid" + } + ] + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/generate-bid-id-many.json b/exchange/exchangetest/generate-bid-id-many.json new file mode 100644 index 00000000000..4c6a18ab174 --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-many.json @@ -0,0 +1,163 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": false + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ] + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid-1", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30 + } + }, + { + "ortbBid": { + "id": "apn-bid-2", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30 + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid-1", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "bidid": "bid-appnexus-1", + "type": "video" + } + } + }, + { + "id": "apn-bid-2", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "bidid": "bid-appnexus-2", + "type": "video" + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/exchangetest/generate-bid-id-one.json b/exchange/exchangetest/generate-bid-id-one.json new file mode 100644 index 00000000000..7d4169d635d --- /dev/null +++ b/exchange/exchangetest/generate-bid-id-one.json @@ -0,0 +1,125 @@ +{ + "bidIDGenerator": { + "generateBidID": true, + "returnError": false + }, + "incomingRequest": { + "ortbRequest": { + "id": "some-request-id", + "site": { + "page": "test.somepage.com" + }, + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ] + } + }, + "outgoingRequests": { + "appnexus": { + "mockResponse": { + "pbsSeatBids": [ + { + "pbsBids": [ + { + "ortbBid": { + "id": "apn-bid", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ] + }, + "bidType": "video", + "bidVideo": { + "duration": 30 + } + } + ], + "seat": "appnexus" + } + ] + } + } + }, + "response": { + "bids": { + "id": "some-request-id", + "seatbid": [ + { + "seat": "appnexus", + "bid": [ + { + "id": "apn-bid", + "impid": "imp-id-1", + "price": 0.3, + "w": 200, + "h": 250, + "crid": "creative-1", + "cat": [ + "IAB1-1" + ], + "ext": { + "origbidcpm": 0.3, + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "bidid": "bid-appnexus-1", + "type": "video" + } + } + } + ] + } + ] + }, + "ext": { + "debug": { + "resolvedrequest": { + "id": "some-request-id", + "imp": [ + { + "id": "imp-id-1", + "video": { + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "prebid": { + "bidder": { + "appnexus": { + "placementId": 1 + } + } + } + } + } + ], + "site": { + "page": "test.somepage.com" + } + } + } + } + } +} \ No newline at end of file diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go index e6cc429e8ea..13a7572a182 100644 --- a/exchange/targeting_test.go +++ b/exchange/targeting_test.go @@ -102,7 +102,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)), gdprDefaultValue: gdpr.SignalYes, categoriesFetcher: categoriesFetcher, - bidIDGenerator: &mockBidIDGenerator{false, false}, + bidIDGenerator: &fakeBidIDGenerator{GenerateBidID: false, ReturnError: false}, } ex.requestSplitter = requestSplitter{ me: ex.me, From 4b1ca6b470863901bed03075ef9915acc24a018a Mon Sep 17 00:00:00 2001 From: Nilesh Chate <97721111+pm-nilesh-chate@users.noreply.github.com> Date: Tue, 5 Mar 2024 21:57:44 +0530 Subject: [PATCH 43/69] Privacy Sandbox: support testing label header (#3381) --- config/account.go | 10 + config/config.go | 2 + config/config_test.go | 9 + endpoints/cookie_sync.go | 50 +++- endpoints/cookie_sync_test.go | 211 +++++++++++++- endpoints/openrtb2/amp_auction.go | 17 +- endpoints/openrtb2/auction.go | 38 ++- endpoints/openrtb2/auction_test.go | 409 +++++++++++++++++++++++++++- endpoints/openrtb2/video_auction.go | 12 +- errortypes/code.go | 1 + openrtb_ext/request_wrapper.go | 34 ++- openrtb_ext/request_wrapper_test.go | 42 +-- 12 files changed, 779 insertions(+), 56 deletions(-) diff --git a/config/account.go b/config/account.go index 1ab6a4c0246..72b6c07a81e 100644 --- a/config/account.go +++ b/config/account.go @@ -337,6 +337,16 @@ type AccountPrivacy struct { AllowActivities *AllowActivities `mapstructure:"allowactivities" json:"allowactivities"` IPv6Config IPv6 `mapstructure:"ipv6" json:"ipv6"` IPv4Config IPv4 `mapstructure:"ipv4" json:"ipv4"` + PrivacySandbox PrivacySandbox `mapstructure:"privacysandbox" json:"privacysandbox"` +} + +type PrivacySandbox struct { + CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"` +} + +type CookieDeprecation struct { + Enabled bool `mapstructure:"enabled"` + TTLSec int `mapstructure:"ttl_sec"` } type IPv6 struct { diff --git a/config/config.go b/config/config.go index 71b6893ce91..8449bc1b7f4 100644 --- a/config/config.go +++ b/config/config.go @@ -1107,6 +1107,8 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400) v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600) v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0) + v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false) + v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800) v.SetDefault("account_defaults.events_enabled", false) v.SetDefault("account_defaults.privacy.ipv6.anon_keep_bits", 56) diff --git a/config/config_test.go b/config/config_test.go index e43628be48d..c58e93de1c9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -203,6 +203,8 @@ func TestDefaults(t *testing.T) { cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period) cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge) cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims) + cmpBools(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) + cmpInts(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) cmpBools(t, "account_defaults.events.enabled", false, cfg.AccountDefaults.Events.Enabled) @@ -503,6 +505,10 @@ account_defaults: anon_keep_bits: 50 ipv4: anon_keep_bits: 20 + privacysandbox: + cookiedeprecation: + enabled: true + ttl_sec: 86400 tmax_adjustments: enabled: true bidder_response_duration_min_ms: 700 @@ -622,6 +628,9 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits) cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) + cmpBools(t, "account_defaults.privacy.cookiedeprecation.enabled", true, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) + cmpInts(t, "account_defaults.privacy.cookiedeprecation.ttl_sec", 86400, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) + // Assert compression related defaults cmpBools(t, "compression.request.enable_gzip", true, cfg.Compression.Request.GZIP) cmpBools(t, "compression.response.enable_gzip", false, cfg.Compression.Response.GZIP) diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go index 84d6d7847ef..d6e0b31a096 100644 --- a/endpoints/cookie_sync.go +++ b/endpoints/cookie_sync.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "strings" + "time" "github.com/golang/glog" "github.com/julienschmidt/httprouter" @@ -29,8 +30,11 @@ import ( "github.com/prebid/prebid-server/v2/usersync" "github.com/prebid/prebid-server/v2/util/jsonutil" stringutil "github.com/prebid/prebid-server/v2/util/stringutil" + "github.com/prebid/prebid-server/v2/util/timeutil" ) +const receiveCookieDeprecation = "receive-cookie-deprecation" + var ( errCookieSyncOptOut = errors.New("User has opted out") errCookieSyncBody = errors.New("Failed to read request body") @@ -73,6 +77,7 @@ func NewCookieSyncEndpoint( metrics: metrics, pbsAnalytics: analyticsRunner, accountsFetcher: accountsFetcher, + time: &timeutil.RealTime{}, } } @@ -83,10 +88,12 @@ type cookieSyncEndpoint struct { metrics metrics.MetricsEngine pbsAnalytics analytics.Runner accountsFetcher stored_requests.AccountFetcher + time timeutil.Time } func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - request, privacyMacros, err := c.parseRequest(r) + request, privacyMacros, account, err := c.parseRequest(r) + c.setCookieDeprecationHeader(w, r, account) if err != nil { c.writeParseRequestErrorMetrics(err) c.handleError(w, err, http.StatusBadRequest) @@ -113,16 +120,16 @@ func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ ht } } -func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, error) { +func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, *config.Account, error) { defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, errCookieSyncBody + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, errCookieSyncBody } request := cookieSyncRequest{} if err := jsonutil.UnmarshalValid(body, &request); err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, fmt.Errorf("JSON parsing failed: %s", err.Error()) + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, fmt.Errorf("JSON parsing failed: %s", err.Error()) } if request.Account == "" { @@ -130,7 +137,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma } account, fetchErrs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account, c.metrics) if len(fetchErrs) > 0 { - return usersync.Request{}, macros.UserSyncPrivacy{}, combineErrors(fetchErrs) + return usersync.Request{}, macros.UserSyncPrivacy{}, nil, combineErrors(fetchErrs) } request = c.setLimit(request, account.CookieSync) @@ -138,7 +145,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma privacyMacros, gdprSignal, privacyPolicies, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, err + return usersync.Request{}, macros.UserSyncPrivacy{}, account, err } ccpaParsedPolicy := ccpa.ParsedPolicy{} @@ -156,7 +163,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma syncTypeFilter, err := parseTypeFilter(request.FilterSettings) if err != nil { - return usersync.Request{}, macros.UserSyncPrivacy{}, err + return usersync.Request{}, macros.UserSyncPrivacy{}, account, err } gdprRequestInfo := gdpr.RequestInfo{ @@ -185,7 +192,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, ma SyncTypeFilter: syncTypeFilter, GPPSID: request.GPPSID, } - return rx, privacyMacros, nil + return rx, privacyMacros, account, nil } func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (macros.UserSyncPrivacy, gdpr.Signal, privacy.Policies, error) { @@ -455,11 +462,38 @@ func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.S }) w.Header().Set("Content-Type", "application/json; charset=utf-8") + enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.Encode(response) } +func (c *cookieSyncEndpoint) setCookieDeprecationHeader(w http.ResponseWriter, r *http.Request, account *config.Account) { + if rcd, err := r.Cookie(receiveCookieDeprecation); err == nil && rcd != nil { + return + } + if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { + return + } + cookie := &http.Cookie{ + Name: receiveCookieDeprecation, + Value: "1", + Secure: true, + HttpOnly: true, + Path: "/", + SameSite: http.SameSiteNoneMode, + Expires: c.time.Now().Add(time.Second * time.Duration(account.Privacy.PrivacySandbox.CookieDeprecation.TTLSec)), + } + setCookiePartitioned(w, cookie) +} + +// setCookiePartitioned temporary substitute for http.SetCookie(w, cookie) until it supports Partitioned cookie type. Refer https://github.com/golang/go/issues/62490 +func setCookiePartitioned(w http.ResponseWriter, cookie *http.Cookie) { + if v := cookie.String(); v != "" { + w.Header().Add("Set-Cookie", v+"; Partitioned;") + } +} + func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.CookieSyncBidder { to := make([]*analytics.CookieSyncBidder, len(from)) for i, b := range from { diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go index adfdcb22fab..35d7dd4baf5 100644 --- a/endpoints/cookie_sync_test.go +++ b/endpoints/cookie_sync_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -11,6 +12,7 @@ import ( "strings" "testing" "testing/iotest" + "time" "github.com/prebid/prebid-server/v2/analytics" "github.com/prebid/prebid-server/v2/config" @@ -23,11 +25,19 @@ import ( "github.com/prebid/prebid-server/v2/privacy/ccpa" "github.com/prebid/prebid-server/v2/usersync" "github.com/prebid/prebid-server/v2/util/ptrutil" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +// fakeTime implements the Time interface +type fakeTime struct { + time time.Time +} + +func (ft *fakeTime) Now() time.Time { + return ft.time +} + func TestNewCookieSyncEndpoint(t *testing.T) { var ( syncersByBidder = map[string]usersync.Syncer{"a": &MockSyncer{}} @@ -111,14 +121,16 @@ func TestCookieSyncHandle(t *testing.T) { cookieWithSyncs.Sync("foo", "anyID") testCases := []struct { - description string - givenCookie *usersync.Cookie - givenBody io.Reader - givenChooserResult usersync.Result - expectedStatusCode int - expectedBody string - setMetricsExpectations func(*metrics.MetricsEngineMock) - setAnalyticsExpectations func(*MockAnalyticsRunner) + description string + givenCookie *usersync.Cookie + givenBody io.Reader + givenChooserResult usersync.Result + givenAccountData map[string]json.RawMessage + expectedStatusCode int + expectedBody string + setMetricsExpectations func(*metrics.MetricsEngineMock) + setAnalyticsExpectations func(*MockAnalyticsRunner) + expectedCookieDeprecationHeader bool }{ { description: "Request With Cookie", @@ -285,6 +297,42 @@ func TestCookieSyncHandle(t *testing.T) { a.On("LogCookieSyncObject", &expected).Once() }, }, + { + description: "CookieDeprecation-Set", + givenCookie: cookieWithSyncs, + givenBody: strings.NewReader(`{"account": "testAccount"}`), + givenChooserResult: usersync.Result{ + Status: usersync.StatusOK, + BiddersEvaluated: []usersync.BidderEvaluation{{Bidder: "a", SyncerKey: "aSyncer", Status: usersync.StatusAlreadySynced}}, + SyncersChosen: []usersync.SyncerChoice{{Bidder: "a", Syncer: &syncer}}, + }, + givenAccountData: map[string]json.RawMessage{ + "testAccount": json.RawMessage(`{"id":"1","privacy":{"privacysandbox":{"cookiedeprecation":{"enabled":true,"ttlsec":86400}}}}`), + }, + expectedStatusCode: 200, + expectedCookieDeprecationHeader: true, + expectedBody: `{"status":"ok","bidder_status":[` + + `{"bidder":"a","no_cookie":true,"usersync":{"url":"aURL","type":"redirect","supportCORS":true}}` + + `]}` + "\n", + setMetricsExpectations: func(m *metrics.MetricsEngineMock) { + m.On("RecordCookieSync", metrics.CookieSyncOK).Once() + m.On("RecordSyncerRequest", "aSyncer", metrics.SyncerCookieSyncAlreadySynced).Once() + }, + setAnalyticsExpectations: func(a *MockAnalyticsRunner) { + expected := analytics.CookieSyncObject{ + Status: 200, + Errors: nil, + BidderStatus: []*analytics.CookieSyncBidder{ + { + BidderCode: "a", + NoCookie: true, + UsersyncInfo: &analytics.UsersyncInfo{URL: "aURL", Type: "redirect", SupportCORS: true}, + }, + }, + } + a.On("LogCookieSyncObject", &expected).Once() + }, + }, } for _, test := range testCases { @@ -294,7 +342,9 @@ func TestCookieSyncHandle(t *testing.T) { mockAnalytics := MockAnalyticsRunner{} test.setAnalyticsExpectations(&mockAnalytics) - fakeAccountFetcher := FakeAccountsFetcher{} + fakeAccountFetcher := FakeAccountsFetcher{ + AccountData: test.givenAccountData, + } gdprPermsBuilder := fakePermissionsBuilder{ permissions: &fakePermissions{}, @@ -329,6 +379,7 @@ func TestCookieSyncHandle(t *testing.T) { metrics: &mockMetrics, pbsAnalytics: &mockAnalytics, accountsFetcher: &fakeAccountFetcher, + time: &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) @@ -336,6 +387,16 @@ func TestCookieSyncHandle(t *testing.T) { assert.Equal(t, test.expectedStatusCode, writer.Code, test.description+":status_code") assert.Equal(t, test.expectedBody, writer.Body.String(), test.description+":body") + + gotCookie := writer.Header().Get("Set-Cookie") + if test.expectedCookieDeprecationHeader { + wantCookieTTL := endpoint.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat) + wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL) + assert.Equal(t, wantCookie, gotCookie, test.description) + } else { + assert.Empty(t, gotCookie, test.description) + } + mockMetrics.AssertExpectations(t) mockAnalytics.AssertExpectations(t) } @@ -1060,7 +1121,7 @@ func TestCookieSyncParseRequest(t *testing.T) { }}, } assert.NoError(t, endpoint.config.MarshalAccountDefaults()) - request, privacyPolicies, err := endpoint.parseRequest(httpRequest) + request, privacyPolicies, _, err := endpoint.parseRequest(httpRequest) if test.expectedError == "" { assert.NoError(t, err, test.description+":err") @@ -2207,3 +2268,131 @@ func getDefaultActivityConfig(componentName string, allow bool) *config.AccountP }, } } + +func TestSetCookieDeprecationHeader(t *testing.T) { + getTestRequest := func(addCookie bool) *http.Request { + r := httptest.NewRequest("POST", "/cookie_sync", nil) + if addCookie { + r.AddCookie(&http.Cookie{Name: receiveCookieDeprecation, Value: "1"}) + } + return r + } + + tests := []struct { + name string + responseWriter http.ResponseWriter + request *http.Request + account *config.Account + expectedCookieDeprecationHeader bool + }{ + { + name: "not-present-account-nil", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: nil, + expectedCookieDeprecationHeader: false, + }, + { + name: "not-present-cookiedeprecation-disabled", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + expectedCookieDeprecationHeader: false, + }, + { + name: "present-cookiedeprecation-disabled", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + expectedCookieDeprecationHeader: false, + }, + { + name: "present-cookiedeprecation-enabled", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + + expectedCookieDeprecationHeader: false, + }, + { + name: "present-account-nil", + request: getTestRequest(true), + responseWriter: httptest.NewRecorder(), + account: nil, + expectedCookieDeprecationHeader: false, + }, + { + name: "not-present-cookiedeprecation-enabled", + request: getTestRequest(false), + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + expectedCookieDeprecationHeader: true, + }, + { + name: "failed-to-read-cookiedeprecation-enabled", + request: &http.Request{}, // nil cookie. error: http: named cookie not present + responseWriter: httptest.NewRecorder(), + account: &config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + expectedCookieDeprecationHeader: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &cookieSyncEndpoint{ + time: &fakeTime{time: time.Date(2024, 2, 22, 9, 42, 4, 13, time.UTC)}, + } + c.setCookieDeprecationHeader(tt.responseWriter, tt.request, tt.account) + gotCookie := tt.responseWriter.Header().Get("Set-Cookie") + if tt.expectedCookieDeprecationHeader { + wantCookieTTL := c.time.Now().Add(time.Second * time.Duration(86400)).UTC().Format(http.TimeFormat) + wantCookie := fmt.Sprintf("receive-cookie-deprecation=1; Path=/; Expires=%v; HttpOnly; Secure; SameSite=None; Partitioned;", wantCookieTTL) + assert.Equal(t, wantCookie, gotCookie, ":set_cookie_deprecation_header") + } else { + assert.Empty(t, gotCookie, ":set_cookie_deprecation_header") + } + }) + } +} diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index a6ad8d3fc65..6d14ed7d69d 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -230,6 +230,19 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } + hasStoredResponses := len(storedAuctionResponses) > 0 + errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false) + errL = append(errL, errs...) + ao.Errors = append(ao.Errors, errs...) + if errortypes.ContainsFatalError(errs) { + w.WriteHeader(http.StatusBadRequest) + for _, err := range errortypes.FatalOnly(errs) { + w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + } + labels.RequestStatus = metrics.RequestStatusBadInput + return + } + tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) activityControl = privacy.NewActivityControl(&account.Privacy) @@ -497,10 +510,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr return } - hasStoredResponses := len(storedAuctionResponses) > 0 - e = deps.validateRequest(req, true, hasStoredResponses, storedBidResponses, false) - errs = append(errs, e...) - return } diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index c7beceb1b52..2aafe6808ef 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -60,6 +60,7 @@ import ( const storedRequestTimeoutMillis = 50 const ampChannel = "amp" const appChannel = "app" +const secCookieDeprecation = "Sec-Cookie-Deprecation" var ( dntKey string = http.CanonicalHeaderKey("DNT") @@ -552,7 +553,7 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric } hasStoredResponses := len(storedAuctionResponses) > 0 - errL := deps.validateRequest(req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) + errL := deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) if len(errL) > 0 { errs = append(errs, errL...) } @@ -746,7 +747,7 @@ func mergeBidderParamsImpExtPrebid(impExt *openrtb_ext.ImpExt, reqExtParams map[ return nil } -func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { +func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http.Request, req *openrtb_ext.RequestWrapper, isAmp bool, hasStoredResponses bool, storedBidResp stored_responses.ImpBidderStoredResp, hasStoredBidRequest bool) []error { errL := []error{} if req.ID == "" { return []error{errors.New("request missing required field: \"id\"")} @@ -875,6 +876,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp return append(errL, err) } + if err := validateOrFillCDep(httpReq, req, account); err != nil { + errL = append(errL, err) + } + if ccpaPolicy, err := ccpa.ReadFromRequestWrapper(req, gpp); err != nil { errL = append(errL, err) if errortypes.ContainsFatalError([]error{err}) { @@ -1922,6 +1927,35 @@ func validateDevice(device *openrtb2.Device) error { return nil } +func validateOrFillCDep(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error { + if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { + return nil + } + + deviceExt, err := req.GetDeviceExt() + if err != nil { + return err + } + + if deviceExt.GetCDep() != "" { + return nil + } + + secCookieDeprecation := httpReq.Header.Get(secCookieDeprecation) + if secCookieDeprecation == "" { + return nil + } + if len(secCookieDeprecation) > 100 { + return &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + } + } + + deviceExt.SetCDep(secCookieDeprecation) + return nil +} + func validateExactlyOneInventoryType(reqWrapper *openrtb_ext.RequestWrapper) error { // Prep for mutual exclusion check diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 9df8b540ab5..f51979db10d 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1650,6 +1650,8 @@ func TestValidateRequest(t *testing.T) { description string givenIsAmp bool givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error expectedChannelObject *openrtb_ext.ExtRequestPrebidChannel }{ @@ -1870,7 +1872,7 @@ func TestValidateRequest(t *testing.T) { } for _, test := range testCases { - errorList := deps.validateRequest(test.givenRequestWrapper, test.givenIsAmp, false, nil, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, test.givenIsAmp, false, nil, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) if len(errorList) == 0 { @@ -3053,7 +3055,7 @@ func TestCurrencyTrunc(t *testing.T) { Cur: []string{"USD", "EUR"}, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"} assert.ElementsMatch(t, errL, []error{&expectedError}) @@ -3104,7 +3106,7 @@ func TestCCPAInvalid(t *testing.T) { }, } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedWarning := errortypes.Warning{ Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)", @@ -3158,7 +3160,7 @@ func TestNoSaleInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided") assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3210,7 +3212,7 @@ func TestValidateSourceTID(t *testing.T) { }, } - deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID") } @@ -3257,7 +3259,7 @@ func TestSChainInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.") assert.ElementsMatch(t, errL, []error{expectedError}) @@ -3826,7 +3828,7 @@ func TestEidPermissionsInvalid(t *testing.T) { Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`), } - errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) + errL := deps.validateRequest(nil, nil, &openrtb_ext.RequestWrapper{BidRequest: &req}, false, false, nil, false) expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`) assert.ElementsMatch(t, errL, []error{expectedError}) @@ -5134,6 +5136,8 @@ func TestValidateStoredResp(t *testing.T) { testCases := []struct { description string givenRequestWrapper *openrtb_ext.RequestWrapper + givenHttpRequest *http.Request + givenAccount *config.Account expectedErrorList []error hasStoredAuctionResponses bool storedBidResponses stored_responses.ImpBidderStoredResp @@ -5675,7 +5679,7 @@ func TestValidateStoredResp(t *testing.T) { for _, test := range testCases { t.Run(test.description, func(t *testing.T) { - errorList := deps.validateRequest(test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) + errorList := deps.validateRequest(test.givenAccount, test.givenHttpRequest, test.givenRequestWrapper, false, test.hasStoredAuctionResponses, test.storedBidResponses, false) assert.Equalf(t, test.expectedErrorList, errorList, "Error doesn't match: %s\n", test.description) }) } @@ -6114,3 +6118,392 @@ func TestValidateAliases(t *testing.T) { func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) { return openrtb_ext.BidderName(strings.ToLower(name)), true } + +func TestValidateOrFillCDep(t *testing.T) { + type args struct { + httpReq *http.Request + req *openrtb_ext.RequestWrapper + account config.Account + } + tests := []struct { + name string + args args + wantDeviceExt json.RawMessage + wantErr error + }{ + { + name: "account-nil", + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-not-enabled", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-disabled-explicitly", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: false, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "cookie-deprecation-enabled-header-not-present-in-request", + args: args{ + httpReq: &http.Request{}, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: nil, + wantErr: nil, + }, + { + name: "header-present-request-device-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: nil, + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-not-nil", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"cdep":"example_label_1","foo":"bar"}`), + wantErr: nil, + }, + { + name: "header-present-with-length-more-than-100", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar"}`), + wantErr: &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + { + name: "header-present-request-device-ext-cdep-present", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{"foo":"bar","cdep":"example_label_2"}`), + wantErr: nil, + }, + { + name: "header-present-request-device-ext-invalid", + args: args{ + httpReq: &http.Request{ + Header: http.Header{secCookieDeprecation: []string{"example_label_1"}}, + }, + req: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Device: &openrtb2.Device{ + Ext: json.RawMessage(`{`), + }, + }, + }, + account: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + }, + }, + }, + }, + }, + wantDeviceExt: json.RawMessage(`{`), + wantErr: &errortypes.FailedToUnmarshal{ + Message: "expects \" or n, but found \x00", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateOrFillCDep(tt.args.httpReq, tt.args.req, &tt.args.account) + assert.Equal(t, tt.wantErr, err) + if tt.args.req != nil { + err := tt.args.req.RebuildRequest() + assert.NoError(t, err) + } + if tt.wantDeviceExt == nil { + if tt.args.req != nil && tt.args.req.Device != nil { + assert.Nil(t, tt.args.req.Device.Ext) + } + } else { + assert.Equal(t, string(tt.wantDeviceExt), string(tt.args.req.Device.Ext)) + } + }) + } +} + +func TestValidateRequestCookieDeprecation(t *testing.T) { + testCases := + []struct { + name string + givenAccount *config.Account + httpReq *http.Request + reqWrapper *openrtb_ext.RequestWrapper + wantErrs []error + wantCDep string + }{ + { + name: "header-with-length-less-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "sample-value") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{}, + wantCDep: "sample-value", + }, + { + name: "header-with-length-more-than-100", + httpReq: func() *http.Request { + req := httptest.NewRequest("POST", "/openrtb2/auction", nil) + req.Header.Set(secCookieDeprecation, "zjfXqGxXFI8yura8AhQl1DK2EMMmryrC8haEpAlwjoerrFfEo2MQTXUq6cSmLohI8gjsnkGU4oAzvXd4TTAESzEKsoYjRJ2zKxmEa") + return req + }(), + givenAccount: &config.Account{ + ID: "1", + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + CookieDeprecation: config.CookieDeprecation{ + Enabled: true, + TTLSec: 86400, + }, + }, + }, + }, + reqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "Some-ID", + App: &openrtb2.App{}, + Imp: []openrtb2.Imp{ + { + ID: "Some-Imp-ID", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{ + { + W: 600, + H: 500, + }, + { + W: 300, + H: 600, + }, + }, + }, + Ext: []byte(`{"pubmatic":{"publisherId": 12345678}}`), + }, + }, + }, + }, + wantErrs: []error{ + &errortypes.Warning{ + Message: "request.device.ext.cdep must not exceed 100 characters", + WarningCode: errortypes.SecCookieDeprecationLenWarningCode, + }, + }, + wantCDep: "", + }, + } + + deps := &endpointDeps{ + fakeUUIDGenerator{}, + &warningsCheckExchange{}, + mockBidderParamValidator{}, + &mockStoredReqFetcher{}, + empty_fetcher.EmptyFetcher{}, + &mockAccountFetcher{}, + &config.Configuration{}, + &metricsConfig.NilMetricsEngine{}, + analyticsBuild.New(&config.Analytics{}), + map[string]string{}, + false, + []byte{}, + openrtb_ext.BuildBidderMap(), + nil, + nil, + hardcodedResponseIPValidator{response: true}, + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + openrtb_ext.NormalizeBidderName, + } + + for _, test := range testCases { + errs := deps.validateRequest(test.givenAccount, test.httpReq, test.reqWrapper, false, false, stored_responses.ImpBidderStoredResp{}, false) + assert.Equal(t, test.wantErrs, errs) + test.reqWrapper.RebuildRequest() + deviceExt, err := test.reqWrapper.GetDeviceExt() + assert.NoError(t, err) + assert.Equal(t, test.wantCDep, deviceExt.GetCDep()) + } +} diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 22248f1f36c..3ddfc31aa39 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -267,12 +267,6 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } - errL = deps.validateRequest(bidReqWrapper, false, false, nil, false) - if errortypes.ContainsFatalError(errL) { - handleError(&labels, w, errL, &vo, &debugLog) - return - } - ctx := context.Background() timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(bidReqWrapper.TMax) * time.Millisecond) if timeout > 0 { @@ -306,6 +300,12 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } + errL = deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) + if errortypes.ContainsFatalError(errL) { + handleError(&labels, w, errL, &vo, &debugLog) + return + } + activityControl = privacy.NewActivityControl(&account.Privacy) secGPC := r.Header.Get("Sec-GPC") diff --git a/errortypes/code.go b/errortypes/code.go index 399dd663498..a30bb8e4bc0 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -32,6 +32,7 @@ const ( BidAdjustmentWarningCode FloorBidRejectionWarningCode InvalidBidResponseDSAWarningCode + SecCookieDeprecationLenWarningCode ) // Coder provides an error or warning code with severity. diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go index d7ff5acc021..4c4eb9a6988 100644 --- a/openrtb_ext/request_wrapper.go +++ b/openrtb_ext/request_wrapper.go @@ -60,6 +60,7 @@ const ( dataKey = "data" schainKey = "schain" us_privacyKey = "us_privacy" + cdepKey = "cdep" ) // LenImp returns the number of impressions without causing the creation of ImpWrapper objects. @@ -883,6 +884,8 @@ type DeviceExt struct { extDirty bool prebid *ExtDevicePrebid prebidDirty bool + cdep string + cdepDirty bool } func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { @@ -910,6 +913,13 @@ func (de *DeviceExt) unmarshal(extJson json.RawMessage) error { } } + cdepJson, hasCDep := de.ext[cdepKey] + if hasCDep && cdepJson != nil { + if err := jsonutil.Unmarshal(cdepJson, &de.cdep); err != nil { + return err + } + } + return nil } @@ -931,6 +941,19 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { de.prebidDirty = false } + if de.cdepDirty { + if len(de.cdep) > 0 { + rawjson, err := jsonutil.Marshal(de.cdep) + if err != nil { + return nil, err + } + de.ext[cdepKey] = rawjson + } else { + delete(de.ext, cdepKey) + } + de.cdepDirty = false + } + de.extDirty = false if len(de.ext) == 0 { return nil, nil @@ -939,7 +962,7 @@ func (de *DeviceExt) marshal() (json.RawMessage, error) { } func (de *DeviceExt) Dirty() bool { - return de.extDirty || de.prebidDirty + return de.extDirty || de.prebidDirty || de.cdepDirty } func (de *DeviceExt) GetExt() map[string]json.RawMessage { @@ -968,6 +991,15 @@ func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) { de.prebidDirty = true } +func (de *DeviceExt) GetCDep() string { + return de.cdep +} + +func (de *DeviceExt) SetCDep(cdep string) { + de.cdep = cdep + de.cdepDirty = true +} + func (de *DeviceExt) Clone() *DeviceExt { if de == nil { return nil diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go index 425229e54e9..ffa925e46ac 100644 --- a/openrtb_ext/request_wrapper_test.go +++ b/openrtb_ext/request_wrapper_test.go @@ -767,13 +767,13 @@ func TestRebuildDeviceExt(t *testing.T) { { description: "Nil - Dirty", request: openrtb2.BidRequest{}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Nil - Dirty - No Change", request: openrtb2.BidRequest{}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{}, }, { @@ -785,37 +785,37 @@ func TestRebuildDeviceExt(t *testing.T) { { description: "Empty - Dirty", request: openrtb2.BidRequest{Device: &openrtb2.Device{}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Empty - Dirty - No Change", request: openrtb2.BidRequest{Device: &openrtb2.Device{}}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{}}, }, { description: "Populated - Not Dirty", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, requestDeviceExtWrapper: DeviceExt{}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent2, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":2,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent2, prebidDirty: true, cdep: "2", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"2","prebid":{"interstitial":{"minwidthperc":2,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty - No Change", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true}, - expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: &prebidContent1, prebidDirty: true, cdep: "1", cdepDirty: true}, + expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, }, { description: "Populated - Dirty - Cleared", - request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, - requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true}, + request: openrtb2.BidRequest{Device: &openrtb2.Device{Ext: json.RawMessage(`{"cdep":"1","prebid":{"interstitial":{"minwidthperc":1,"minheightperc":0}}}`)}}, + requestDeviceExtWrapper: DeviceExt{prebid: nil, prebidDirty: true, cdep: "", cdepDirty: true}, expectedRequest: openrtb2.BidRequest{Device: &openrtb2.Device{}}, }, } @@ -1014,6 +1014,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, devExtCopy: &DeviceExt{ ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, @@ -1021,6 +1023,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, mutator: func(t *testing.T, devExt *DeviceExt) {}, }, @@ -1032,6 +1036,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65.0, MinHeightPerc: 75.0}, }, + cdep: "1", + cdepDirty: true, }, devExtCopy: &DeviceExt{ ext: map[string]json.RawMessage{"A": json.RawMessage(`{}`), "B": json.RawMessage(`{"foo":"bar"}`)}, @@ -1039,6 +1045,8 @@ func TestCloneDeviceExt(t *testing.T) { prebid: &ExtDevicePrebid{ Interstitial: &ExtDeviceInt{MinWidthPerc: 65, MinHeightPerc: 75}, }, + cdep: "1", + cdepDirty: true, }, mutator: func(t *testing.T, devExt *DeviceExt) { devExt.ext["A"] = json.RawMessage(`"string"`) @@ -1047,6 +1055,8 @@ func TestCloneDeviceExt(t *testing.T) { devExt.prebid.Interstitial.MinHeightPerc = 55 devExt.prebid.Interstitial = &ExtDeviceInt{MinWidthPerc: 80} devExt.prebid = nil + devExt.cdep = "" + devExt.cdepDirty = true }, }, } From 7c4f5f11bf767f946f6c0dd99a108203c0da1bd7 Mon Sep 17 00:00:00 2001 From: mustafa kemal Date: Thu, 7 Mar 2024 10:54:52 +0300 Subject: [PATCH 44/69] New Adapter: Theadx (#3498) Co-authored-by: mku --- adapters/theadx/params_test.go | 66 +++++++ adapters/theadx/theadx.go | 148 ++++++++++++++++ adapters/theadx/theadx_test.go | 20 +++ .../theadxtest/exemplary/dynamic-tag.json | 100 +++++++++++ .../theadxtest/exemplary/multi-format.json | 162 ++++++++++++++++++ .../theadxtest/exemplary/multi-native.json | 128 ++++++++++++++ .../theadxtest/exemplary/single-banner.json | 105 ++++++++++++ .../theadxtest/exemplary/single-native.json | 100 +++++++++++ .../theadxtest/exemplary/single-video.json | 103 +++++++++++ .../theadxtest/supplemental/bad-request.json | 48 ++++++ .../supplemental/empty-response.json | 42 +++++ .../supplemental/nobid-response.json | 49 ++++++ .../theadxtest/supplemental/server-error.json | 49 ++++++ .../supplemental/unparsable-response.json | 49 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_theadx.go | 12 ++ static/bidder-info/theadx.yaml | 23 +++ static/bidder-params/theadx.json | 32 ++++ 19 files changed, 1240 insertions(+) create mode 100644 adapters/theadx/params_test.go create mode 100644 adapters/theadx/theadx.go create mode 100644 adapters/theadx/theadx_test.go create mode 100644 adapters/theadx/theadxtest/exemplary/dynamic-tag.json create mode 100644 adapters/theadx/theadxtest/exemplary/multi-format.json create mode 100644 adapters/theadx/theadxtest/exemplary/multi-native.json create mode 100644 adapters/theadx/theadxtest/exemplary/single-banner.json create mode 100644 adapters/theadx/theadxtest/exemplary/single-native.json create mode 100644 adapters/theadx/theadxtest/exemplary/single-video.json create mode 100644 adapters/theadx/theadxtest/supplemental/bad-request.json create mode 100644 adapters/theadx/theadxtest/supplemental/empty-response.json create mode 100644 adapters/theadx/theadxtest/supplemental/nobid-response.json create mode 100644 adapters/theadx/theadxtest/supplemental/server-error.json create mode 100644 adapters/theadx/theadxtest/supplemental/unparsable-response.json create mode 100644 openrtb_ext/imp_theadx.go create mode 100644 static/bidder-info/theadx.yaml create mode 100644 static/bidder-params/theadx.json diff --git a/adapters/theadx/params_test.go b/adapters/theadx/params_test.go new file mode 100644 index 00000000000..c9e10e2f0be --- /dev/null +++ b/adapters/theadx/params_test.go @@ -0,0 +1,66 @@ +package theadx + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +// This file actually intends to test static/bidder-params/theadx.json +// +// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.theadx + +// TestValidParams makes sure that the theadx schema accepts all imp.ext fields which we intend to support. +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderTheadx, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected theadx params: %s", validParam) + } + } +} + +// TestInvalidParams makes sure that the theadx schema rejects all the imp.ext fields we don't support. +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderTheadx, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{"tagid":321}`, + `{"tagid":321,"wid":"456"}`, + `{"tagid":321,"pid":"12345"}`, + `{"tagid":321,"pname":"plc_mobile_300x250"}`, + `{"tagid":321,"inv":321,"mname":"pcl1"}`, + `{"tagid":"123","wid":"456","pid":"12345","pname":"plc_mobile_300x250"}`, +} + +var invalidParams = []string{ + ``, + `null`, + `true`, + `5`, + `4.2`, + `[]`, + `{}`, + `{"notmid":"123"}`, + `{"mid":"placementID"}`, + `{"inv":321,"mname":12345}`, + `{"inv":321}`, + `{"inv":"321"}`, + `{"mname":"12345"}`, + `{"mid":"123","priceType":"GROSS"}`, +} diff --git a/adapters/theadx/theadx.go b/adapters/theadx/theadx.go new file mode 100644 index 00000000000..5b3f019a724 --- /dev/null +++ b/adapters/theadx/theadx.go @@ -0,0 +1,148 @@ +package theadx + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint string +} + +// Builder builds a new instance of the theadx adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + bidder := &adapter{ + endpoint: config.Endpoint, + } + return bidder, nil +} +func getHeaders(request *openrtb2.BidRequest) http.Header { + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + headers.Add("X-Openrtb-Version", "2.5") + headers.Add("X-TEST", "1") + + if request.Device != nil { + if len(request.Device.UA) > 0 { + headers.Add("X-Device-User-Agent", request.Device.UA) + } + + if len(request.Device.IPv6) > 0 { + headers.Add("X-Real-IP", request.Device.IPv6) + } + + if len(request.Device.IP) > 0 { + headers.Add("X-Real-IP", request.Device.IP) + } + } + + return headers +} +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var errors []error + var validImps = make([]openrtb2.Imp, 0, len(request.Imp)) + + for _, imp := range request.Imp { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: err.Error(), + }) + continue + } + + var theadxImpExt openrtb_ext.ExtImpTheadx + if err := json.Unmarshal(bidderExt.Bidder, &theadxImpExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: err.Error(), + }) + continue + } + + imp.TagID = theadxImpExt.TagID.String() + validImps = append(validImps, imp) + } + + request.Imp = validImps + + requestJSON, err := json.Marshal(request) + if err != nil { + errors = append(errors, err) + return nil, errors + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: a.endpoint, + Body: requestJSON, + Headers: getHeaders(request), + } + + return []*adapters.RequestData{requestData}, errors +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if responseData.StatusCode == http.StatusNoContent { + return nil, nil + } + + if responseData.StatusCode == http.StatusBadRequest { + err := &errortypes.BadInput{ + Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", responseData.StatusCode), + } + return nil, []error{err} + } + + if responseData.StatusCode != http.StatusOK { + err := &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Unexpected status code: %d.", responseData.StatusCode), + } + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errors []error + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + errors = append(errors, err) + continue + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + return bidResponse, errors +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to parse impression \"%s\" mediatype", bid.ImpID), + } +} diff --git a/adapters/theadx/theadx_test.go b/adapters/theadx/theadx_test.go new file mode 100644 index 00000000000..e64126c6a10 --- /dev/null +++ b/adapters/theadx/theadx_test.go @@ -0,0 +1,20 @@ +package theadx + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderTheadx, config.Adapter{ + Endpoint: "https://ssp.theadx.com/request"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "theadxtest", bidder) +} diff --git a/adapters/theadx/theadxtest/exemplary/dynamic-tag.json b/adapters/theadx/theadxtest/exemplary/dynamic-tag.json new file mode 100644 index 00000000000..738a426d60c --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/dynamic-tag.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123", + "pname": "placement" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "test-imp-id2", + "ext": { + "bidder": { + "tagid": "123", + "wid": 456, + "pname": "placement1" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 300 + }] + } + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "tagid": "123", + "ext": { + "bidder": { + "tagid": "123", + "pname": "placement" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "test-imp-id2", + "tagid": "123", + "ext": { + "bidder": { + "tagid": "123", + "wid": 456, + "pname": "placement1" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 300 + }] + } + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + } + }, + "mockResponse": { + "status": 204 + } + }], + "expectedMakeRequestsErrors": [], + "expectedBidResponses": [] +} diff --git a/adapters/theadx/theadxtest/exemplary/multi-format.json b/adapters/theadx/theadxtest/exemplary/multi-format.json new file mode 100644 index 00000000000..8f188547d05 --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/multi-format.json @@ -0,0 +1,162 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id-1", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }, { + "id": "test-imp-id-2", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id-1", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "tagid": "123" + }, { + "id": "test-imp-id-2", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "tagid": "123" + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id-1", + "impid": "test-imp-id-1", + "price": 10, + "adm": "{video xml}", + "adomain": [], + "crid": "test-creative-id-1", + "ext": { + "prebid": { + "type": "video" + } + } + }] + }, { + "bid": [{ + "id": "test-bid-id-2", + "impid": "test-imp-id-2", + "price": 2, + "adm": "{banner html}", + "adomain": [ "ad-domain" ], + "crid": "test-creative-id-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }], + "cur": "TRY" + } + } + }], + "expectedBidResponses": [{ + "currency": "TRY", + "bids": [{ + "bid": { + "id": "test-bid-id-1", + "impid": "test-imp-id-1", + "price": 10, + "adm": "{video xml}", + "crid": "test-creative-id-1", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + }, { + "bid": { + "id": "test-bid-id-2", + "impid": "test-imp-id-2", + "price": 2, + "adm": "{banner html}", + "adomain": [ "ad-domain" ], + "crid": "test-creative-id-2", + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + }] + }] +} diff --git a/adapters/theadx/theadxtest/exemplary/multi-native.json b/adapters/theadx/theadxtest/exemplary/multi-native.json new file mode 100644 index 00000000000..d025b0ea742 --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/multi-native.json @@ -0,0 +1,128 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id-1", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "native": { + "request": "{json string 1}", + "ver": "1.2" + } + }, { + "id": "test-imp-id-2", + "ext": { + "bidder": { + "tagid": "124" + } + }, + "native": { + "request": "{json string 2}", + "ver": "1.2" + } + }] + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id-1", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "native": { + "request": "{json string 1}", + "ver": "1.2" + }, + "tagid": "123" + }, { + "id": "test-imp-id-2", + "ext": { + "bidder": { + "tagid": "124" + } + }, + "native": { + "request": "{json string 2}", + "ver": "1.2" + }, + "tagid": "124" + }] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id-1", + "impid": "test-imp-id-1", + "price": 10, + "adm": "{json response string 1}", + "adomain": [], + "crid": "test-creative-id-1", + "ext": { + "prebid": { + "type": "native" + } + } + }, { + "id": "test-bid-id-2", + "impid": "test-imp-id-2", + "price": 2, + "adm": "{json response string 2}", + "adomain": [ "ad-domain" ], + "crid": "test-creative-id-2", + "ext": { + "prebid": { + "type": "native" + } + } + }] + }], + "cur": "EUR" + } + } + }], + "expectedBidResponses": [{ + "currency": "EUR", + "bids": [{ + "bid": { + "id": "test-bid-id-1", + "impid": "test-imp-id-1", + "price": 10, + "adm": "{json response string 1}", + "crid": "test-creative-id-1", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + }, { + "bid": { + "id": "test-bid-id-2", + "impid": "test-imp-id-2", + "price": 2, + "adm": "{json response string 2}", + "adomain": [ "ad-domain" ], + "crid": "test-creative-id-2", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + }] + }] +} diff --git a/adapters/theadx/theadxtest/exemplary/single-banner.json b/adapters/theadx/theadxtest/exemplary/single-banner.json new file mode 100644 index 00000000000..32a4357dd6a --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/single-banner.json @@ -0,0 +1,105 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + } + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "banner": { + "format": [{ + "w": 300, + "h": 250 + }] + }, + "tagid": "123" + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{banner html}", + "adomain": [ "test.com" ], + "crid": "test-creative-id", + "ext": { + "prebid": { + "type": "banner" + } + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{banner html}", + "crid": "test-creative-id", + "adomain": [ "test.com" ], + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + }] +} diff --git a/adapters/theadx/theadxtest/exemplary/single-native.json b/adapters/theadx/theadxtest/exemplary/single-native.json new file mode 100644 index 00000000000..09e97dcfccd --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/single-native.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "native": { + "request": "{json string}", + "ver": "1.2" + } + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "native": { + "request": "{json string}", + "ver": "1.2" + }, + "tagid": "123" + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{json response string}", + "adomain": [], + "crid": "test-creative-id", + "ext": { + "prebid": { + "type": "native" + } + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{json response string}", + "crid": "test-creative-id", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + }] +} diff --git a/adapters/theadx/theadxtest/exemplary/single-video.json b/adapters/theadx/theadxtest/exemplary/single-video.json new file mode 100644 index 00000000000..9d3b5f77382 --- /dev/null +++ b/adapters/theadx/theadxtest/exemplary/single-video.json @@ -0,0 +1,103 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + } + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + }, + "httpCalls": [{ + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "ext": { + "bidder": { + "tagid": "123" + } + }, + "video": { + "mimes": [ + "video/mp4" + ], + "placement": 1 + }, + "tagid": "123" + }], + "site": { + "publisher": { + "id": "1" + }, + "page": "some-page-url" + }, + "device": { + "w": 1920, + "h": 800 + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [{ + "bid": [{ + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{vast xml}", + "crid": "test-creative-id", + "ext": { + "prebid": { + "type": "video" + } + } + }] + }], + "cur": "USD" + } + } + }], + "expectedBidResponses": [{ + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 10, + "adm": "{vast xml}", + "crid": "test-creative-id", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + }] +} diff --git a/adapters/theadx/theadxtest/supplemental/bad-request.json b/adapters/theadx/theadxtest/supplemental/bad-request.json new file mode 100644 index 00000000000..52aefa57c3f --- /dev/null +++ b/adapters/theadx/theadxtest/supplemental/bad-request.json @@ -0,0 +1,48 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "tagid": 12345 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "tagid": 12345 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + }, + "tagid": "12345" + }] + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/theadx/theadxtest/supplemental/empty-response.json b/adapters/theadx/theadxtest/supplemental/empty-response.json new file mode 100644 index 00000000000..43546563dcd --- /dev/null +++ b/adapters/theadx/theadxtest/supplemental/empty-response.json @@ -0,0 +1,42 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "tagid": 123 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "tagid": 123 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + }, + "tagid": "123" + }] + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/theadx/theadxtest/supplemental/nobid-response.json b/adapters/theadx/theadxtest/supplemental/nobid-response.json new file mode 100644 index 00000000000..14e15de4cd1 --- /dev/null +++ b/adapters/theadx/theadxtest/supplemental/nobid-response.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "tagid": 123 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "tagid": 123 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + }, + "tagid": "123" + }] + } + }, + "mockResponse": { + "status": 204, + "body": { + "id": "test-request-id", + "seatbid": null, + "bidid": null, + "cur": null + } + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} diff --git a/adapters/theadx/theadxtest/supplemental/server-error.json b/adapters/theadx/theadxtest/supplemental/server-error.json new file mode 100644 index 00000000000..5130aba4e91 --- /dev/null +++ b/adapters/theadx/theadxtest/supplemental/server-error.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "tagid": 123 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "tagid": 123 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + }, + "tagid": "123" + }] + } + }, + "mockResponse": { + "status": 500, + "body": "Server error" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500.", + "comparison": "literal" + } + ] +} diff --git a/adapters/theadx/theadxtest/supplemental/unparsable-response.json b/adapters/theadx/theadxtest/supplemental/unparsable-response.json new file mode 100644 index 00000000000..2affa437fea --- /dev/null +++ b/adapters/theadx/theadxtest/supplemental/unparsable-response.json @@ -0,0 +1,49 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "tagid": 123 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://ssp.theadx.com/request", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "tagid": 123 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + }, + "tagid": "123" + }] + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index e188d2936e2..90a0059ca9f 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -171,6 +171,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/tappx" "github.com/prebid/prebid-server/v2/adapters/teads" "github.com/prebid/prebid-server/v2/adapters/telaria" + "github.com/prebid/prebid-server/v2/adapters/theadx" "github.com/prebid/prebid-server/v2/adapters/tpmn" "github.com/prebid/prebid-server/v2/adapters/trafficgate" "github.com/prebid/prebid-server/v2/adapters/triplelift" @@ -375,6 +376,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderTappx: tappx.Builder, openrtb_ext.BidderTeads: teads.Builder, openrtb_ext.BidderTelaria: telaria.Builder, + openrtb_ext.BidderTheadx: theadx.Builder, openrtb_ext.BidderTpmn: tpmn.Builder, openrtb_ext.BidderTrafficGate: trafficgate.Builder, openrtb_ext.BidderTriplelift: triplelift.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index fcd2b15ce22..f1493add953 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -189,6 +189,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderTappx, BidderTeads, BidderTelaria, + BidderTheadx, BidderTpmn, BidderTrafficGate, BidderTriplelift, @@ -469,6 +470,7 @@ const ( BidderTappx BidderName = "tappx" BidderTeads BidderName = "teads" BidderTelaria BidderName = "telaria" + BidderTheadx BidderName = "theadx" BidderTpmn BidderName = "tpmn" BidderTrafficGate BidderName = "trafficgate" BidderTriplelift BidderName = "triplelift" diff --git a/openrtb_ext/imp_theadx.go b/openrtb_ext/imp_theadx.go new file mode 100644 index 00000000000..bee3ac6088a --- /dev/null +++ b/openrtb_ext/imp_theadx.go @@ -0,0 +1,12 @@ +package openrtb_ext + +import ( + "encoding/json" +) + +type ExtImpTheadx struct { + TagID json.Number `json:"tagid"` + InventorySourceID int `json:"wid,omitempty"` + MemberID int `json:"pid,optional,omitempty"` + PlacementName string `json:"pname,optional,omitempty"` +} diff --git a/static/bidder-info/theadx.yaml b/static/bidder-info/theadx.yaml new file mode 100644 index 00000000000..01d3312a561 --- /dev/null +++ b/static/bidder-info/theadx.yaml @@ -0,0 +1,23 @@ +endpoint: "https://ssp.theadx.com/request?pbs=1" +maintainer: + email: "ssp@theadx.com" +gvlVendorID: 556 +debug: + allow: true +capabilities: + app: + mediaTypes: + - banner + - native + - video + - audio + site: + mediaTypes: + - banner + - native + - video + - audio +userSync: + redirect: + url: "https://ssp.theadx.com/cookie?redirect_url={{.RedirectURL}}&?pbs=1" + userMacro: "$UID" diff --git a/static/bidder-params/theadx.json b/static/bidder-params/theadx.json new file mode 100644 index 00000000000..cdbbf22f96b --- /dev/null +++ b/static/bidder-params/theadx.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Theadx Adapter Params", + "description": "A schema which validates params accepted by the theadx adapter", + "type": "object", + "properties": { + "pid": { + "type": ["integer", "string"], + "pattern": "^\\d+$", + "description": "An ID which identifies the partner selling the impression" + }, + "tagid": { + "type": ["integer", "string"], + "pattern": "^\\d+$", + "description": "An ID which identifies the placement selling the impression" + }, + "wid": { + "type": ["integer", "string"], + "description": "An ID which identifies the Theadx inventory source id" + } + + }, + "anyOf":[ + { + "required": ["tagid"] + } + , { + "required": ["pid", "wid","tagid"] + } + + ] +} From aff36081afe301ea1dd39e89a35053e02935c321 Mon Sep 17 00:00:00 2001 From: Piotr Jaworski <109736938+piotrj-rtbh@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:06:02 +0100 Subject: [PATCH 45/69] RTB House: regional endpoints (#3551) --- static/bidder-info/rtbhouse.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/static/bidder-info/rtbhouse.yaml b/static/bidder-info/rtbhouse.yaml index 1f8b131678d..fd9df062b15 100644 --- a/static/bidder-info/rtbhouse.yaml +++ b/static/bidder-info/rtbhouse.yaml @@ -1,4 +1,15 @@ +# Contact prebid@rtbhouse.com to ask about enabling a connection to the bidder. +# Please configure the following endpoints for your datacenter +# EMEA endpoint: "http://prebidserver-s2s-ams.creativecdn.com/bidder/prebidserver/bids" +# US East +# endpoint: "http://prebidserver-s2s-ash.creativecdn.com/bidder/prebidserver/bids" +# US West +# endpoint: "http://prebidserver-s2s-phx.creativecdn.com/bidder/prebidserver/bids" +# APAC +# endpoint: "http://prebidserver-s2s-sin.creativecdn.com/bidder/prebidserver/bids" +geoscope: + - global maintainer: email: "prebid@rtbhouse.com" endpointCompression: gzip From e2a9806c37fdddd19b732d869ef1ab441e681a0f Mon Sep 17 00:00:00 2001 From: IQZoneAdx <88879712+IQZoneAdx@users.noreply.github.com> Date: Thu, 7 Mar 2024 10:25:17 +0200 Subject: [PATCH 46/69] iqzone get media type from mtype bid property (#3557) --- adapters/iqzone/iqzone.go | 29 +++++++------------ .../iqzonetest/exemplary/endpointId.json | 2 ++ .../iqzonetest/exemplary/simple-banner.json | 2 ++ .../iqzonetest/exemplary/simple-native.json | 4 ++- .../iqzonetest/exemplary/simple-video.json | 2 ++ .../exemplary/simple-web-banner.json | 2 ++ .../supplemental/bad_media_type.json | 2 +- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/adapters/iqzone/iqzone.go b/adapters/iqzone/iqzone.go index 1888711df93..099d11106b3 100644 --- a/adapters/iqzone/iqzone.go +++ b/adapters/iqzone/iqzone.go @@ -111,7 +111,7 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R bidResponse.Currency = response.Cur for _, seatBid := range response.SeatBid { for i := range seatBid.Bid { - bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, request.Imp) + bidType, err := getBidMediaType(&seatBid.Bid[i]) if err != nil { return nil, []error{err} } @@ -126,22 +126,15 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R return bidResponse, nil } -func getMediaTypeForImp(impID string, imps []openrtb2.Imp) (openrtb_ext.BidType, error) { - for _, imp := range imps { - if imp.ID == impID { - if imp.Banner != nil { - return openrtb_ext.BidTypeBanner, nil - } - if imp.Banner == nil && imp.Video != nil { - return openrtb_ext.BidTypeVideo, nil - } - if imp.Banner == nil && imp.Video == nil && imp.Native != nil { - return openrtb_ext.BidTypeNative, nil - } - } - } - - return "", &errortypes.BadInput{ - Message: fmt.Sprintf("Failed to find impression \"%s\"", impID), +func getBidMediaType(bid *openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("Unable to fetch mediaType in multi-format: %s", bid.ImpID) } } diff --git a/adapters/iqzone/iqzonetest/exemplary/endpointId.json b/adapters/iqzone/iqzonetest/exemplary/endpointId.json index 79887443a89..dc8a0010bee 100644 --- a/adapters/iqzone/iqzonetest/exemplary/endpointId.json +++ b/adapters/iqzone/iqzonetest/exemplary/endpointId.json @@ -90,6 +90,7 @@ "dealid": "test_dealid", "w": 300, "h": 250, + "mtype": 1, "ext": { "prebid": { "type": "banner" @@ -119,6 +120,7 @@ "dealid": "test_dealid", "w": 300, "h": 250, + "mtype": 1, "ext": { "prebid": { "type": "banner" diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-banner.json b/adapters/iqzone/iqzonetest/exemplary/simple-banner.json index 12f97e1bb52..08d27d151cd 100644 --- a/adapters/iqzone/iqzonetest/exemplary/simple-banner.json +++ b/adapters/iqzone/iqzonetest/exemplary/simple-banner.json @@ -90,6 +90,7 @@ "dealid": "test_dealid", "w": 300, "h": 250, + "mtype": 1, "ext": { "prebid": { "type": "banner" @@ -119,6 +120,7 @@ "dealid": "test_dealid", "w": 300, "h": 250, + "mtype": 1, "ext": { "prebid": { "type": "banner" diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-native.json b/adapters/iqzone/iqzonetest/exemplary/simple-native.json index 967a846dd07..bf811487c7d 100644 --- a/adapters/iqzone/iqzonetest/exemplary/simple-native.json +++ b/adapters/iqzone/iqzonetest/exemplary/simple-native.json @@ -74,6 +74,7 @@ "dealid": "test_dealid", "w": 300, "h": 250, + "mtype": 4, "ext": { "prebid": { "type": "native" @@ -107,7 +108,8 @@ "prebid": { "type": "native" } - } + }, + "mtype": 4 }, "type": "native" } diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-video.json b/adapters/iqzone/iqzonetest/exemplary/simple-video.json index 7e2b7971e24..5130a6e651b 100644 --- a/adapters/iqzone/iqzonetest/exemplary/simple-video.json +++ b/adapters/iqzone/iqzonetest/exemplary/simple-video.json @@ -86,6 +86,7 @@ "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", + "mtype": 2, "ext": { "prebid": { "type": "video" @@ -114,6 +115,7 @@ "cid": "test_cid", "crid": "test_crid", "dealid": "test_dealid", + "mtype": 2, "ext": { "prebid": { "type": "video" diff --git a/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json b/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json index 4d69ef41336..96ff1b88aa8 100644 --- a/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json +++ b/adapters/iqzone/iqzonetest/exemplary/simple-web-banner.json @@ -90,6 +90,7 @@ "dealid": "test_dealid", "w": 468, "h": 60, + "mtype": 1, "ext": { "prebid": { "type": "banner" @@ -119,6 +120,7 @@ "dealid": "test_dealid", "w": 468, "h": 60, + "mtype": 1, "ext": { "prebid": { "type": "banner" diff --git a/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json b/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json index 4dddd568932..a2c886504b0 100644 --- a/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json +++ b/adapters/iqzone/iqzonetest/supplemental/bad_media_type.json @@ -79,7 +79,7 @@ }], "expectedMakeBidsErrors": [ { - "value": "Failed to find impression \"test-imp-id\"", + "value": "Unable to fetch mediaType in multi-format: test-imp-id", "comparison": "literal" } ] From 0362eb44e417f0cf1d9387a3795bba671a579068 Mon Sep 17 00:00:00 2001 From: dengxinjing <43231655+dengxinjing@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:22:42 +0800 Subject: [PATCH 47/69] New Adapter: Roulax (#3447) Co-authored-by: dengxinjing --- adapters/roulax/roulax.go | 115 +++++++++++++++++ adapters/roulax/roulax_test.go | 24 ++++ .../roulaxtest/exemplary/simple-banner.json | 115 +++++++++++++++++ .../roulaxtest/exemplary/simple-native.json | 97 ++++++++++++++ .../roulaxtest/exemplary/simple-video.json | 121 ++++++++++++++++++ .../supplemental/no-bid-response.json | 79 ++++++++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_roulax.go | 6 + static/bidder-info/roulax.yaml | 14 ++ static/bidder-params/roulax.json | 19 +++ 11 files changed, 594 insertions(+) create mode 100644 adapters/roulax/roulax.go create mode 100644 adapters/roulax/roulax_test.go create mode 100644 adapters/roulax/roulaxtest/exemplary/simple-banner.json create mode 100644 adapters/roulax/roulaxtest/exemplary/simple-native.json create mode 100644 adapters/roulax/roulaxtest/exemplary/simple-video.json create mode 100644 adapters/roulax/roulaxtest/supplemental/no-bid-response.json create mode 100644 openrtb_ext/imp_roulax.go create mode 100644 static/bidder-info/roulax.yaml create mode 100644 static/bidder-params/roulax.json diff --git a/adapters/roulax/roulax.go b/adapters/roulax/roulax.go new file mode 100644 index 00000000000..ea9e1e73cf2 --- /dev/null +++ b/adapters/roulax/roulax.go @@ -0,0 +1,115 @@ +package roulax + +import ( + "encoding/json" + "fmt" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "net/http" + "text/template" +) + +type adapter struct { + endpoint *template.Template +} + +// Builder builds a new instance of the Roulax adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +// getImpAdotExt parses and return first imp ext or nil +func getImpRoulaxExt(imp *openrtb2.Imp) (openrtb_ext.ExtImpRoulax, error) { + var extBidder adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &extBidder); err != nil { + return openrtb_ext.ExtImpRoulax{}, err + } + var extImpRoulax openrtb_ext.ExtImpRoulax + if err := json.Unmarshal(extBidder.Bidder, &extImpRoulax); err != nil { + return openrtb_ext.ExtImpRoulax{}, err + } + return extImpRoulax, nil +} + +// MakeRequests makes the HTTP requests which should be made to fetch bids. +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (res []*adapters.RequestData, errs []error) { + reqJson, err := json.Marshal(request) + if err != nil { + return nil, append(errs, err) + } + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + roulaxExt, err := getImpRoulaxExt(&request.Imp[0]) + if err != nil { + return nil, append(errs, err) + } + + endpointParams := macros.EndpointTemplateParams{AccountID: roulaxExt.Pid, PublisherID: roulaxExt.PublisherPath} + url, err := macros.ResolveMacros(a.endpoint, endpointParams) + if err != nil { + return nil, append(errs, err) + } + + return []*adapters.RequestData{{ + Method: "POST", + Uri: url, + Body: reqJson, + Headers: headers, + }}, errs +} + +// MakeBids unpacks the server's response into Bids. +func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + var errs []error + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + bidType, err := getMediaTypeForBid(bid) + if err != nil { + errs = append(errs, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + return bidResponse, errs +} + +func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + default: + return "", fmt.Errorf("Unable to fetch mediaType in impID: %s, mType: %d", bid.ImpID, bid.MType) + } +} diff --git a/adapters/roulax/roulax_test.go b/adapters/roulax/roulax_test.go new file mode 100644 index 00000000000..0b13b9874c4 --- /dev/null +++ b/adapters/roulax/roulax_test.go @@ -0,0 +1,24 @@ +package roulax + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +const testsDir = "roulaxtest" +const testsBidderEndpoint = "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner" + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder( + openrtb_ext.BidderRoulax, + config.Adapter{Endpoint: testsBidderEndpoint}, + config.Server{ExternalUrl: "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner", GvlID: 1, DataCenter: "2"}) + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, testsDir, bidder) +} diff --git a/adapters/roulax/roulaxtest/exemplary/simple-banner.json b/adapters/roulax/roulaxtest/exemplary/simple-banner.json new file mode 100644 index 00000000000..1f63e607fbf --- /dev/null +++ b/adapters/roulax/roulaxtest/exemplary/simple-banner.json @@ -0,0 +1,115 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 300 + } + ] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "mtype": 1 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + }] + } + ] +} diff --git a/adapters/roulax/roulaxtest/exemplary/simple-native.json b/adapters/roulax/roulaxtest/exemplary/simple-native.json new file mode 100644 index 00000000000..4c0170c37ae --- /dev/null +++ b/adapters/roulax/roulaxtest/exemplary/simple-native.json @@ -0,0 +1,97 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "test-native-request" + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "test-native-request" + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "mtype": 4 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "mtype": 4 + }, + "type": "native" + }] + } + ] +} \ No newline at end of file diff --git a/adapters/roulax/roulaxtest/exemplary/simple-video.json b/adapters/roulax/roulaxtest/exemplary/simple-video.json new file mode 100644 index 00000000000..8b3e30dfb21 --- /dev/null +++ b/adapters/roulax/roulaxtest/exemplary/simple-video.json @@ -0,0 +1,121 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.500000, + "adid": "29681110", + "adm": "some-test-ad", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "h": 250, + "w": 300, + "mtype": 2 + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [{ + "bid": { + "id": "7706636740145184841", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "adid": "29681110", + "adomain": [ + "yahoo.com" + ], + "cid": "958", + "crid": "29681110", + "w": 300, + "h": 250, + "mtype": 2 + }, + "type": "video" + }] + } + ] +} \ No newline at end of file diff --git a/adapters/roulax/roulaxtest/supplemental/no-bid-response.json b/adapters/roulax/roulaxtest/supplemental/no-bid-response.json new file mode 100644 index 00000000000..7d9ff2e6ebb --- /dev/null +++ b/adapters/roulax/roulaxtest/supplemental/no-bid-response.json @@ -0,0 +1,79 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://dsp.rcoreads.com/api/vidmate?pid=vidmate_android_banner", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 1, + "maxduration": 2, + "protocols": [1,3,5], + "w": 1020, + "h": 780, + "startdelay": 1, + "placement": 1, + "playbackmethod": [2], + "delivery": [1], + "api": [1,2,3,4] + }, + "ext": { + "bidder": { + "PublisherPath": "72721", + "Pid": "mvo", + "zone": "1r" + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [] + } + ] +} \ No newline at end of file diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 90a0059ca9f..57318c006da 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -147,6 +147,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/revcontent" "github.com/prebid/prebid-server/v2/adapters/richaudience" "github.com/prebid/prebid-server/v2/adapters/rise" + "github.com/prebid/prebid-server/v2/adapters/roulax" "github.com/prebid/prebid-server/v2/adapters/rtbhouse" "github.com/prebid/prebid-server/v2/adapters/rubicon" salunamedia "github.com/prebid/prebid-server/v2/adapters/sa_lunamedia" @@ -352,6 +353,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderRevcontent: revcontent.Builder, openrtb_ext.BidderRichaudience: richaudience.Builder, openrtb_ext.BidderRise: rise.Builder, + openrtb_ext.BidderRoulax: roulax.Builder, openrtb_ext.BidderRTBHouse: rtbhouse.Builder, openrtb_ext.BidderRubicon: rubicon.Builder, openrtb_ext.BidderSeedingAlliance: seedingAlliance.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index f1493add953..baf54f97a7f 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -165,6 +165,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderRevcontent, BidderRichaudience, BidderRise, + BidderRoulax, BidderRTBHouse, BidderRubicon, BidderSeedingAlliance, @@ -446,6 +447,7 @@ const ( BidderRevcontent BidderName = "revcontent" BidderRichaudience BidderName = "richaudience" BidderRise BidderName = "rise" + BidderRoulax BidderName = "roulax" BidderRTBHouse BidderName = "rtbhouse" BidderRubicon BidderName = "rubicon" BidderSeedingAlliance BidderName = "seedingAlliance" diff --git a/openrtb_ext/imp_roulax.go b/openrtb_ext/imp_roulax.go new file mode 100644 index 00000000000..8ce720f42ba --- /dev/null +++ b/openrtb_ext/imp_roulax.go @@ -0,0 +1,6 @@ +package openrtb_ext + +type ExtImpRoulax struct { + Pid string `json:"Pid,omitempty"` + PublisherPath string `json:"publisherPath,omitempty"` +} diff --git a/static/bidder-info/roulax.yaml b/static/bidder-info/roulax.yaml new file mode 100644 index 00000000000..646a9cbc77a --- /dev/null +++ b/static/bidder-info/roulax.yaml @@ -0,0 +1,14 @@ +endpoint: "http://dsp.rcoreads.com/api/{{.PublisherID}}?pid={{.AccountID}}" +maintainer: + email: "bussiness@roulax.io" +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native diff --git a/static/bidder-params/roulax.json b/static/bidder-params/roulax.json new file mode 100644 index 00000000000..adcf847c0be --- /dev/null +++ b/static/bidder-params/roulax.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Roulax Adapter Params", + "description": "A schema which validates params accepted by the Roulax adapter", + "type": "object", + "properties": { + "PId": { + "type": "string", + "minLength": 1, + "description": "PID" + }, + "PublisherPath": { + "type": "string", + "minLength": 1, + "description": "PublisherPath" + } + }, + "required": ["Pid", "PublisherPath"] +} \ No newline at end of file From 40fd4fffba95c0b919155792a6aacea498a74ce9 Mon Sep 17 00:00:00 2001 From: Scott Kay Date: Mon, 11 Mar 2024 13:32:11 -0400 Subject: [PATCH 48/69] Extract FPD Merge To Separate Package (#3533) --- firstpartydata/first_party_data.go | 170 +---- firstpartydata/first_party_data_test.go | 679 +----------------- ortb/merge/app.go | 61 ++ ortb/merge/app_test.go | 217 ++++++ {firstpartydata => ortb/merge}/extmerger.go | 9 +- .../merge}/extmerger_test.go | 26 +- ortb/merge/site.go | 61 ++ ortb/merge/site_test.go | 217 ++++++ ortb/merge/user.go | 37 + ortb/merge/user_test.go | 118 +++ 10 files changed, 757 insertions(+), 838 deletions(-) create mode 100644 ortb/merge/app.go create mode 100644 ortb/merge/app_test.go rename {firstpartydata => ortb/merge}/extmerger.go (89%) rename {firstpartydata => ortb/merge}/extmerger_test.go (81%) create mode 100644 ortb/merge/site.go create mode 100644 ortb/merge/site_test.go create mode 100644 ortb/merge/user.go create mode 100644 ortb/merge/user_test.go diff --git a/firstpartydata/first_party_data.go b/firstpartydata/first_party_data.go index 8c77f61a3d6..5234a693fc7 100644 --- a/firstpartydata/first_party_data.go +++ b/firstpartydata/first_party_data.go @@ -2,6 +2,7 @@ package firstpartydata import ( "encoding/json" + "errors" "fmt" "github.com/prebid/openrtb/v20/openrtb2" @@ -9,11 +10,14 @@ import ( "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/openrtb_ext" - "github.com/prebid/prebid-server/v2/ortb" - "github.com/prebid/prebid-server/v2/util/jsonutil" + "github.com/prebid/prebid-server/v2/ortb/merge" "github.com/prebid/prebid-server/v2/util/ptrutil" ) +var ( + ErrBadFPD = errors.New("invalid first party data ext") +) + const ( siteKey = "site" appKey = "app" @@ -195,7 +199,11 @@ func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, gl newUser.Data = openRtbGlobalFPD[userDataKey] } if fpdConfigUser != nil { - if err := mergeUser(newUser, fpdConfigUser); err != nil { + var err error + if newUser, err = merge.User(newUser, fpdConfigUser); err != nil { + if err == merge.ErrBadOverride { + return nil, ErrBadFPD + } return nil, err } } @@ -203,34 +211,6 @@ func resolveUser(fpdConfig *openrtb_ext.ORTB2, bidRequestUser *openrtb2.User, gl return newUser, nil } -func mergeUser(v *openrtb2.User, overrideJSON json.RawMessage) error { - *v = *ortb.CloneUser(v) - - // Track EXTs - // It's not necessary to track `ext` fields in array items because the array - // items will be replaced entirely with the override JSON, so no merge is required. - var ext, extGeo extMerger - ext.Track(&v.Ext) - if v.Geo != nil { - extGeo.Track(&v.Geo.Ext) - } - - // Merge - if err := jsonutil.Unmarshal(overrideJSON, &v); err != nil { - return err - } - - // Merge EXTs - if err := ext.Merge(); err != nil { - return err - } - if err := extGeo.Merge(); err != nil { - return err - } - - return nil -} - func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.Site, error) { var fpdConfigSite json.RawMessage @@ -278,70 +258,22 @@ func resolveSite(fpdConfig *openrtb_ext.ORTB2, bidRequestSite *openrtb2.Site, gl newSite.Content.Data = openRtbGlobalFPD[siteContentDataKey] } if fpdConfigSite != nil { - if err := mergeSite(newSite, fpdConfigSite, bidderName); err != nil { + var err error + if newSite, err = merge.Site(newSite, fpdConfigSite, bidderName); err != nil { + if err == merge.ErrBadOverride { + return nil, ErrBadFPD + } return nil, err } - } - return newSite, nil -} - -func mergeSite(v *openrtb2.Site, overrideJSON json.RawMessage, bidderName string) error { - *v = *ortb.CloneSite(v) - - // Track EXTs - // It's not necessary to track `ext` fields in array items because the array - // items will be replaced entirely with the override JSON, so no merge is required. - var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger - ext.Track(&v.Ext) - if v.Publisher != nil { - extPublisher.Track(&v.Publisher.Ext) - } - if v.Content != nil { - extContent.Track(&v.Content.Ext) - } - if v.Content != nil && v.Content.Producer != nil { - extContentProducer.Track(&v.Content.Producer.Ext) - } - if v.Content != nil && v.Content.Network != nil { - extContentNetwork.Track(&v.Content.Network.Ext) - } - if v.Content != nil && v.Content.Channel != nil { - extContentChannel.Track(&v.Content.Channel.Ext) - } - // Merge - if err := jsonutil.Unmarshal(overrideJSON, &v); err != nil { - return err - } - - // Merge EXTs - if err := ext.Merge(); err != nil { - return err - } - if err := extPublisher.Merge(); err != nil { - return err - } - if err := extContent.Merge(); err != nil { - return err - } - if err := extContentProducer.Merge(); err != nil { - return err - } - if err := extContentNetwork.Merge(); err != nil { - return err - } - if err := extContentChannel.Merge(); err != nil { - return err - } - - // Re-Validate Site - if v.ID == "" && v.Page == "" { - return &errortypes.BadInput{ - Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), + // Re-Validate Site + if newSite.ID == "" && newSite.Page == "" { + return nil, &errortypes.BadInput{ + Message: fmt.Sprintf("incorrect First Party Data for bidder %s: Site object cannot set empty page if req.site.id is empty", bidderName), + } } } - - return nil + return newSite, nil } func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globalFPD map[string][]byte, openRtbGlobalFPD map[string][]openrtb2.Data, bidderName string) (*openrtb2.App, error) { @@ -394,7 +326,11 @@ func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globa } if fpdConfigApp != nil { - if err := mergeApp(newApp, fpdConfigApp); err != nil { + var err error + if newApp, err = merge.App(newApp, fpdConfigApp); err != nil { + if err == merge.ErrBadOverride { + return nil, ErrBadFPD + } return nil, err } } @@ -402,58 +338,6 @@ func resolveApp(fpdConfig *openrtb_ext.ORTB2, bidRequestApp *openrtb2.App, globa return newApp, nil } -func mergeApp(v *openrtb2.App, overrideJSON json.RawMessage) error { - *v = *ortb.CloneApp(v) - - // Track EXTs - // It's not necessary to track `ext` fields in array items because the array - // items will be replaced entirely with the override JSON, so no merge is required. - var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger - ext.Track(&v.Ext) - if v.Publisher != nil { - extPublisher.Track(&v.Publisher.Ext) - } - if v.Content != nil { - extContent.Track(&v.Content.Ext) - } - if v.Content != nil && v.Content.Producer != nil { - extContentProducer.Track(&v.Content.Producer.Ext) - } - if v.Content != nil && v.Content.Network != nil { - extContentNetwork.Track(&v.Content.Network.Ext) - } - if v.Content != nil && v.Content.Channel != nil { - extContentChannel.Track(&v.Content.Channel.Ext) - } - - // Merge - if err := jsonutil.Unmarshal(overrideJSON, &v); err != nil { - return err - } - - // Merge EXTs - if err := ext.Merge(); err != nil { - return err - } - if err := extPublisher.Merge(); err != nil { - return err - } - if err := extContent.Merge(); err != nil { - return err - } - if err := extContentProducer.Merge(); err != nil { - return err - } - if err := extContentNetwork.Merge(); err != nil { - return err - } - if err := extContentChannel.Merge(); err != nil { - return err - } - - return nil -} - func buildExtData(data []byte) []byte { res := make([]byte, 0, len(data)+len(`"{"data":}"`)) res = append(res, []byte(`{"data":`)...) diff --git a/firstpartydata/first_party_data_test.go b/firstpartydata/first_party_data_test.go index f417a24d7e7..7bc52b5527f 100644 --- a/firstpartydata/first_party_data_test.go +++ b/firstpartydata/first_party_data_test.go @@ -4,14 +4,12 @@ import ( "encoding/json" "os" "path/filepath" - "reflect" "testing" "github.com/prebid/openrtb/v20/openrtb2" "github.com/prebid/prebid-server/v2/errortypes" "github.com/prebid/prebid-server/v2/openrtb_ext" "github.com/prebid/prebid-server/v2/util/jsonutil" - "github.com/prebid/prebid-server/v2/util/ptrutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -525,6 +523,7 @@ func TestExtractBidderConfigFPD(t *testing.T) { }) } } + func TestResolveFPD(t *testing.T) { testPath := "tests/resolvefpd" @@ -633,6 +632,7 @@ func TestResolveFPD(t *testing.T) { }) } } + func TestExtractFPDForBidders(t *testing.T) { if specFiles, err := os.ReadDir("./tests/extractfpdforbidders"); err == nil { for _, specFile := range specFiles { @@ -1215,681 +1215,6 @@ func TestBuildExtData(t *testing.T) { } } -func TestMergeUser(t *testing.T) { - testCases := []struct { - name string - givenUser openrtb2.User - givenFPD json.RawMessage - expectedUser openrtb2.User - expectError bool - }{ - { - name: "empty", - givenUser: openrtb2.User{}, - givenFPD: []byte(`{}`), - expectedUser: openrtb2.User{}, - }, - { - name: "toplevel", - givenUser: openrtb2.User{ID: "1"}, - givenFPD: []byte(`{"id":"2"}`), - expectedUser: openrtb2.User{ID: "2"}, - }, - { - name: "toplevel-ext", - givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`)}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), - expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, - }, - { - name: "toplevel-ext-err", - givenUser: openrtb2.User{ID: "1", Ext: []byte(`malformed`)}, - givenFPD: []byte(`{"id":"2"}`), - expectError: true, - }, - { - name: "nested-geo", - givenUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: ptrutil.ToPtr(1.0)}}, - givenFPD: []byte(`{"geo":{"lat": 2}}`), - expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: ptrutil.ToPtr(2.0)}}, - }, - { - name: "nested-geo-ext", - givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":2}`)}}, - givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), - expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, - }, - { - name: "toplevel-ext-and-nested-geo-ext", - givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":20}`)}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "geo":{"ext":{"b":100,"c":3}}}`), - expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, - }, - { - name: "nested-geo-ext-err", - givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`malformed`)}}, - givenFPD: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), - expectError: true, - }, - { - name: "fpd-err", - givenUser: openrtb2.User{ID: "1", Ext: []byte(`{"a":1}`)}, - givenFPD: []byte(`malformed`), - expectError: true, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - err := mergeUser(&test.givenUser, test.givenFPD) - - if test.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, test.expectedUser, test.givenUser, "result user is incorrect") - } - }) - } -} - -func TestMergeApp(t *testing.T) { - testCases := []struct { - name string - givenApp openrtb2.App - givenFPD json.RawMessage - expectedApp openrtb2.App - expectError bool - }{ - { - name: "empty", - givenApp: openrtb2.App{}, - givenFPD: []byte(`{}`), - expectedApp: openrtb2.App{}, - }, - { - name: "toplevel", - givenApp: openrtb2.App{ID: "1"}, - givenFPD: []byte(`{"id":"2"}`), - expectedApp: openrtb2.App{ID: "2"}, - }, - { - name: "toplevel-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`)}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, - }, - { - name: "toplevel-ext-err", - givenApp: openrtb2.App{ID: "1", Ext: []byte(`malformed`)}, - givenFPD: []byte(`{"id":"2"}`), - expectError: true, - }, - { - name: "nested-publisher", - givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub1"}}, - givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), - expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub2"}}, - }, - { - name: "nested-content", - givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1"}}, - givenFPD: []byte(`{"content":{"title": "content2"}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2"}}, - }, - { - name: "nested-content-producer", - givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, - }, - { - name: "nested-content-network", - givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, - }, - { - name: "nested-content-channel", - givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, - }, - { - name: "nested-publisher-ext", - givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, - givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), - expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, - }, - { - name: "nested-content-ext", - givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, - givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, - }, - { - name: "nested-content-producer-ext", - givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "nested-content-network-ext", - givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "nested-content-channel-ext", - givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-publisher-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, - }, - { - name: "toplevel-ext-and-nested-content-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, - }, - { - name: "toplevel-ext-and-nested-content-producer-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-content-network-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-content-channel-ext", - givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), - expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "nested-publisher-ext-err", - givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, - givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), - expectError: true, - }, - { - name: "nested-content-ext-err", - givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, - givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), - expectError: true, - }, - { - name: "nested-content-producer-ext-err", - givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "nested-content-network-ext-err", - givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "nested-content-channel-ext-err", - givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "fpd-err", - givenApp: openrtb2.App{ID: "1", Ext: []byte(`{"a":1}`)}, - givenFPD: []byte(`malformed`), - expectError: true, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - err := mergeApp(&test.givenApp, test.givenFPD) - - if test.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, test.expectedApp, test.givenApp, " result app is incorrect") - } - }) - } -} - -func TestMergeSite(t *testing.T) { - testCases := []struct { - name string - givenSite openrtb2.Site - givenFPD json.RawMessage - expectedSite openrtb2.Site - expectError bool - }{ - { - name: "empty", - givenSite: openrtb2.Site{}, - givenFPD: []byte(`{}`), - expectError: true, - }, - { - name: "toplevel", - givenSite: openrtb2.Site{ID: "1"}, - givenFPD: []byte(`{"id":"2"}`), - expectedSite: openrtb2.Site{ID: "2"}, - }, - { - name: "toplevel-ext", - givenSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":2}`)}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}}`), - expectedSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":100,"c":3}`)}, - }, - { - name: "toplevel-ext-err", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`malformed`)}, - givenFPD: []byte(`{"id":"2"}`), - expectError: true, - }, - { - name: "nested-publisher", - givenSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub1"}}, - givenFPD: []byte(`{"publisher":{"name": "pub2"}}`), - expectedSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub2"}}, - }, - { - name: "nested-content", - givenSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content1"}}, - givenFPD: []byte(`{"content":{"title": "content2"}}`), - expectedSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content2"}}, - }, - { - name: "nested-content-producer", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, - }, - { - name: "nested-content-network", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, - }, - { - name: "nested-content-channel", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, - givenFPD: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, - }, - { - name: "nested-publisher-ext", - givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, - givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), - expectedSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, - }, - { - name: "nested-content-ext", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, - givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, - }, - { - name: "nested-content-producer-ext", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "nested-content-network-ext", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "nested-content-channel-ext", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, - givenFPD: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-publisher-ext", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), - expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, - }, - { - name: "toplevel-ext-and-nested-content-ext", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), - expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, - }, - { - name: "toplevel-ext-and-nested-content-producer-ext", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-content-network-ext", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "toplevel-ext-and-nested-content-channel-ext", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, - givenFPD: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), - expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, - }, - { - name: "nested-publisher-ext-err", - givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, - givenFPD: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), - expectError: true, - }, - { - name: "nested-content-ext-err", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, - givenFPD: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), - expectError: true, - }, - { - name: "nested-content-producer-ext-err", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "nested-content-network-ext-err", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "nested-content-channel-ext-err", - givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, - givenFPD: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), - expectError: true, - }, - { - name: "fpd-err", - givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1}`)}, - givenFPD: []byte(`malformed`), - expectError: true, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - err := mergeSite(&test.givenSite, test.givenFPD, "BidderA") - - if test.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, test.expectedSite, test.givenSite, " result Site is incorrect") - } - }) - } -} - -// TestMergeObjectStructure detects when new nested objects are added to First Party Data supported -// fields, as these will invalidate the mergeSite, mergeApp, and mergeUser methods. If this test fails, -// fix the merge methods to add support and update this test to set a new baseline. -func TestMergeObjectStructure(t *testing.T) { - testCases := []struct { - name string - kind any - knownStructs []string - }{ - { - name: "Site", - kind: openrtb2.Site{}, - knownStructs: []string{ - "Publisher", - "Content", - "Content.Producer", - "Content.Network", - "Content.Channel", - }, - }, - { - name: "App", - kind: openrtb2.App{}, - knownStructs: []string{ - "Publisher", - "Content", - "Content.Producer", - "Content.Network", - "Content.Channel", - }, - }, - { - name: "User", - kind: openrtb2.User{}, - knownStructs: []string{ - "Geo", - }, - }, - } - - for _, test := range testCases { - t.Run(test.name, func(t *testing.T) { - nestedStructs := []string{} - - var discover func(parent string, t reflect.Type) - discover = func(parent string, t reflect.Type) { - fields := reflect.VisibleFields(t) - for _, field := range fields { - if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { - nestedStructs = append(nestedStructs, parent+field.Name) - discover(parent+field.Name+".", field.Type.Elem()) - } - } - } - discover("", reflect.TypeOf(test.kind)) - - assert.ElementsMatch(t, test.knownStructs, nestedStructs) - }) - } -} - -// user memory protect test -func TestMergeUserMemoryProtection(t *testing.T) { - inputGeo := &openrtb2.Geo{ - Ext: json.RawMessage(`{"a":1,"b":2}`), - } - input := openrtb2.User{ - ID: "1", - Geo: inputGeo, - } - - err := mergeUser(&input, userFPD) - assert.NoError(t, err) - - // Input user object is expected to be a copy. Changes are ok. - assert.Equal(t, "2", input.ID, "user-id-copied") - - // Nested objects must be copied before changes. - assert.JSONEq(t, `{"a":1,"b":2}`, string(inputGeo.Ext), "geo-input") - assert.JSONEq(t, `{"a":1,"b":100,"c":3}`, string(input.Geo.Ext), "geo-copied") -} - -// app memory protect test -func TestMergeAppMemoryProtection(t *testing.T) { - inputPublisher := &openrtb2.Publisher{ - ID: "InPubId", - Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), - } - inputContent := &openrtb2.Content{ - ID: "InContentId", - Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), - Producer: &openrtb2.Producer{ - ID: "InProducerId", - Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), - }, - Network: &openrtb2.Network{ - ID: "InNetworkId", - Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), - }, - Channel: &openrtb2.Channel{ - ID: "InChannelId", - Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), - }, - } - input := openrtb2.App{ - ID: "InAppID", - Publisher: inputPublisher, - Content: inputContent, - Ext: json.RawMessage(`{"a": "inputAppExt", "b": 1}`), - } - - err := mergeApp(&input, fpdWithPublisherAndContent) - assert.NoError(t, err) - - // Input app object is expected to be a copy. Changes are ok. - assert.Equal(t, "FPDID", input.ID, "app-id-copied") - assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "app-ext-copied") - - // Nested objects must be copied before changes. - assert.Equal(t, "InPubId", inputPublisher.ID, "app-pub-id-input") - assert.Equal(t, "FPDPubId", input.Publisher.ID, "app-pub-id-copied") - assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "app-pub-ext-input") - assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "app-pub-ext-copied") - - assert.Equal(t, "InContentId", inputContent.ID, "app-content-id-input") - assert.Equal(t, "FPDContentId", input.Content.ID, "app-content-id-copied") - assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "app-content-ext-input") - assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "app-content-ext-copied") - - assert.Equal(t, "InProducerId", inputContent.Producer.ID, "app-content-producer-id-input") - assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "app-content-producer-id-copied") - assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "app-content-producer-ext-input") - assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "app-content-producer-ext-copied") - - assert.Equal(t, "InNetworkId", inputContent.Network.ID, "app-content-network-id-input") - assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "app-content-network-id-copied") - assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "app-content-network-ext-input") - assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "app-content-network-ext-copied") - - assert.Equal(t, "InChannelId", inputContent.Channel.ID, "app-content-channel-id-input") - assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "app-content-channel-id-copied") - assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "app-content-channel-ext-input") - assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "app-content-channel-ext-copied") -} - -// site memory protect test -func TestMergeSiteMemoryProtection(t *testing.T) { - inputPublisher := &openrtb2.Publisher{ - ID: "InPubId", - Ext: json.RawMessage(`{"a": "inputPubExt", "b": 1}`), - } - inputContent := &openrtb2.Content{ - ID: "InContentId", - Ext: json.RawMessage(`{"a": "inputContentExt", "b": 1}`), - Producer: &openrtb2.Producer{ - ID: "InProducerId", - Ext: json.RawMessage(`{"a": "inputProducerExt", "b": 1}`), - }, - Network: &openrtb2.Network{ - ID: "InNetworkId", - Ext: json.RawMessage(`{"a": "inputNetworkExt", "b": 1}`), - }, - Channel: &openrtb2.Channel{ - ID: "InChannelId", - Ext: json.RawMessage(`{"a": "inputChannelExt", "b": 1}`), - }, - } - input := openrtb2.Site{ - ID: "InSiteID", - Publisher: inputPublisher, - Content: inputContent, - Ext: json.RawMessage(`{"a": "inputSiteExt", "b": 1}`), - } - - err := mergeSite(&input, fpdWithPublisherAndContent, "BidderA") - assert.NoError(t, err) - - // Input app object is expected to be a copy. Changes are ok. - assert.Equal(t, "FPDID", input.ID, "site-id-copied") - assert.JSONEq(t, `{"a": "FPDExt", "b": 2}`, string(input.Ext), "site-ext-copied") - - // Nested objects must be copied before changes. - assert.Equal(t, "InPubId", inputPublisher.ID, "site-pub-id-input") - assert.Equal(t, "FPDPubId", input.Publisher.ID, "site-pub-id-copied") - assert.JSONEq(t, `{"a": "inputPubExt", "b": 1}`, string(inputPublisher.Ext), "site-pub-ext-input") - assert.JSONEq(t, `{"a": "FPDPubExt", "b": 2}`, string(input.Publisher.Ext), "site-pub-ext-copied") - - assert.Equal(t, "InContentId", inputContent.ID, "site-content-id-input") - assert.Equal(t, "FPDContentId", input.Content.ID, "site-content-id-copied") - assert.JSONEq(t, `{"a": "inputContentExt", "b": 1}`, string(inputContent.Ext), "site-content-ext-input") - assert.JSONEq(t, `{"a": "FPDContentExt", "b": 2}`, string(input.Content.Ext), "site-content-ext-copied") - - assert.Equal(t, "InProducerId", inputContent.Producer.ID, "site-content-producer-id-input") - assert.Equal(t, "FPDProducerId", input.Content.Producer.ID, "site-content-producer-id-copied") - assert.JSONEq(t, `{"a": "inputProducerExt", "b": 1}`, string(inputContent.Producer.Ext), "site-content-producer-ext-input") - assert.JSONEq(t, `{"a": "FPDProducerExt", "b": 2}`, string(input.Content.Producer.Ext), "site-content-producer-ext-copied") - - assert.Equal(t, "InNetworkId", inputContent.Network.ID, "site-content-network-id-input") - assert.Equal(t, "FPDNetworkId", input.Content.Network.ID, "site-content-network-id-copied") - assert.JSONEq(t, `{"a": "inputNetworkExt", "b": 1}`, string(inputContent.Network.Ext), "site-content-network-ext-input") - assert.JSONEq(t, `{"a": "FPDNetworkExt", "b": 2}`, string(input.Content.Network.Ext), "site-content-network-ext-copied") - - assert.Equal(t, "InChannelId", inputContent.Channel.ID, "site-content-channel-id-input") - assert.Equal(t, "FPDChannelId", input.Content.Channel.ID, "site-content-channel-id-copied") - assert.JSONEq(t, `{"a": "inputChannelExt", "b": 1}`, string(inputContent.Channel.Ext), "site-content-channel-ext-input") - assert.JSONEq(t, `{"a": "FPDChannelExt", "b": 2}`, string(input.Content.Channel.Ext), "site-content-channel-ext-copied") -} - -var ( - userFPD = []byte(` -{ - "id": "2", - "geo": { - "ext": { - "b": 100, - "c": 3 - } - } -} -`) - - fpdWithPublisherAndContent = []byte(` -{ - "id": "FPDID", - "ext": {"a": "FPDExt", "b": 2}, - "publisher": { - "id": "FPDPubId", - "ext": {"a": "FPDPubExt", "b": 2} - }, - "content": { - "id": "FPDContentId", - "ext": {"a": "FPDContentExt", "b": 2}, - "producer": { - "id": "FPDProducerId", - "ext": {"a": "FPDProducerExt", "b": 2} - }, - "network": { - "id": "FPDNetworkId", - "ext": {"a": "FPDNetworkExt", "b": 2} - }, - "channel": { - "id": "FPDChannelId", - "ext": {"a": "FPDChannelExt", "b": 2} - } - } -} -`) -) - func loadTestFile[T any](filename string) (T, error) { var testFile T diff --git a/ortb/merge/app.go b/ortb/merge/app.go new file mode 100644 index 00000000000..0a2976e858a --- /dev/null +++ b/ortb/merge/app.go @@ -0,0 +1,61 @@ +package merge + +import ( + "encoding/json" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +func App(v *openrtb2.App, overrideJSON json.RawMessage) (*openrtb2.App, error) { + c := ortb.CloneApp(v) + + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger + ext.Track(&c.Ext) + if c.Publisher != nil { + extPublisher.Track(&c.Publisher.Ext) + } + if c.Content != nil { + extContent.Track(&c.Content.Ext) + } + if c.Content != nil && c.Content.Producer != nil { + extContentProducer.Track(&c.Content.Producer.Ext) + } + if c.Content != nil && c.Content.Network != nil { + extContentNetwork.Track(&c.Content.Network.Ext) + } + if c.Content != nil && c.Content.Channel != nil { + extContentChannel.Track(&c.Content.Channel.Ext) + } + + // Merge + if err := jsonutil.Unmarshal(overrideJSON, &c); err != nil { + return nil, err + } + + // Merge EXTs + if err := ext.Merge(); err != nil { + return nil, err + } + if err := extPublisher.Merge(); err != nil { + return nil, err + } + if err := extContent.Merge(); err != nil { + return nil, err + } + if err := extContentProducer.Merge(); err != nil { + return nil, err + } + if err := extContentNetwork.Merge(); err != nil { + return nil, err + } + if err := extContentChannel.Merge(); err != nil { + return nil, err + } + + return c, nil +} diff --git a/ortb/merge/app_test.go b/ortb/merge/app_test.go new file mode 100644 index 00000000000..69dc851ffdb --- /dev/null +++ b/ortb/merge/app_test.go @@ -0,0 +1,217 @@ +package merge + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/stretchr/testify/assert" +) + +func TestApp(t *testing.T) { + testCases := []struct { + name string + givenApp openrtb2.App + givenJson json.RawMessage + expectedApp openrtb2.App + expectError bool + }{ + { + name: "empty", + givenApp: openrtb2.App{}, + givenJson: []byte(`{}`), + expectedApp: openrtb2.App{}, + }, + { + name: "toplevel", + givenApp: openrtb2.App{ID: "1"}, + givenJson: []byte(`{"id":"2"}`), + expectedApp: openrtb2.App{ID: "2"}, + }, + { + name: "toplevel-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`)}, + givenJson: []byte(`{"ext":{"b":100,"c":3}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenApp: openrtb2.App{ID: "1", Ext: []byte(`malformed`)}, + givenJson: []byte(`{"id":"2"}`), + expectError: true, + }, + { + name: "nested-publisher", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub1"}}, + givenJson: []byte(`{"publisher":{"name": "pub2"}}`), + expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Name: "pub2"}}, + }, + { + name: "nested-content", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1"}}, + givenJson: []byte(`{"content":{"title": "content2"}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2"}}, + }, + { + name: "nested-content-producer", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, + }, + { + name: "nested-content-network", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, + }, + { + name: "nested-content-channel", + givenApp: openrtb2.App{Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, + }, + { + name: "nested-publisher-ext", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, + givenJson: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, + givenJson: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-producer-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-network-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-channel-ext", + givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-publisher-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-producer-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-network-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-channel-ext", + givenApp: openrtb2.App{Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), + expectedApp: openrtb2.App{Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "nested-publisher-ext-err", + givenApp: openrtb2.App{Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, + givenJson: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectError: true, + }, + { + name: "nested-content-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, + givenJson: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectError: true, + }, + { + name: "nested-content-producer-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "nested-content-network-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "nested-content-channel-ext-err", + givenApp: openrtb2.App{Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "json-err", + givenApp: openrtb2.App{ID: "1", Ext: []byte(`{"a":1}`)}, + givenJson: []byte(`malformed`), + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + originalApp := ortb.CloneApp(&test.givenApp) + merged, err := App(&test.givenApp, test.givenJson) + + assert.Equal(t, &test.givenApp, originalApp) + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, &test.expectedApp, merged) + } + }) + } +} + +// TestAppObjectStructure detects when new nested objects are added to the App object, +// as these will create a gap in the merge.App logic. If this test fails, fix merge.App +// to add support and update this test to set a new baseline. +func TestAppObjectStructure(t *testing.T) { + knownNestedStructs := []string{ + "Publisher", + "Content", + "Content.Producer", + "Content.Network", + "Content.Channel", + } + + discoveredNestedStructs := []string{} + + var discover func(parent string, t reflect.Type) + discover = func(parent string, t reflect.Type) { + fields := reflect.VisibleFields(t) + for _, field := range fields { + if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { + discoveredNestedStructs = append(discoveredNestedStructs, parent+field.Name) + discover(parent+field.Name+".", field.Type.Elem()) + } + } + } + discover("", reflect.TypeOf(openrtb2.App{})) + + assert.ElementsMatch(t, knownNestedStructs, discoveredNestedStructs) +} diff --git a/firstpartydata/extmerger.go b/ortb/merge/extmerger.go similarity index 89% rename from firstpartydata/extmerger.go rename to ortb/merge/extmerger.go index f3196bea996..dd7da425cdc 100644 --- a/firstpartydata/extmerger.go +++ b/ortb/merge/extmerger.go @@ -1,17 +1,16 @@ -package firstpartydata +package merge import ( "encoding/json" "errors" - "fmt" "github.com/prebid/prebid-server/v2/util/sliceutil" jsonpatch "gopkg.in/evanphx/json-patch.v4" ) var ( - ErrBadRequest = fmt.Errorf("invalid request ext") - ErrBadFPD = fmt.Errorf("invalid first party data ext") + ErrBadRequest = errors.New("invalid request ext") + ErrBadOverride = errors.New("invalid override ext") ) // extMerger tracks a JSON `ext` field within an OpenRTB request. The value of the @@ -50,7 +49,7 @@ func (e extMerger) Merge() error { if errors.Is(err, jsonpatch.ErrBadJSONDoc) { return ErrBadRequest } else if errors.Is(err, jsonpatch.ErrBadJSONPatch) { - return ErrBadFPD + return ErrBadOverride } return err } diff --git a/firstpartydata/extmerger_test.go b/ortb/merge/extmerger_test.go similarity index 81% rename from firstpartydata/extmerger_test.go rename to ortb/merge/extmerger_test.go index 4107b0d1144..9340887ac9a 100644 --- a/firstpartydata/extmerger_test.go +++ b/ortb/merge/extmerger_test.go @@ -1,4 +1,4 @@ -package firstpartydata +package merge import ( "encoding/json" @@ -18,62 +18,62 @@ func TestExtMerger(t *testing.T) { testCases := []struct { name string givenOriginal json.RawMessage - givenFPD json.RawMessage + givenJson json.RawMessage expectedExt json.RawMessage expectedErr string }{ { name: "both-populated", givenOriginal: json.RawMessage(`{"a":1,"b":2}`), - givenFPD: json.RawMessage(`{"b":200,"c":3}`), + givenJson: json.RawMessage(`{"b":200,"c":3}`), expectedExt: json.RawMessage(`{"a":1,"b":200,"c":3}`), }, { name: "both-nil", - givenFPD: nil, + givenJson: nil, givenOriginal: nil, expectedExt: nil, }, { name: "both-empty", givenOriginal: json.RawMessage(`{}`), - givenFPD: json.RawMessage(`{}`), + givenJson: json.RawMessage(`{}`), expectedExt: json.RawMessage(`{}`), }, { name: "ext-nil", givenOriginal: json.RawMessage(`{"b":2}`), - givenFPD: nil, + givenJson: nil, expectedExt: json.RawMessage(`{"b":2}`), }, { name: "ext-empty", givenOriginal: json.RawMessage(`{"b":2}`), - givenFPD: json.RawMessage(`{}`), + givenJson: json.RawMessage(`{}`), expectedExt: json.RawMessage(`{"b":2}`), }, { name: "ext-malformed", givenOriginal: json.RawMessage(`{"b":2}`), - givenFPD: json.RawMessage(`malformed`), - expectedErr: "invalid first party data ext", + givenJson: json.RawMessage(`malformed`), + expectedErr: "invalid override ext", }, { name: "snapshot-nil", givenOriginal: nil, - givenFPD: json.RawMessage(`{"a":1}`), + givenJson: json.RawMessage(`{"a":1}`), expectedExt: json.RawMessage(`{"a":1}`), }, { name: "snapshot-empty", givenOriginal: json.RawMessage(`{}`), - givenFPD: json.RawMessage(`{"a":1}`), + givenJson: json.RawMessage(`{"a":1}`), expectedExt: json.RawMessage(`{"a":1}`), }, { name: "snapshot-malformed", givenOriginal: json.RawMessage(`malformed`), - givenFPD: json.RawMessage(`{"a":1}`), + givenJson: json.RawMessage(`{"a":1}`), expectedErr: "invalid request ext", }, } @@ -88,7 +88,7 @@ func TestExtMerger(t *testing.T) { merger.Track(&simulatedExt) // Unmarshal - simulatedExt.UnmarshalJSON(test.givenFPD) + simulatedExt.UnmarshalJSON(test.givenJson) // Merge actualErr := merger.Merge() diff --git a/ortb/merge/site.go b/ortb/merge/site.go new file mode 100644 index 00000000000..3e059acf282 --- /dev/null +++ b/ortb/merge/site.go @@ -0,0 +1,61 @@ +package merge + +import ( + "encoding/json" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +func Site(v *openrtb2.Site, overrideJSON json.RawMessage, bidderName string) (*openrtb2.Site, error) { + c := ortb.CloneSite(v) + + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extPublisher, extContent, extContentProducer, extContentNetwork, extContentChannel extMerger + ext.Track(&c.Ext) + if c.Publisher != nil { + extPublisher.Track(&c.Publisher.Ext) + } + if c.Content != nil { + extContent.Track(&c.Content.Ext) + } + if c.Content != nil && c.Content.Producer != nil { + extContentProducer.Track(&c.Content.Producer.Ext) + } + if c.Content != nil && c.Content.Network != nil { + extContentNetwork.Track(&c.Content.Network.Ext) + } + if c.Content != nil && c.Content.Channel != nil { + extContentChannel.Track(&c.Content.Channel.Ext) + } + + // Merge + if err := jsonutil.Unmarshal(overrideJSON, &c); err != nil { + return nil, err + } + + // Merge EXTs + if err := ext.Merge(); err != nil { + return nil, err + } + if err := extPublisher.Merge(); err != nil { + return nil, err + } + if err := extContent.Merge(); err != nil { + return nil, err + } + if err := extContentProducer.Merge(); err != nil { + return nil, err + } + if err := extContentNetwork.Merge(); err != nil { + return nil, err + } + if err := extContentChannel.Merge(); err != nil { + return nil, err + } + + return c, nil +} diff --git a/ortb/merge/site_test.go b/ortb/merge/site_test.go new file mode 100644 index 00000000000..800886239f4 --- /dev/null +++ b/ortb/merge/site_test.go @@ -0,0 +1,217 @@ +package merge + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/stretchr/testify/assert" +) + +func TestSite(t *testing.T) { + testCases := []struct { + name string + givenSite openrtb2.Site + givenJson json.RawMessage + expectedSite openrtb2.Site + expectError bool + }{ + { + name: "empty", + givenSite: openrtb2.Site{}, + givenJson: []byte(`{}`), + expectedSite: openrtb2.Site{}, + }, + { + name: "toplevel", + givenSite: openrtb2.Site{ID: "1"}, + givenJson: []byte(`{"id":"2"}`), + expectedSite: openrtb2.Site{ID: "2"}, + }, + { + name: "toplevel-ext", + givenSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":2}`)}, + givenJson: []byte(`{"ext":{"b":100,"c":3}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`malformed`)}, + givenJson: []byte(`{"id":"2"}`), + expectError: true, + }, + { + name: "nested-publisher", + givenSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub1"}}, + givenJson: []byte(`{"publisher":{"name": "pub2"}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Publisher: &openrtb2.Publisher{Name: "pub2"}}, + }, + { + name: "nested-content", + givenSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content1"}}, + givenJson: []byte(`{"content":{"title": "content2"}}`), + expectedSite: openrtb2.Site{Page: "test.com/page", Content: &openrtb2.Content{Title: "content2"}}, + }, + { + name: "nested-content-producer", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Producer: &openrtb2.Producer{Name: "producer1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "producer":{"name":"producer2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Producer: &openrtb2.Producer{Name: "producer2"}}}, + }, + { + name: "nested-content-network", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Network: &openrtb2.Network{Name: "network1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "network":{"name":"network2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Network: &openrtb2.Network{Name: "network2"}}}, + }, + { + name: "nested-content-channel", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content1", Channel: &openrtb2.Channel{Name: "channel1"}}}, + givenJson: []byte(`{"content":{"title": "content2", "channel":{"name":"channel2"}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Title: "content2", Channel: &openrtb2.Channel{Name: "channel2"}}}, + }, + { + name: "nested-publisher-ext", + givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":2}`)}}, + givenJson: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":2}`)}}, + givenJson: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "nested-content-producer-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"producer":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-network-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"network":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "nested-content-channel-ext", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":2}`)}}}, + givenJson: []byte(`{"content":{"channel":{"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-publisher-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":20}`)}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "publisher":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Publisher: &openrtb2.Publisher{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":20}`)}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"ext":{"b":100,"c":3}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-content-producer-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-network-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "toplevel-ext-and-nested-content-channel-ext", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":2}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":20}`)}}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "content":{"channel": {"ext":{"b":100,"c":3}}}}`), + expectedSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1,"b":100,"c":3}`), Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}}, + }, + { + name: "nested-publisher-ext-err", + givenSite: openrtb2.Site{ID: "1", Publisher: &openrtb2.Publisher{Ext: []byte(`malformed`)}}, + givenJson: []byte(`{"publisher":{"ext":{"b":100,"c":3}}}`), + expectError: true, + }, + { + name: "nested-content-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Ext: []byte(`malformed`)}}, + givenJson: []byte(`{"content":{"ext":{"b":100,"c":3}}}`), + expectError: true, + }, + { + name: "nested-content-producer-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Producer: &openrtb2.Producer{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"producer": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "nested-content-network-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Network: &openrtb2.Network{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"network": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "nested-content-channel-ext-err", + givenSite: openrtb2.Site{ID: "1", Content: &openrtb2.Content{Channel: &openrtb2.Channel{Ext: []byte(`malformed`)}}}, + givenJson: []byte(`{"content":{"channelx": {"ext":{"b":100,"c":3}}}}`), + expectError: true, + }, + { + name: "json-err", + givenSite: openrtb2.Site{ID: "1", Ext: []byte(`{"a":1}`)}, + givenJson: []byte(`malformed`), + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + originalSite := ortb.CloneSite(&test.givenSite) + merged, err := Site(&test.givenSite, test.givenJson, "BidderA") + + assert.Equal(t, &test.givenSite, originalSite) + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, &test.expectedSite, merged, " result Site is incorrect") + } + }) + } +} + +// TestSiteObjectStructure detects when new nested objects are added to the Site object, +// as these will create a gap in the merge.Site logic. If this test fails, fix merge.Site +// to add support and update this test to set a new baseline. +func TestSiteObjectStructure(t *testing.T) { + knownNestedStructs := []string{ + "Publisher", + "Content", + "Content.Producer", + "Content.Network", + "Content.Channel", + } + + discoveredNestedStructs := []string{} + + var discover func(parent string, t reflect.Type) + discover = func(parent string, t reflect.Type) { + fields := reflect.VisibleFields(t) + for _, field := range fields { + if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { + discoveredNestedStructs = append(discoveredNestedStructs, parent+field.Name) + discover(parent+field.Name+".", field.Type.Elem()) + } + } + } + discover("", reflect.TypeOf(openrtb2.Site{})) + + assert.ElementsMatch(t, knownNestedStructs, discoveredNestedStructs) +} diff --git a/ortb/merge/user.go b/ortb/merge/user.go new file mode 100644 index 00000000000..e2834500846 --- /dev/null +++ b/ortb/merge/user.go @@ -0,0 +1,37 @@ +package merge + +import ( + "encoding/json" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +func User(v *openrtb2.User, overrideJSON json.RawMessage) (*openrtb2.User, error) { + c := ortb.CloneUser(v) + + // Track EXTs + // It's not necessary to track `ext` fields in array items because the array + // items will be replaced entirely with the override JSON, so no merge is required. + var ext, extGeo extMerger + ext.Track(&c.Ext) + if c.Geo != nil { + extGeo.Track(&c.Geo.Ext) + } + + // Merge + if err := jsonutil.Unmarshal(overrideJSON, &c); err != nil { + return nil, err + } + + // Merge EXTs + if err := ext.Merge(); err != nil { + return nil, err + } + if err := extGeo.Merge(); err != nil { + return nil, err + } + + return c, nil +} diff --git a/ortb/merge/user_test.go b/ortb/merge/user_test.go new file mode 100644 index 00000000000..3dd7ddf2dcc --- /dev/null +++ b/ortb/merge/user_test.go @@ -0,0 +1,118 @@ +package merge + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/ortb" + "github.com/prebid/prebid-server/v2/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +func TestUser(t *testing.T) { + testCases := []struct { + name string + givenUser openrtb2.User + givenJson json.RawMessage + expectedUser openrtb2.User + expectError bool + }{ + { + name: "empty", + givenUser: openrtb2.User{}, + givenJson: []byte(`{}`), + expectedUser: openrtb2.User{}, + }, + { + name: "toplevel", + givenUser: openrtb2.User{ID: "1"}, + givenJson: []byte(`{"id":"2"}`), + expectedUser: openrtb2.User{ID: "2"}, + }, + { + name: "toplevel-ext", + givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`)}, + givenJson: []byte(`{"ext":{"b":100,"c":3}}`), + expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`)}, + }, + { + name: "toplevel-ext-err", + givenUser: openrtb2.User{ID: "1", Ext: []byte(`malformed`)}, + givenJson: []byte(`{"id":"2"}`), + expectError: true, + }, + { + name: "nested-geo", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: ptrutil.ToPtr(1.0)}}, + givenJson: []byte(`{"geo":{"lat": 2}}`), + expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Lat: ptrutil.ToPtr(2.0)}}, + }, + { + name: "nested-geo-ext", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":2}`)}}, + givenJson: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), + expectedUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`{"a":1,"b":100,"c":3}`)}}, + }, + { + name: "toplevel-ext-and-nested-geo-ext", + givenUser: openrtb2.User{Ext: []byte(`{"a":1,"b":2}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":20}`)}}, + givenJson: []byte(`{"ext":{"b":100,"c":3}, "geo":{"ext":{"b":100,"c":3}}}`), + expectedUser: openrtb2.User{Ext: []byte(`{"a":1,"b":100,"c":3}`), Geo: &openrtb2.Geo{Ext: []byte(`{"a":10,"b":100,"c":3}`)}}, + }, + { + name: "nested-geo-ext-err", + givenUser: openrtb2.User{Geo: &openrtb2.Geo{Ext: []byte(`malformed`)}}, + givenJson: []byte(`{"geo":{"ext":{"b":100,"c":3}}}`), + expectError: true, + }, + { + name: "json-err", + givenUser: openrtb2.User{ID: "1", Ext: []byte(`{"a":1}`)}, + givenJson: []byte(`malformed`), + expectError: true, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + originalUser := ortb.CloneUser(&test.givenUser) + merged, err := User(&test.givenUser, test.givenJson) + + assert.Equal(t, &test.givenUser, originalUser) + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, &test.expectedUser, merged, "result user is incorrect") + } + }) + } +} + +// TestUserObjectStructure detects when new nested objects are added to the User object, +// as these will create a gap in the merge.User logic. If this test fails, fix merge.User +// to add support and update this test to set a new baseline. +func TestUserObjectStructure(t *testing.T) { + knownNestedStructs := []string{ + "Geo", + } + + discoveredNestedStructs := []string{} + + var discover func(parent string, t reflect.Type) + discover = func(parent string, t reflect.Type) { + fields := reflect.VisibleFields(t) + for _, field := range fields { + if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct { + discoveredNestedStructs = append(discoveredNestedStructs, parent+field.Name) + discover(parent+field.Name+".", field.Type.Elem()) + } + } + } + discover("", reflect.TypeOf(openrtb2.User{})) + + assert.ElementsMatch(t, knownNestedStructs, discoveredNestedStructs) +} From 063101a09ba1ab43c2ba76cf60af7cec5c120b0e Mon Sep 17 00:00:00 2001 From: e-volution-tech <61746103+e-volution-tech@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:10:03 +0200 Subject: [PATCH 49/69] Evolution Bid Adapter: add iframe to userSync (#3561) --- static/bidder-info/e_volution.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/bidder-info/e_volution.yaml b/static/bidder-info/e_volution.yaml index b9d5532ca04..aa33e4660d1 100644 --- a/static/bidder-info/e_volution.yaml +++ b/static/bidder-info/e_volution.yaml @@ -17,4 +17,6 @@ userSync: redirect: url: "https://sync.e-volution.ai/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redirect={{.RedirectURL}}" userMacro: "[UID]" - + iframe: + url: "https://sync.e-volution.ai/pbserverIframe?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&pbserverUrl={{.RedirectURL}}" + userMacro: "[UID]" From c13178991cbdd2ee8c62531c17295cbd85f103c1 Mon Sep 17 00:00:00 2001 From: xmgiddev <133856186+xmgiddev@users.noreply.github.com> Date: Tue, 12 Mar 2024 11:10:09 +0200 Subject: [PATCH 50/69] MgidX Bid Adapter: add disabled param (#3562) Co-authored-by: gaudeamus Co-authored-by: Evgeny Nagorny Co-authored-by: xmgiddev <> --- static/bidder-info/mgidX.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/bidder-info/mgidX.yaml b/static/bidder-info/mgidX.yaml index ab783beb560..bbef148389f 100644 --- a/static/bidder-info/mgidX.yaml +++ b/static/bidder-info/mgidX.yaml @@ -1,4 +1,7 @@ -endpoint: "https://us-east-x.mgid.com/pserver" +disabled: true +# We have the following regional endpoint domains: 'us-east-x' and 'eu' +# Please deploy this config in each of your datacenters with the appropriate regional subdomain +endpoint: "https://REGION.mgid.com/pserver" maintainer: email: "prebid@mgid.com" gvlVendorID: 358 @@ -8,7 +11,6 @@ capabilities: - banner - video - native - app: mediaTypes: - banner From e78ffb48a6b7afd7be56959a6a60fa20056783de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20M=C3=BCller?= Date: Tue, 12 Mar 2024 18:16:09 +0100 Subject: [PATCH 51/69] New Analytics Adapter: agma (#3400) * Init agma * Updated imports from v19 to v20 * Change default url * Fixed typo in README * Fixed buffers typo in readme * Check for Market Research Puropse on Consent * Fix Typo in sruct * Removes errir check on buffer write * Extract App and Sites lookup into an extra function * Adds test for checking the StatusCode --- analytics/agma/README.md | 28 ++ analytics/agma/agma_module.go | 266 ++++++++++++ analytics/agma/agma_module_test.go | 666 +++++++++++++++++++++++++++++ analytics/agma/model.go | 50 +++ analytics/agma/model_test.go | 46 ++ analytics/agma/sender.go | 84 ++++ analytics/agma/sender_test.go | 133 ++++++ analytics/build/build.go | 14 + analytics/build/build_test.go | 35 +- config/config.go | 38 +- config/config_test.go | 34 +- 11 files changed, 1390 insertions(+), 4 deletions(-) create mode 100644 analytics/agma/README.md create mode 100644 analytics/agma/agma_module.go create mode 100644 analytics/agma/agma_module_test.go create mode 100644 analytics/agma/model.go create mode 100644 analytics/agma/model_test.go create mode 100644 analytics/agma/sender.go create mode 100644 analytics/agma/sender_test.go diff --git a/analytics/agma/README.md b/analytics/agma/README.md new file mode 100644 index 00000000000..cc9736feb85 --- /dev/null +++ b/analytics/agma/README.md @@ -0,0 +1,28 @@ +# agma Analytics + +In order to use the Agma Analytics Adapter, please adjust the accounts / endpoint with the data provided by agma (https://www.agma-mmc.de). + +## Configuration + +```yaml +analytics: + agma: + # Required: enable the module + enabled: true + # Required: set the accounts you want to track + accounts: + - code: "my-code" # Required: provied by agma + publisher_id: "123" # Required: Exchange specific publisher_id + site_app_id: "openrtb2-site.id-or-app.id" # optional: scope to the publisher with an openrtb2 Site object id or App object id + # Optional properties (advanced configuration) + endpoint: + url: "https://go.pbs.agma-analytics.de/v1/prebid-server" # Check with agma if your site needs an extra url + timeout: "2s" + gzip: true + buffers: # Flush events when (first condition reached) + # Size of the buffer in bytes + size: "2MB" # greater than 2MB (size using SI standard eg. "44kB", "17MB") + count : 100 # greater than 100 events + timeout: "15m" # greater than 15 minutes (parsed as golang duration) + +``` diff --git a/analytics/agma/agma_module.go b/analytics/agma/agma_module.go new file mode 100644 index 00000000000..534c189d914 --- /dev/null +++ b/analytics/agma/agma_module.go @@ -0,0 +1,266 @@ +package agma + +import ( + "bytes" + "errors" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/benbjohnson/clock" + "github.com/docker/go-units" + "github.com/golang/glog" + "github.com/prebid/go-gdpr/vendorconsent" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type httpSender = func(payload []byte) error + +const ( + agmaGVLID = 1122 + p9 = 9 +) + +type AgmaLogger struct { + sender httpSender + clock clock.Clock + accounts []config.AgmaAnalyticsAccount + eventCount int64 + maxEventCount int64 + maxBufferByteSize int64 + maxDuration time.Duration + mux sync.RWMutex + sigTermCh chan os.Signal + buffer bytes.Buffer + bufferCh chan []byte +} + +func newAgmaLogger(cfg config.AgmaAnalytics, sender httpSender, clock clock.Clock) (*AgmaLogger, error) { + pSize, err := units.FromHumanSize(cfg.Buffers.BufferSize) + if err != nil { + return nil, err + } + pDuration, err := time.ParseDuration(cfg.Buffers.Timeout) + if err != nil { + return nil, err + } + if len(cfg.Accounts) == 0 { + return nil, errors.New("Please configure at least one account for Agma Analytics") + } + + buffer := bytes.Buffer{} + buffer.Write([]byte("[")) + + return &AgmaLogger{ + sender: sender, + clock: clock, + accounts: cfg.Accounts, + maxBufferByteSize: pSize, + eventCount: 0, + maxEventCount: int64(cfg.Buffers.EventCount), + maxDuration: pDuration, + buffer: buffer, + bufferCh: make(chan []byte), + sigTermCh: make(chan os.Signal, 1), + }, nil +} + +func NewModule(httpClient *http.Client, cfg config.AgmaAnalytics, clock clock.Clock) (analytics.Module, error) { + sender, err := createHttpSender(httpClient, cfg.Endpoint) + if err != nil { + return nil, err + } + + m, err := newAgmaLogger(cfg, sender, clock) + if err != nil { + return nil, err + } + + signal.Notify(m.sigTermCh, os.Interrupt, syscall.SIGTERM) + + go m.start() + + return m, nil +} + +func (l *AgmaLogger) start() { + ticker := l.clock.Ticker(l.maxDuration) + for { + select { + case <-l.sigTermCh: + glog.Infof("[AgmaAnalytics] Received Close, trying to flush buffer") + l.flush() + return + case event := <-l.bufferCh: + l.bufferEvent(event) + if l.isFull() { + l.flush() + } + case <-ticker.C: + l.flush() + } + } +} + +func (l *AgmaLogger) bufferEvent(data []byte) { + l.mux.Lock() + defer l.mux.Unlock() + + l.buffer.Write(data) + l.buffer.WriteByte(',') + l.eventCount++ +} + +func (l *AgmaLogger) isFull() bool { + l.mux.RLock() + defer l.mux.RUnlock() + return l.eventCount >= l.maxEventCount || int64(l.buffer.Len()) >= l.maxBufferByteSize +} + +func (l *AgmaLogger) flush() { + l.mux.Lock() + + if l.eventCount == 0 || l.buffer.Len() == 0 { + l.mux.Unlock() + return + } + + // Close the json array, remove last , + l.buffer.Truncate(l.buffer.Len() - 1) + l.buffer.Write([]byte("]")) + + payload := make([]byte, l.buffer.Len()) + _, err := l.buffer.Read(payload) + if err != nil { + l.reset() + l.mux.Unlock() + glog.Warning("[AgmaAnalytics] fail to copy the buffer") + return + } + + go l.sender(payload) + + l.reset() + l.mux.Unlock() +} + +func (l *AgmaLogger) reset() { + l.buffer.Reset() + l.buffer.Write([]byte("[")) + l.eventCount = 0 +} + +func (l *AgmaLogger) extractPublisherAndSite(requestWrapper *openrtb_ext.RequestWrapper) (string, string) { + publisherId := "" + appSiteId := "" + if requestWrapper.Site != nil { + if requestWrapper.Site.Publisher != nil { + publisherId = requestWrapper.Site.Publisher.ID + } + appSiteId = requestWrapper.Site.ID + } + if requestWrapper.App != nil { + if requestWrapper.App.Publisher != nil { + publisherId = requestWrapper.App.Publisher.ID + } + appSiteId = requestWrapper.App.ID + } + return publisherId, appSiteId +} + +func (l *AgmaLogger) shouldTrackEvent(requestWrapper *openrtb_ext.RequestWrapper) (bool, string) { + userExt, err := requestWrapper.GetUserExt() + if err != nil || userExt == nil { + return false, "" + } + consent := userExt.GetConsent() + if consent == nil { + return false, "" + } + consentStr := *consent + parsedConsent, err := vendorconsent.ParseString(consentStr) + if err != nil { + return false, "" + } + + p9Allowed := parsedConsent.PurposeAllowed(p9) + agmaAllowed := parsedConsent.VendorConsent(agmaGVLID) + if !p9Allowed || !agmaAllowed { + return false, "" + } + + publisherId, appSiteId := l.extractPublisherAndSite(requestWrapper) + if publisherId == "" && appSiteId == "" { + return false, "" + } + + for _, account := range l.accounts { + if account.PublisherId == publisherId { + if account.SiteAppId == "" { + return true, account.Code + } + if account.SiteAppId == appSiteId { + return true, account.Code + } + } + } + + return false, "" +} + +func (l *AgmaLogger) LogAuctionObject(event *analytics.AuctionObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAuction, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing auction object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) LogAmpObject(event *analytics.AmpObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAmp, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing amp object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) LogVideoObject(event *analytics.VideoObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeVideo, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing video object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgmaLogger) LogCookieSyncObject(event *analytics.CookieSyncObject) {} +func (l *AgmaLogger) LogNotificationEventObject(event *analytics.NotificationEvent) {} +func (l *AgmaLogger) LogSetUIDObject(event *analytics.SetUIDObject) {} diff --git a/analytics/agma/agma_module_test.go b/analytics/agma/agma_module_test.go new file mode 100644 index 00000000000..3e4955bd8a5 --- /dev/null +++ b/analytics/agma/agma_module_test.go @@ -0,0 +1,666 @@ +package agma + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "syscall" + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var agmaConsent = "CP6-v9RP6-v9RNlAAAENCZCAAICAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A" + +var mockValidAuctionObject = analytics.AuctionObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidVideoObject = analytics.VideoObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "track-me-app", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidAmpObject = analytics.AmpObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidAccounts = []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "track-me-app", + }, + { + PublisherId: "track-me", + Code: "abcd", + SiteAppId: "track-me-site", + }, +} + +type MockedSender struct { + mock.Mock +} + +func (m *MockedSender) Send(payload []byte) error { + args := m.Called(payload) + return args.Error(0) +} + +func TestConfigParsingError(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + shouldFail bool + }{ + { + name: "Test with invalid/empty URL", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "%%2815197306101420000%29", + Timeout: "1s", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with invalid timout", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1x", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with no accounts", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1s", + Gzip: false, + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{}, + }, + shouldFail: true, + }, + } + clockMock := clock.NewMock() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewModule(&http.Client{}, tc.config, clockMock) + if tc.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestShouldTrackEvent(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + // no userExt + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-not", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // no userExt + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No agma + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIwgAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No Purpose 9 + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAIAAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // No valid sites / apps / empty publisher app + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "", + Publisher: &openrtb2.Publisher{ + ID: "", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) +} + +func TestShouldTrackMultipleAccounts(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me-a", + Code: "abc", + }, + { + PublisherId: "track-me-b", + Code: "123", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-a", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "abc", code) + + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "site-test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-b", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "123", code) +} + +func TestShouldNotTrackLog(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + }{ + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "do-not-track", + Code: "abc", + }, + }, + }, + }, + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "do-not-track", + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(tc.config, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + clockMock.Add(2 * time.Minute) + mockedSender.AssertNumberOfCalls(t, "Send", 0) + assert.Zero(t, logger.eventCount) + }) + } +} + +func TestRaceAllEvents(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + clockMock.Add(10 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(3), logger.eventCount) + logger.mux.RUnlock() +} + +func TestFlushOnSigterm(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + done := make(chan struct{}) + go func() { + logger.start() + close(done) + }() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + logger.sigTermCh <- syscall.SIGTERM + <-done + + time.Sleep(100 * time.Millisecond) + + mockedSender.AssertCalled(t, "Send", mock.Anything) +} + +func TestRaceBufferCount(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + // Test EventCount Buffer + logger.LogAuctionObject(&mockValidAuctionObject) + + clockMock.Add(1 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(1), logger.eventCount) + logger.mux.RUnlock() + + assert.Equal(t, false, logger.isFull()) + + // add 1 more + logger.LogAuctionObject(&mockValidAuctionObject) + clockMock.Add(1 * time.Millisecond) + + // should trigger send and flash the buffer + mockedSender.AssertCalled(t, "Send", mock.Anything) + + logger.mux.RLock() + assert.Equal(t, int64(0), logger.eventCount) + logger.mux.RUnlock() +} + +func TestBufferSize(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "20Kb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 50; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Millisecond) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestBufferTime(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 5; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Minute) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestRaceEnd2End(t *testing.T) { + var mu sync.Mutex + + requestBodyAsString := "" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check for reponse + requestBody, err := io.ReadAll(r.Body) + mu.Lock() + requestBodyAsString = string(requestBody) + mu.Unlock() + if err != nil { + http.Error(w, "Error reading request body", 500) + return + } + + w.WriteHeader(http.StatusOK) + })) + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: server.URL, + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + + clockMock := clock.NewMock() + clockMock.Set(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)) + + logger, err := NewModule(&http.Client{}, cfg, clockMock) + assert.NoError(t, err) + + logger.LogAmpObject(&mockValidAmpObject) + logger.LogAmpObject(&mockValidAmpObject) + + time.Sleep(250 * time.Millisecond) + + expected := "[{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"" + agmaConsent + "\"}},\"created_at\":\"2023-02-01T00:00:00Z\"},{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"" + agmaConsent + "\"}},\"created_at\":\"2023-02-01T00:00:00Z\"}]" + + mu.Lock() + actual := requestBodyAsString + mu.Unlock() + + assert.Equal(t, expected, actual) +} diff --git a/analytics/agma/model.go b/analytics/agma/model.go new file mode 100644 index 00000000000..a0f5f55f08f --- /dev/null +++ b/analytics/agma/model.go @@ -0,0 +1,50 @@ +package agma + +import ( + "fmt" + "time" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type EventType string + +const ( + EventTypeAuction EventType = "auction" + EventTypeAmp EventType = "amp" + EventTypeVideo EventType = "video" +) + +type logObject struct { + EventType EventType `json:"type"` + RequestId string `json:"id"` + AccountCode string `json:"code"` + Site *openrtb2.Site `json:"site,omitempty"` + App *openrtb2.App `json:"app,omitempty"` + Device *openrtb2.Device `json:"device,omitempty"` + User *openrtb2.User `json:"user,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func serializeAnayltics( + requestwrapper *openrtb_ext.RequestWrapper, + eventType EventType, + code string, + createdAt time.Time, +) ([]byte, error) { + if requestwrapper == nil || requestwrapper.BidRequest == nil { + return nil, fmt.Errorf("requestwrapper or BidRequest object nil") + } + return jsonutil.Marshal(&logObject{ + EventType: eventType, + RequestId: requestwrapper.ID, + AccountCode: code, + Site: requestwrapper.BidRequest.Site, + App: requestwrapper.BidRequest.App, + Device: requestwrapper.BidRequest.Device, + User: requestwrapper.BidRequest.User, + CreatedAt: createdAt, + }) +} diff --git a/analytics/agma/model_test.go b/analytics/agma/model_test.go new file mode 100644 index 00000000000..ea1134c2320 --- /dev/null +++ b/analytics/agma/model_test.go @@ -0,0 +1,46 @@ +package agma + +import ( + "testing" + "time" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestCheckForNil(t *testing.T) { + code := "test" + _, err := serializeAnayltics(nil, EventTypeAuction, code, time.Now()) + assert.Error(t, err) +} + +func TestSerializeAuctionObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeAuction, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"auction\"") +} + +func TestSerializeVideoObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeVideo, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"video\"") +} + +func TestSerializeAmpObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeAmp, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"amp\"") +} diff --git a/analytics/agma/sender.go b/analytics/agma/sender.go new file mode 100644 index 00000000000..6cdc5ea1526 --- /dev/null +++ b/analytics/agma/sender.go @@ -0,0 +1,84 @@ +package agma + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/golang/glog" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/version" +) + +func compressToGZIP(requestBody []byte) ([]byte, error) { + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write([]byte(requestBody)) + if err != nil { + _ = w.Close() + return nil, err + } + err = w.Close() + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func createHttpSender(httpClient *http.Client, endpoint config.AgmaAnalyticsHttpEndpoint) (httpSender, error) { + _, err := url.Parse(endpoint.Url) + if err != nil { + return nil, err + } + + httpTimeout, err := time.ParseDuration(endpoint.Timeout) + if err != nil { + return nil, err + } + + return func(payload []byte) error { + ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) + defer cancel() + + var requestBody []byte + var err error + + if endpoint.Gzip { + requestBody, err = compressToGZIP(payload) + if err != nil { + glog.Errorf("[agmaAnalytics] Compressing request failed %v", err) + return err + } + } else { + requestBody = payload + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.Url, bytes.NewBuffer(requestBody)) + if err != nil { + glog.Errorf("[agmaAnalytics] Creating request failed %v", err) + return err + } + + req.Header.Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + req.Header.Set("Content-Type", "application/json") + if endpoint.Gzip { + req.Header.Set("Content-Encoding", "gzip") + } + + resp, err := httpClient.Do(req) + if err != nil { + glog.Errorf("[agmaAnalytics] Sending request failed %v", err) + return err + } + + if resp.StatusCode != http.StatusOK { + glog.Errorf("[agmaAnalytics] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + return fmt.Errorf("wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + } + return nil + }, nil +} diff --git a/analytics/agma/sender_test.go b/analytics/agma/sender_test.go new file mode 100644 index 00000000000..05714674fea --- /dev/null +++ b/analytics/agma/sender_test.go @@ -0,0 +1,133 @@ +package agma + +import ( + "compress/gzip" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prebid/prebid-server/v2/config" + "github.com/stretchr/testify/assert" +) + +func TestCreateHttpSender(t *testing.T) { + testCases := []struct { + name string + endpoint config.AgmaAnalyticsHttpEndpoint + wantHeaders http.Header + wantErr bool + }{ + { + name: "Test with invalid/empty URL", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "%%2815197306101420000%29", + Timeout: "1s", + Gzip: false, + }, + wantErr: true, + }, + { + name: "Test with timeout", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "2x", // Very short timeout + Gzip: false, + }, + wantErr: true, + }, + { + name: "Test with Gzip true", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + Gzip: true, + }, + wantHeaders: http.Header{ + "Content-Encoding": []string{"gzip"}, + "Content-Type": []string{"application/json"}, + }, + wantErr: false, + }, + { + name: "Test with Gzip false", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + Gzip: false, + }, + wantHeaders: http.Header{ + "Content-Type": []string{"application/json"}, + }, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testBody := []byte("[{ \"type\": \"test\" }]") + // Create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the headers + for name, wantValues := range tc.wantHeaders { + assert.Equal(t, wantValues, r.Header[name], "Expected header '%s' to be '%v', got '%v'", name, wantValues, r.Header[name]) + } + defer r.Body.Close() + var reader io.ReadCloser + var err error + if tc.endpoint.Gzip { + reader, err = gzip.NewReader(r.Body) + assert.NoError(t, err) + defer reader.Close() + } else { + reader = r.Body + } + + decompressedData, err := io.ReadAll(reader) + assert.NoError(t, err) + + assert.Equal(t, string(testBody), string(decompressedData)) + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + // Update the URL of the endpoint to the URL of the test server + if !tc.wantErr { + tc.endpoint.Url = ts.URL + } + + // Create a test client + client := &http.Client{} + + // Test the createHttpSender function + sender, err := createHttpSender(client, tc.endpoint) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Test the returned HttpSender function + err = sender([]byte(testBody)) + assert.NoError(t, err) + }) + } +} + +func TestSenderErrorReponse(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer ts.Close() + + client := &http.Client{} + sender, err := createHttpSender(client, config.AgmaAnalyticsHttpEndpoint{ + Url: ts.URL, + Timeout: "1s", + Gzip: false, + }) + testBody := []byte("[{ \"type\": \"test\" }]") + err = sender([]byte(testBody)) + assert.Error(t, err) +} diff --git a/analytics/build/build.go b/analytics/build/build.go index 37846b7d3e4..b9746186e3f 100644 --- a/analytics/build/build.go +++ b/analytics/build/build.go @@ -4,6 +4,7 @@ import ( "github.com/benbjohnson/clock" "github.com/golang/glog" "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/analytics/agma" "github.com/prebid/prebid-server/v2/analytics/clients" "github.com/prebid/prebid-server/v2/analytics/filesystem" "github.com/prebid/prebid-server/v2/analytics/pubstack" @@ -40,6 +41,19 @@ func New(analytics *config.Analytics) analytics.Runner { glog.Errorf("Could not initialize PubstackModule: %v", err) } } + + if analytics.Agma.Enabled { + agmaModule, err := agma.NewModule( + clients.GetDefaultHttpInstance(), + analytics.Agma, + clock.New()) + if err == nil { + modules["agma"] = agmaModule + } else { + glog.Errorf("Could not initialize Agma Anayltics: %v", err) + } + } + return modules } diff --git a/analytics/build/build_test.go b/analytics/build/build_test.go index d9b433cec4b..b8723980219 100644 --- a/analytics/build/build_test.go +++ b/analytics/build/build_test.go @@ -115,7 +115,6 @@ func TestNewPBSAnalytics_FileLogger(t *testing.T) { } func TestNewPBSAnalytics_Pubstack(t *testing.T) { - pbsAnalyticsWithoutError := New(&config.Analytics{ Pubstack: config.Pubstack{ Enabled: true, @@ -142,6 +141,40 @@ func TestNewPBSAnalytics_Pubstack(t *testing.T) { assert.Equal(t, len(instanceWithError), 0) } +func TestNewModuleHttp(t *testing.T) { + agmaAnalyticsWithoutError := New(&config.Analytics{ + Agma: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + BufferSize: "100KB", + EventCount: 50, + Timeout: "30s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "123", + Code: "abc", + }, + }, + }, + }) + instanceWithoutError := agmaAnalyticsWithoutError.(enabledAnalytics) + + assert.Equal(t, len(instanceWithoutError), 1) + + agmaAnalyticsWithError := New(&config.Analytics{ + Agma: config.AgmaAnalytics{ + Enabled: true, + }, + }) + instanceWithError := agmaAnalyticsWithError.(enabledAnalytics) + assert.Equal(t, len(instanceWithError), 0) +} + func TestSampleModuleActivitiesAllowed(t *testing.T) { var count int am := initAnalytics(&count) diff --git a/config/config.go b/config/config.go index 8449bc1b7f4..6094065c489 100644 --- a/config/config.go +++ b/config/config.go @@ -442,8 +442,9 @@ type LMT struct { } type Analytics struct { - File FileLogs `mapstructure:"file"` - Pubstack Pubstack `mapstructure:"pubstack"` + File FileLogs `mapstructure:"file"` + Agma AgmaAnalytics `mapstructure:"agma"` + Pubstack Pubstack `mapstructure:"pubstack"` } type CurrencyConverter struct { @@ -459,6 +460,31 @@ func (cfg *CurrencyConverter) validate(errs []error) []error { return errs } +type AgmaAnalytics struct { + Enabled bool `mapstructure:"enabled"` + Endpoint AgmaAnalyticsHttpEndpoint `mapstructure:"endpoint"` + Buffers AgmaAnalyticsBuffer `mapstructure:"buffers"` + Accounts []AgmaAnalyticsAccount `mapstructure:"accounts"` +} + +type AgmaAnalyticsHttpEndpoint struct { + Url string `mapstructure:"url"` + Timeout string `mapstructure:"timeout"` + Gzip bool `mapstructure:"gzip"` +} + +type AgmaAnalyticsBuffer struct { + BufferSize string `mapstructure:"size"` + EventCount int `mapstructure:"count"` + Timeout string `mapstructure:"timeout"` +} + +type AgmaAnalyticsAccount struct { + Code string `mapstructure:"code"` + PublisherId string `mapstructure:"publisher_id"` + SiteAppId string `mapstructure:"site_app_id"` +} + // FileLogs Corresponding config for FileLogger as a PBS Analytics Module type FileLogs struct { Filename string `mapstructure:"filename"` @@ -1046,6 +1072,14 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("analytics.pubstack.buffers.size", "2MB") v.SetDefault("analytics.pubstack.buffers.count", 100) v.SetDefault("analytics.pubstack.buffers.timeout", "900s") + v.SetDefault("analytics.agma.enabled", false) + v.SetDefault("analytics.agma.endpoint.url", "https://go.pbs.agma-analytics.de/v1/prebid-server") + v.SetDefault("analytics.agma.endpoint.timeout", "2s") + v.SetDefault("analytics.agma.endpoint.gzip", false) + v.SetDefault("analytics.agma.buffers.size", "2MB") + v.SetDefault("analytics.agma.buffers.count", 100) + v.SetDefault("analytics.agma.buffers.timeout", "15m") + v.SetDefault("analytics.agma.accounts", []AgmaAnalyticsAccount{}) v.SetDefault("amp_timeout_adjustment_ms", 0) v.BindEnv("gdpr.default_value") v.SetDefault("gdpr.enabled", true) diff --git a/config/config_test.go b/config/config_test.go index c58e93de1c9..1d1fb858bc0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -224,6 +224,14 @@ func TestDefaults(t *testing.T) { cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 24, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) //Assert purpose VendorExceptionMap hash tables were built correctly + cmpBools(t, "analytics.agma.enabled", false, cfg.Analytics.Agma.Enabled) + cmpStrings(t, "analytics.agma.endpoint.timeout", "2s", cfg.Analytics.Agma.Endpoint.Timeout) + cmpBools(t, "analytics.agma.endpoint.gzip", false, cfg.Analytics.Agma.Endpoint.Gzip) + cmpStrings(t, "analytics.agma.endppoint.url", "https://go.pbs.agma-analytics.de/v1/prebid-server", cfg.Analytics.Agma.Endpoint.Url) + cmpStrings(t, "analytics.agma.buffers.size", "2MB", cfg.Analytics.Agma.Buffers.BufferSize) + cmpInts(t, "analytics.agma.buffers.count", 100, cfg.Analytics.Agma.Buffers.EventCount) + cmpStrings(t, "analytics.agma.buffers.timeout", "15m", cfg.Analytics.Agma.Buffers.Timeout) + cmpInts(t, "analytics.agma.accounts", 0, len(cfg.Analytics.Agma.Accounts)) expectedTCF2 := TCF2{ Enabled: true, Purpose1: TCF2Purpose{ @@ -514,6 +522,21 @@ tmax_adjustments: bidder_response_duration_min_ms: 700 bidder_network_latency_buffer_ms: 100 pbs_response_preparation_duration_ms: 100 +analytics: + agma: + enabled: true + endpoint: + url: "http://test.com" + timeout: "5s" + gzip: false + buffers: + size: 10MB + count: 111 + timeout: 5m + accounts: + - code: agma-code + publisher_id: publisher-id + site_app_id: site-or-app-id `) func cmpStrings(t *testing.T, key, expected, actual string) { @@ -796,6 +819,16 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 10, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs) cmpBools(t, "hooks.enabled", true, cfg.Hooks.Enabled) cmpBools(t, "account_modules_metrics", true, cfg.Metrics.Disabled.AccountModulesMetrics) + cmpBools(t, "analytics.agma.enabled", true, cfg.Analytics.Agma.Enabled) + cmpStrings(t, "analytics.agma.endpoint.timeout", "5s", cfg.Analytics.Agma.Endpoint.Timeout) + cmpBools(t, "analytics.agma.endpoint.gzip", false, cfg.Analytics.Agma.Endpoint.Gzip) + cmpStrings(t, "analytics.agma.endpoint.url", "http://test.com", cfg.Analytics.Agma.Endpoint.Url) + cmpStrings(t, "analytics.agma.buffers.size", "10MB", cfg.Analytics.Agma.Buffers.BufferSize) + cmpInts(t, "analytics.agma.buffers.count", 111, cfg.Analytics.Agma.Buffers.EventCount) + cmpStrings(t, "analytics.agma.buffers.timeout", "5m", cfg.Analytics.Agma.Buffers.Timeout) + cmpStrings(t, "analytics.agma.accounts.0.publisher_id", "publisher-id", cfg.Analytics.Agma.Accounts[0].PublisherId) + cmpStrings(t, "analytics.agma.accounts.0.code", "agma-code", cfg.Analytics.Agma.Accounts[0].Code) + cmpStrings(t, "analytics.agma.accounts.0.site_app_id", "site-or-app-id", cfg.Analytics.Agma.Accounts[0].SiteAppId) } func TestValidateConfig(t *testing.T) { @@ -909,7 +942,6 @@ func TestUserSyncFromEnv(t *testing.T) { assert.Equal(t, "http://somedifferent.url/sync?redirect={{.RedirectURL}}", cfg.BidderInfos["bidder2"].Syncer.IFrame.URL) assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.Redirect) assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.SupportCORS) - } func TestBidderInfoFromEnv(t *testing.T) { From 4870355f3ecfe7b32b275d5bf9ab7edbb991c51f Mon Sep 17 00:00:00 2001 From: linux019 Date: Wed, 13 Mar 2024 19:26:48 +0200 Subject: [PATCH 52/69] Fix loading of default bid adjustments for "account_defaults" (#3555) * Default account bid adjustments was not loaded * add a test for account_defaults.bidadjustments --------- Co-authored-by: oaleksieiev --- config/config_test.go | 30 ++++++++++++++++++++++++++++++ exchange/utils_test.go | 2 +- openrtb_ext/request.go | 20 ++++++++++---------- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index 1d1fb858bc0..3cad8fa12f5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -508,6 +508,20 @@ account_defaults: period_sec: 2000 max_age_sec: 6000 max_schema_dims: 10 + bidadjustments: + mediatype: + '*': + '*': + '*': + - adjtype: multiplier + value: 1.01 + currency: USD + video-instream: + bidder: + deal_id: + - adjtype: cpm + value: 1.02 + currency: EUR privacy: ipv6: anon_keep_bits: 50 @@ -783,7 +797,23 @@ func TestFullConfig(t *testing.T) { 9: &expectedTCF2.Purpose9, 10: &expectedTCF2.Purpose10, } + + expectedBidAdjustments := &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "*": { + "*": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.01, Currency: "USD"}}, + }, + }, + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidder": { + "deal_id": []openrtb_ext.Adjustment{{Type: "cpm", Value: 1.02, Currency: "EUR"}}, + }, + }, + }, + } assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2") + assert.Equal(t, expectedBidAdjustments, cfg.AccountDefaults.BidAdjustments) cmpStrings(t, "currency_converter.fetch_url", "https://currency.prebid.org", cfg.CurrencyConverter.FetchURL) cmpInts(t, "currency_converter.fetch_interval_seconds", 1800, cfg.CurrencyConverter.FetchIntervalSeconds) diff --git a/exchange/utils_test.go b/exchange/utils_test.go index 0c8153b6b11..2354e17b2f6 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -3177,7 +3177,7 @@ func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { }}, }, { - description: "bidAjustement Not provided", + description: "bidAdjustment Not provided", gdprAccountEnabled: &falseValue, gdprHostEnabled: true, gdpr: "1", diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index f5b403d6a1e..d5f2df09306 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -164,7 +164,7 @@ type ExtRequestPrebidCacheVAST struct { // ExtRequestPrebidBidAdjustments defines the contract for bidrequest.ext.prebid.bidadjustments type ExtRequestPrebidBidAdjustments struct { - MediaType MediaType `json:"mediatype,omitempty"` + MediaType MediaType `mapstructure:"mediatype" json:"mediatype,omitempty"` } // AdjustmentsByDealID maps a dealID to a slice of bid adjustments @@ -173,19 +173,19 @@ type AdjustmentsByDealID map[string][]Adjustment // MediaType defines contract for bidrequest.ext.prebid.bidadjustments.mediatype // BidderName will map to a DealID that will map to a slice of bid adjustments type MediaType struct { - Banner map[BidderName]AdjustmentsByDealID `json:"banner,omitempty"` - VideoInstream map[BidderName]AdjustmentsByDealID `json:"video-instream,omitempty"` - VideoOutstream map[BidderName]AdjustmentsByDealID `json:"video-outstream,omitempty"` - Audio map[BidderName]AdjustmentsByDealID `json:"audio,omitempty"` - Native map[BidderName]AdjustmentsByDealID `json:"native,omitempty"` - WildCard map[BidderName]AdjustmentsByDealID `json:"*,omitempty"` + Banner map[BidderName]AdjustmentsByDealID `mapstructure:"banner" json:"banner,omitempty"` + VideoInstream map[BidderName]AdjustmentsByDealID `mapstructure:"video-instream" json:"video-instream,omitempty"` + VideoOutstream map[BidderName]AdjustmentsByDealID `mapstructure:"video-outstream" json:"video-outstream,omitempty"` + Audio map[BidderName]AdjustmentsByDealID `mapstructure:"audio" json:"audio,omitempty"` + Native map[BidderName]AdjustmentsByDealID `mapstructure:"native" json:"native,omitempty"` + WildCard map[BidderName]AdjustmentsByDealID `mapstructure:"*" json:"*,omitempty"` } // Adjustment defines the object that will be present in the slice of bid adjustments found from MediaType map type Adjustment struct { - Type string `json:"adjtype,omitempty"` - Value float64 `json:"value,omitempty"` - Currency string `json:"currency,omitempty"` + Type string `mapstructure:"adjtype" json:"adjtype,omitempty"` + Value float64 `mapstructure:"value" json:"value,omitempty"` + Currency string `mapstructure:"currency" json:"currency,omitempty"` } // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting From db3415592c3126f7c76fdaa2f631aa215b2c9071 Mon Sep 17 00:00:00 2001 From: "Adserver.Online" <61009237+adserver-online@users.noreply.github.com> Date: Mon, 18 Mar 2024 07:43:29 +0200 Subject: [PATCH 53/69] New Adapter: Aso (#3565) Co-authored-by: dev --- adapters/aso/aso.go | 152 +++++++++++ adapters/aso/aso_test.go | 28 ++ .../aso/asotest/exemplary/app-banner.json | 125 +++++++++ .../exemplary/app-multi-impressions.json | 247 ++++++++++++++++++ .../aso/asotest/exemplary/app-native.json | 111 ++++++++ adapters/aso/asotest/exemplary/app-video.json | 135 ++++++++++ .../aso/asotest/exemplary/site-banner.json | 129 +++++++++ .../aso/asotest/exemplary/site-native.json | 112 ++++++++ .../aso/asotest/exemplary/site-video.json | 135 ++++++++++ .../supplemental/bad-request-no-bidder.json | 25 ++ .../supplemental/bad-request-no-ext.json | 22 ++ .../aso/asotest/supplemental/bad-request.json | 51 ++++ .../asotest/supplemental/empty-response.json | 41 +++ .../supplemental/media-type-absent.json | 87 ++++++ .../supplemental/media-type-mapping.json | 100 +++++++ .../asotest/supplemental/server-error.json | 48 ++++ .../supplemental/unparsable-response.json | 48 ++++ adapters/aso/params_test.go | 47 ++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_aso.go | 5 + static/bidder-info/aso.yaml | 21 ++ static/bidder-info/bcmint.yaml | 10 + static/bidder-info/bidgency.yaml | 10 + static/bidder-params/aso.json | 13 + 25 files changed, 1706 insertions(+) create mode 100644 adapters/aso/aso.go create mode 100644 adapters/aso/aso_test.go create mode 100644 adapters/aso/asotest/exemplary/app-banner.json create mode 100644 adapters/aso/asotest/exemplary/app-multi-impressions.json create mode 100644 adapters/aso/asotest/exemplary/app-native.json create mode 100644 adapters/aso/asotest/exemplary/app-video.json create mode 100644 adapters/aso/asotest/exemplary/site-banner.json create mode 100644 adapters/aso/asotest/exemplary/site-native.json create mode 100644 adapters/aso/asotest/exemplary/site-video.json create mode 100644 adapters/aso/asotest/supplemental/bad-request-no-bidder.json create mode 100644 adapters/aso/asotest/supplemental/bad-request-no-ext.json create mode 100644 adapters/aso/asotest/supplemental/bad-request.json create mode 100644 adapters/aso/asotest/supplemental/empty-response.json create mode 100644 adapters/aso/asotest/supplemental/media-type-absent.json create mode 100644 adapters/aso/asotest/supplemental/media-type-mapping.json create mode 100644 adapters/aso/asotest/supplemental/server-error.json create mode 100644 adapters/aso/asotest/supplemental/unparsable-response.json create mode 100644 adapters/aso/params_test.go create mode 100644 openrtb_ext/imp_aso.go create mode 100644 static/bidder-info/aso.yaml create mode 100644 static/bidder-info/bcmint.yaml create mode 100644 static/bidder-info/bidgency.yaml create mode 100644 static/bidder-params/aso.json diff --git a/adapters/aso/aso.go b/adapters/aso/aso.go new file mode 100644 index 00000000000..6b52afa4d81 --- /dev/null +++ b/adapters/aso/aso.go @@ -0,0 +1,152 @@ +package aso + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + endpointTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint template: %v", err) + } + + bidder := &adapter{ + endpoint: endpointTemplate, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + + var requests []*adapters.RequestData + var errors []error + + requestCopy := *request + + for _, imp := range request.Imp { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("invalid imp.ext, %s", err.Error()), + }) + continue + } + + var impExt openrtb_ext.ExtImpAso + if err := json.Unmarshal(bidderExt.Bidder, &impExt); err != nil { + errors = append(errors, &errortypes.BadInput{ + Message: fmt.Sprintf("invalid bidderExt.Bidder, %s", err.Error()), + }) + continue + } + + requestCopy.Imp = []openrtb2.Imp{imp} + endpoint, err := a.buildEndpointURL(&impExt) + + if err != nil { + errors = append(errors, err) + continue + } + + reqJSON, err := json.Marshal(requestCopy) + if err != nil { + errors = append(errors, err) + continue + } + + headers := http.Header{} + headers.Add("Content-Type", "application/json;charset=utf-8") + headers.Add("Accept", "application/json") + + requestData := &adapters.RequestData{ + Method: http.MethodPost, + Uri: endpoint, + Body: reqJSON, + Headers: headers, + } + requests = append(requests, requestData) + } + return requests, errors +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{err} + } + + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + + var errors []error + for _, seatBid := range response.SeatBid { + for i, bid := range seatBid.Bid { + resolveMacros(&seatBid.Bid[i]) + + bidType, err := getMediaType(bid) + if err != nil { + errors = append(errors, err) + continue + } + + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + + return bidResponse, errors +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtImpAso) (string, error) { + endpointParams := macros.EndpointTemplateParams{ZoneID: strconv.Itoa(params.Zone)} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func getMediaType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + if bid.Ext != nil { + var bidExt openrtb_ext.ExtBid + err := json.Unmarshal(bid.Ext, &bidExt) + if err == nil && bidExt.Prebid != nil { + return openrtb_ext.ParseBidType(string(bidExt.Prebid.Type)) + } + } + + return "", &errortypes.BadServerResponse{ + Message: fmt.Sprintf("Failed to get type of bid \"%s\"", bid.ImpID), + } +} + +func resolveMacros(bid *openrtb2.Bid) { + if bid == nil { + return + } + price := strconv.FormatFloat(bid.Price, 'f', -1, 64) + bid.NURL = strings.Replace(bid.NURL, "${AUCTION_PRICE}", price, -1) + bid.AdM = strings.Replace(bid.AdM, "${AUCTION_PRICE}", price, -1) +} diff --git a/adapters/aso/aso_test.go b/adapters/aso/aso_test.go new file mode 100644 index 00000000000..603afaff900 --- /dev/null +++ b/adapters/aso/aso_test.go @@ -0,0 +1,28 @@ +package aso + +import ( + "github.com/stretchr/testify/assert" + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderAso, config.Adapter{ + Endpoint: "https://srv.aso1.net/pbs/bidder?zid={{.ZoneID}}"}, config.Server{ExternalUrl: "http://hosturl.com"}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "asotest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderAso, config.Adapter{ + Endpoint: "zid={{ZoneID}}"}, config.Server{ExternalUrl: "http://hosturl.com"}) + + assert.Error(t, buildErr) +} diff --git a/adapters/aso/asotest/exemplary/app-banner.json b/adapters/aso/asotest/exemplary/app-banner.json new file mode 100644 index 00000000000..fb97e04fe67 --- /dev/null +++ b/adapters/aso/asotest/exemplary/app-banner.json @@ -0,0 +1,125 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "html code", + "crid": "123", + "h": 50, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "html code", + "crid": "123", + "w": 300, + "h": 50, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/exemplary/app-multi-impressions.json b/adapters/aso/asotest/exemplary/app-multi-impressions.json new file mode 100644 index 00000000000..a4c1d9a2236 --- /dev/null +++ b/adapters/aso/asotest/exemplary/app-multi-impressions.json @@ -0,0 +1,247 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 250, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + }, + { + "id": "video-imp", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "zone": 123457 + } + } + } + ], + "app": { + "bundle": "com.prebid" + } + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "banner-imp", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 250, + "h": 250 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ], + "app": { + "bundle": "com.prebid" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "banner-imp", + "price": 1.0, + "cid": "1", + "crid": "11", + "adid": "11", + "adm": "html code", + "ext": { + "prebid": { + "type": "banner" + } + }, + "h": 250, + "w": 300 + }, + { + "id": "2", + "impid": "banner-imp", + "price": 1.5, + "cid": "11", + "crid": "111", + "adid": "111", + "adm": "html code", + "ext": { + "prebid": { + "type": "banner" + } + }, + "h": 250, + "w": 250 + } + ] + } + ] + } + } + }, + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123457", + "body": { + "id": "test-request-id", + "imp": [ + { + "id": "video-imp", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480 + }, + "ext": { + "bidder": { + "zone": 123457 + } + } + } + ], + "app": { + "bundle": "com.prebid" + } + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "2", + "impid": "video-imp", + "price": 2.0, + "cid": "2", + "crid": "22", + "adid": "22", + "adm": "vast xml ${AUCTION_PRICE}", + "nurl": "${AUCTION_PRICE}", + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "banner-imp", + "price": 1.0, + "cid": "1", + "crid": "11", + "adid": "11", + "adm": "html code", + "ext": { + "prebid": { + "type": "banner" + } + }, + "h": 250, + "w": 300 + }, + "type": "banner" + }, + { + "bid": { + "id": "2", + "impid": "banner-imp", + "price": 1.5, + "cid": "11", + "crid": "111", + "adid": "111", + "adm": "html code", + "ext": { + "prebid": { + "type": "banner" + } + }, + "h": 250, + "w": 250 + }, + "type": "banner" + } + ] + }, + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "2", + "impid": "video-imp", + "price": 2.0, + "cid": "2", + "crid": "22", + "adid": "22", + "adm": "vast xml 2", + "nurl": "2", + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/aso/asotest/exemplary/app-native.json b/adapters/aso/asotest/exemplary/app-native.json new file mode 100644 index 00000000000..0d51e15e844 --- /dev/null +++ b/adapters/aso/asotest/exemplary/app-native.json @@ -0,0 +1,111 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat", + "bid": [{ + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "native object", + "crid": "123", + "ext": { + "prebid": { + "type": "native" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "native object", + "crid": "123", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/exemplary/app-video.json b/adapters/aso/asotest/exemplary/app-video.json new file mode 100644 index 00000000000..58b7cda92c7 --- /dev/null +++ b/adapters/aso/asotest/exemplary/app-video.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 728 + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 728 + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "seat", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "vast xml", + "crid": "123", + "w": 1024, + "h": 728, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "vast xml", + "crid": "123", + "w": 1024, + "h": 728, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/exemplary/site-banner.json b/adapters/aso/asotest/exemplary/site-banner.json new file mode 100644 index 00000000000..6680e7a8119 --- /dev/null +++ b/adapters/aso/asotest/exemplary/site-banner.json @@ -0,0 +1,129 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "bidfloor": 1, + "bidfloorcur": "USD", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "bidfloor": 1, + "bidfloorcur": "USD", + "banner": { + "format": [ + { + "w": 300, + "h": 50 + } + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "html code", + "crid": "123", + "h": 50, + "w": 300, + "ext": { + "prebid": { + "type": "banner" + } + } + } + ] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "html code", + "crid": "123", + "w": 300, + "h": 50, + "ext": { + "prebid": { + "type": "banner" + } + } + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/exemplary/site-native.json b/adapters/aso/asotest/exemplary/site-native.json new file mode 100644 index 00000000000..a1200007408 --- /dev/null +++ b/adapters/aso/asotest/exemplary/site-native.json @@ -0,0 +1,112 @@ + +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "native": { + "ver": "1.1", + "request": "{\"ver\":\"1.0\",\"layout\":1,\"adunit\":1,\"plcmttype\":1,\"plcmtcnt\":1,\"seq\":0,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":60,\"hmin\":60,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":3,\"required\":0,\"data\":{\"type\":2,\"len\":75}},{\"id\":4,\"required\":0,\"data\":{\"type\":6,\"len\":1000}},{\"id\":5,\"required\":0,\"data\":{\"type\":7,\"len\":1000}},{\"id\":6,\"required\":0,\"data\":{\"type\":11,\"len\":1000}}]}" + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "seat", + "bid": [{ + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "native object", + "crid": "123", + "ext": { + "prebid": { + "type": "native" + } + } + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "native object", + "crid": "123", + "ext": { + "prebid": { + "type": "native" + } + } + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/exemplary/site-video.json b/adapters/aso/asotest/exemplary/site-video.json new file mode 100644 index 00000000000..792ce8a24f8 --- /dev/null +++ b/adapters/aso/asotest/exemplary/site-video.json @@ -0,0 +1,135 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 728 + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "headers": { + "Accept": [ + "application/json" + ], + "Content-Type": [ + "application/json;charset=utf-8" + ] + }, + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "device": { + "ip":"127.0.0.1" + }, + "imp": [ + { + "id": "test-imp-id", + "video": { + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, + 3, + 5, + 6 + ], + "w": 1024, + "h": 728 + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "cur": "USD", + "seatbid": [ + { + "seat": "seat", + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "vast xml", + "crid": "123", + "w": 1024, + "h": 728, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "vast xml", + "crid": "123", + "w": 1024, + "h": 728, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/aso/asotest/supplemental/bad-request-no-bidder.json b/adapters/aso/asotest/supplemental/bad-request-no-bidder.json new file mode 100644 index 00000000000..c19a7c62a44 --- /dev/null +++ b/adapters/aso/asotest/supplemental/bad-request-no-bidder.json @@ -0,0 +1,25 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bar": "foo" + } + } + ] + }, + "httpCalls": [ + ], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [ + { + "value": "invalid bidderExt.Bidder, unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/aso/asotest/supplemental/bad-request-no-ext.json b/adapters/aso/asotest/supplemental/bad-request-no-ext.json new file mode 100644 index 00000000000..9911931d521 --- /dev/null +++ b/adapters/aso/asotest/supplemental/bad-request-no-ext.json @@ -0,0 +1,22 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "" + } + } + ] + }, + "httpCalls": [ + ], + "expectedBidResponses": [], + "expectedMakeRequestsErrors": [ + { + "value": "invalid imp.ext, unexpected end of JSON input", + "comparison": "literal" + } + ] +} diff --git a/adapters/aso/asotest/supplemental/bad-request.json b/adapters/aso/asotest/supplemental/bad-request.json new file mode 100644 index 00000000000..24147fc587c --- /dev/null +++ b/adapters/aso/asotest/supplemental/bad-request.json @@ -0,0 +1,51 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "zone": 12345 + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=12345", + "body": { + "id": "test-request-id", + "imp": [ + { + "ext": { + "bidder": { + "zone": 12345 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + } + } + ] + } + }, + "mockResponse": { + "status": 400 + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/aso/asotest/supplemental/empty-response.json b/adapters/aso/asotest/supplemental/empty-response.json new file mode 100644 index 00000000000..09ca2b43a77 --- /dev/null +++ b/adapters/aso/asotest/supplemental/empty-response.json @@ -0,0 +1,41 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "zone": 12345 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=12345", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "zone": 12345 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + } + }] + } + }, + "mockResponse": { + "status": 204 + } + } + ], + "expectedBidResponses": [] +} diff --git a/adapters/aso/asotest/supplemental/media-type-absent.json b/adapters/aso/asotest/supplemental/media-type-absent.json new file mode 100644 index 00000000000..b594b247ed3 --- /dev/null +++ b/adapters/aso/asotest/supplemental/media-type-absent.json @@ -0,0 +1,87 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "at": 1, + "tmax":100, + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "body": { + "id": "test-request-id", + "at": 1, + "tmax":100, + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id", + "price": 1.2, + "w": 900, + "h": 250 + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + ] + } + ], + "expectedMakeBidsErrors": [ + { + "value": "Failed to get type of bid \"test-imp-id\"", + "comparison": "literal" + } + ] +} + diff --git a/adapters/aso/asotest/supplemental/media-type-mapping.json b/adapters/aso/asotest/supplemental/media-type-mapping.json new file mode 100644 index 00000000000..4af63675f54 --- /dev/null +++ b/adapters/aso/asotest/supplemental/media-type-mapping.json @@ -0,0 +1,100 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "at": 1, + "tmax":100, + "imp": [ + { + "id": "test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=123456", + "body": { + "id": "test-request-id", + "at": 1, + "tmax":100, + "imp": [ + { + "id":"test-imp-id", + "video": { + "w": 900, + "h": 250, + "mimes": [ + "video/mp4" + ] + }, + "ext": { + "bidder": { + "zone": 123456 + } + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "test-bid-id", + "impid": "test-imp-id1", + "price": 1.2, + "w": 900, + "h": 250, + "ext": { + "prebid": { + "type": "video" + } + } + } + ] + } + ] + } + } + } + ], + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "test-bid-id", + "impid": "test-imp-id1", + "price": 1.2, + "w": 900, + "h": 250, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type": "video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/aso/asotest/supplemental/server-error.json b/adapters/aso/asotest/supplemental/server-error.json new file mode 100644 index 00000000000..8af763e7d23 --- /dev/null +++ b/adapters/aso/asotest/supplemental/server-error.json @@ -0,0 +1,48 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "zone": 12345 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=12345", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "zone": 12345 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + } + }] + } + }, + "mockResponse": { + "status": 500, + "body": "Server error" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 500. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/aso/asotest/supplemental/unparsable-response.json b/adapters/aso/asotest/supplemental/unparsable-response.json new file mode 100644 index 00000000000..fb2142e52f4 --- /dev/null +++ b/adapters/aso/asotest/supplemental/unparsable-response.json @@ -0,0 +1,48 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "imp": [{ + "id": "test-imp-id", + "native": { + "request": "" + }, + "ext": { + "bidder": { + "zone": 12345 + } + } + }] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "https://srv.aso1.net/pbs/bidder?zid=12345", + "body": { + "id": "test-request-id", + "imp": [{ + "ext": { + "bidder": { + "zone": 12345 + } + }, + "id": "test-imp-id", + "native": { + "request": "" + } + }] + } + }, + "mockResponse": { + "status": 200, + "body": "" + } + } + ], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse", + "comparison": "literal" + } + ] +} diff --git a/adapters/aso/params_test.go b/adapters/aso/params_test.go new file mode 100644 index 00000000000..caf86acc01b --- /dev/null +++ b/adapters/aso/params_test.go @@ -0,0 +1,47 @@ +package aso + +import ( + "encoding/json" + "testing" + + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +func TestValidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, validParam := range validParams { + if err := validator.Validate(openrtb_ext.BidderAso, json.RawMessage(validParam)); err != nil { + t.Errorf("Schema rejected aso params: %s", validParam) + } + } +} + +func TestInvalidParams(t *testing.T) { + validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params") + if err != nil { + t.Fatalf("Failed to fetch the json-schemas. %v", err) + } + + for _, invalidParam := range invalidParams { + if err := validator.Validate(openrtb_ext.BidderAso, json.RawMessage(invalidParam)); err == nil { + t.Errorf("Schema allowed unexpected params: %s", invalidParam) + } + } +} + +var validParams = []string{ + `{ "zone": 12345 }`, +} + +var invalidParams = []string{ + ``, + `null`, + `{}`, + `{ "zone": "abc" }`, + `{ "zone": "12345" }`, + `{ "zone": "" }`, +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index 57318c006da..a85516667a4 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -38,6 +38,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/apacdex" "github.com/prebid/prebid-server/v2/adapters/appnexus" "github.com/prebid/prebid-server/v2/adapters/appush" + "github.com/prebid/prebid-server/v2/adapters/aso" "github.com/prebid/prebid-server/v2/adapters/audienceNetwork" "github.com/prebid/prebid-server/v2/adapters/automatad" "github.com/prebid/prebid-server/v2/adapters/avocet" @@ -242,6 +243,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderApacdex: apacdex.Builder, openrtb_ext.BidderAppnexus: appnexus.Builder, openrtb_ext.BidderAppush: appush.Builder, + openrtb_ext.BidderAso: aso.Builder, openrtb_ext.BidderAudienceNetwork: audienceNetwork.Builder, openrtb_ext.BidderAutomatad: automatad.Builder, openrtb_ext.BidderAvocet: avocet.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index baf54f97a7f..71bed294dbe 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -54,6 +54,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderApacdex, BidderAppnexus, BidderAppush, + BidderAso, BidderAudienceNetwork, BidderAutomatad, BidderAvocet, @@ -336,6 +337,7 @@ const ( BidderApacdex BidderName = "apacdex" BidderAppnexus BidderName = "appnexus" BidderAppush BidderName = "appush" + BidderAso BidderName = "aso" BidderAudienceNetwork BidderName = "audienceNetwork" BidderAutomatad BidderName = "automatad" BidderAvocet BidderName = "avocet" diff --git a/openrtb_ext/imp_aso.go b/openrtb_ext/imp_aso.go new file mode 100644 index 00000000000..b06a5154b8e --- /dev/null +++ b/openrtb_ext/imp_aso.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtImpAso struct { + Zone int `json:"zone"` +} diff --git a/static/bidder-info/aso.yaml b/static/bidder-info/aso.yaml new file mode 100644 index 00000000000..429b8441a50 --- /dev/null +++ b/static/bidder-info/aso.yaml @@ -0,0 +1,21 @@ +endpoint: "https://srv.aso1.net/pbs/bidder?zid={{.ZoneID}}" +maintainer: + email: "support@adsrv.org" + +capabilities: + app: + mediaTypes: + - banner + - video + - native + site: + mediaTypes: + - banner + - video + - native +userSync: + # Adserver.Online supports user syncing, but requires configuration by the host.Contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect + - iframe diff --git a/static/bidder-info/bcmint.yaml b/static/bidder-info/bcmint.yaml new file mode 100644 index 00000000000..9d5a62cc516 --- /dev/null +++ b/static/bidder-info/bcmint.yaml @@ -0,0 +1,10 @@ +aliasOf: aso +endpoint: "https://srv.datacygnal.io/pbs/bidder?zid={{.ZoneID}}" +maintainer: + email: "contact@bcm.ltd" +userSync: + # BCM Int. supports user syncing, but requires configuration by the host.Contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect + - iframe \ No newline at end of file diff --git a/static/bidder-info/bidgency.yaml b/static/bidder-info/bidgency.yaml new file mode 100644 index 00000000000..ad5816dffe7 --- /dev/null +++ b/static/bidder-info/bidgency.yaml @@ -0,0 +1,10 @@ +aliasOf: aso +endpoint: "https://srv.bidgx.com/pbs/bidder?zid={{.ZoneID}}" +maintainer: + email: "aso@bidgency.com" +userSync: + # Bidgency supports user syncing, but requires configuration by the host.Contact this + # bidder directly at the email address in this file to ask about enabling user sync. + supports: + - redirect + - iframe \ No newline at end of file diff --git a/static/bidder-params/aso.json b/static/bidder-params/aso.json new file mode 100644 index 00000000000..edb3c0feb9c --- /dev/null +++ b/static/bidder-params/aso.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Adserver.Online Adapter Params", + "description": "A schema which validates params accepted by the aso adapter", + "type": "object", + "properties": { + "zone": { + "type": "integer", + "description": "An ID which identifies the zone selling the impression" + } + }, + "required": ["zone"] +} From 82cb3e84959975eef78e42fb2258003298a97c36 Mon Sep 17 00:00:00 2001 From: Ashish Garg Date: Mon, 18 Mar 2024 16:36:14 +0530 Subject: [PATCH 54/69] Fix yandex properties file (#3568) --- static/bidder-params/yandex.json | 52 +++++++++++++------------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/static/bidder-params/yandex.json b/static/bidder-params/yandex.json index ca777077611..24de473f31e 100644 --- a/static/bidder-params/yandex.json +++ b/static/bidder-params/yandex.json @@ -3,41 +3,31 @@ "title": "Yandex Adapter Params", "description": "A schema which validates params accepted by the Yandex adapter", "type": "object", + "properties": { + "page_id": { + "type": "integer", + "minLength": 1, + "minimum": 1, + "description": "Special Page Id provided by Yandex Manager" + }, + "imp_id": { + "type": "integer", + "minLength": 1, + "minimum": 1, + "description": "Special identifier provided by Yandex Manager" + }, + "placement_id": { + "type": "string", + "description": "Ad placement identifier", + "pattern": "(\\S+-)?\\d+-\\d+" + } + }, "oneOf": [ { - "type": "object", - "description": "Deprecated composite ad placement identifier", - "properties": { - "page_id": { - "type": "integer", - "minLength": 1, - "minimum": 1, - "description": "Special Page Id provided by Yandex Manager" - }, - "imp_id": { - "type": "integer", - "minLength": 1, - "minimum": 1, - "description": "Special identifier provided by Yandex Manager" - } - }, - "required": [ - "page_id", - "imp_id" - ] + "required": [ "page_id", "imp_id" ] }, { - "type": "object", - "properties": { - "placement_id": { - "type": "string", - "description": "Ad placement identifier", - "pattern": "(\\S+-)?\\d+-\\d+" - } - }, - "required": [ - "placement_id" - ] + "required": [ "placement_id" ] } ] } \ No newline at end of file From 1484a46cfd8f0019f27402a21913a485b3bce7c0 Mon Sep 17 00:00:00 2001 From: JonGoSonobi Date: Mon, 18 Mar 2024 08:45:22 -0400 Subject: [PATCH 55/69] Sonobi: Added consent macros to the user sync url (#3572) --- static/bidder-info/sonobi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/bidder-info/sonobi.yaml b/static/bidder-info/sonobi.yaml index 6f9afc36b3f..135bdacfe93 100644 --- a/static/bidder-info/sonobi.yaml +++ b/static/bidder-info/sonobi.yaml @@ -13,5 +13,5 @@ capabilities: - video userSync: redirect: - url: "https://sync.go.sonobi.com/us.gif?loc={{.RedirectURL}}" + url: "https://sync.go.sonobi.com/us.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&loc={{.RedirectURL}}" userMacro: "[UID]" From 6cc298c7742154c59068eafb493838aff28bf6b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:06:31 -0400 Subject: [PATCH 56/69] Bump google.golang.org/protobuf from 1.30.0 to 1.33.0 (#3573) Bumps google.golang.org/protobuf from 1.30.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 94e2a2a4e3c..783c0044b2e 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/lib/pq v1.10.4 github.com/mitchellh/copystructure v1.2.0 + github.com/modern-go/reflect2 v1.0.2 github.com/pkg/errors v0.9.1 github.com/prebid/go-gdpr v1.12.0 github.com/prebid/go-gpp v0.2.0 @@ -53,7 +54,6 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -73,7 +73,7 @@ require ( golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.15.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index bed6194599a..28805a382d6 100644 --- a/go.sum +++ b/go.sum @@ -962,8 +962,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 82ba5856f836c0910319eec94b4a6eadfc9409a2 Mon Sep 17 00:00:00 2001 From: linux019 Date: Mon, 18 Mar 2024 20:10:27 +0200 Subject: [PATCH 57/69] Fix: Panic in bid adjustments (#3547) * #3543 fix panic in bids adjustment * add a test for empty ext.prebid and account with enabled bid adjustments * add more tests for bid adjustments * remove extra space --------- Co-authored-by: oaleksieiev --- bidadjustment/build_rules.go | 8 +- bidadjustment/build_rules_test.go | 254 +++++++++++++++++++----------- 2 files changed, 164 insertions(+), 98 deletions(-) diff --git a/bidadjustment/build_rules.go b/bidadjustment/build_rules.go index f028d78616a..019ad87e9cf 100644 --- a/bidadjustment/build_rules.go +++ b/bidadjustment/build_rules.go @@ -60,13 +60,11 @@ func merge(req *openrtb_ext.RequestWrapper, acct *openrtb_ext.ExtRequestPrebidBi } extPrebid := reqExt.GetPrebid() - if extPrebid == nil && acct == nil { - return nil, nil - } - if extPrebid == nil && acct != nil { + if extPrebid == nil || extPrebid.BidAdjustments == nil { return acct, nil } - if extPrebid != nil && acct == nil { + + if acct == nil { return extPrebid.BidAdjustments, nil } diff --git a/bidadjustment/build_rules_test.go b/bidadjustment/build_rules_test.go index b3a9a930559..2c19932fec7 100644 --- a/bidadjustment/build_rules_test.go +++ b/bidadjustment/build_rules_test.go @@ -220,7 +220,7 @@ func TestMerge(t *testing.T) { testCases := []struct { name string givenRequestWrapper *openrtb_ext.RequestWrapper - givenAccount *config.Account + acctBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments expectedBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments }{ { @@ -228,26 +228,18 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, - }, - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, @@ -257,23 +249,17 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"audio":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, }, }, }, @@ -283,14 +269,10 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "static", "value": 3.00, "currency": "USD"}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, @@ -310,26 +292,18 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"native":{"bidderA":{"dealId":[{"adjtype": "cpm", "value": 0.18, "currency": "USD"}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}}, - }, - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}}}, + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, @@ -339,28 +313,20 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-outstream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, }, }, }, @@ -370,23 +336,17 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, @@ -396,28 +356,20 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, }, - givenAccount: &config.Account{ - BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ - MediaType: openrtb_ext.MediaType{ - WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, - }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, }, }, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderB": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}, - }, + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}}}, }, VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, }, }, }, @@ -427,7 +379,7 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)}, }, - givenAccount: &config.Account{}, + acctBidAdjustments: nil, expectedBidAdjustments: nil, }, { @@ -435,13 +387,129 @@ func TestMerge(t *testing.T) { givenRequestWrapper: &openrtb_ext.RequestWrapper{ BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, }, - givenAccount: &config.Account{}, + acctBidAdjustments: nil, expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ MediaType: openrtb_ext.MediaType{ Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ - "bidderA": { - "dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}, - }, + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + }, + + { + name: "NilExtPrebid-NilExtPrebidBidAdj_NilAcct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + acctBidAdjustments: nil, + expectedBidAdjustments: nil, + }, + { + name: "NilExtPrebid-NilExtPrebidBidAdj-Acct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{}, + }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + }, + { + name: "NotNilExtPrebid-NilExtBidAdj-NilAcct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{}}`)}, + }, + acctBidAdjustments: nil, + expectedBidAdjustments: nil, + }, + { + name: "NotNilExtPrebid_NilExtBidAdj_NotNilAcct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{}}`)}, + }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + }, + { + name: "NotNilExtPrebid-NotNilExtBidAdj-NilAcct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + acctBidAdjustments: nil, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + }, + }, + }, + { + name: "NotNilExtPrebid-NotNilExtBidAdj-NotNilAcct", + givenRequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)}, + }, + acctBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderC": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderD": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderE": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderF": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + }, + }, + expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{ + MediaType: openrtb_ext.MediaType{ + Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderA": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}}}, + }, + VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderB": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderC": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderD": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderE": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, + }, + WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{ + "bidderF": {"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3}}}, }, }, }, @@ -450,7 +518,7 @@ func TestMerge(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - mergedBidAdj, err := merge(test.givenRequestWrapper, test.givenAccount.BidAdjustments) + mergedBidAdj, err := merge(test.givenRequestWrapper, test.acctBidAdjustments) assert.NoError(t, err) assert.Equal(t, test.expectedBidAdjustments, mergedBidAdj) }) From 54874d0dc30a286333917b296fb612e7c4ba26d6 Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Wed, 20 Mar 2024 18:58:59 +0100 Subject: [PATCH 58/69] add 'debug' and 'integration' to sample requests (#3575) Description in openrtb2/sample-requests/valid-whole/exemplary/all-ext.json says: "This demonstrates all of the OpenRTB extensions supported by Prebid Server." However, ext.prebid.debug and ext.prebid.integration, documented in https://docs.prebid.org/prebid-server/endpoints/openrtb2/pbs-endpoint-auction.html#openrtb-extensions are currently missing. Add them for completeness. Signed-off-by: Dmitry S --- .../openrtb2/sample-requests/valid-whole/exemplary/all-ext.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json index 02dc6160d49..556a04fbec8 100644 --- a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/all-ext.json @@ -83,6 +83,8 @@ "name": "video", "version": "1.0" }, + "debug": true, + "integration": "managed", "targeting": { "includewinners": false, "pricegranularity": { From 94148bf12714a77c0f05b000bc98c1f7ea98f5cf Mon Sep 17 00:00:00 2001 From: Dmitry Savintsev Date: Mon, 25 Mar 2024 18:22:49 +0100 Subject: [PATCH 59/69] Fix semgrep dgryski.semgrep-go issues (#3511) * fix semgrep dgryski.semgrep-go issues Fix most of the semgrep issues with the http://semgrep.dev/r/dgryski.semgrep-go ruleset (`semgrep --config http://semgrep.dev/r/dgryski.semgrep-go`). Left the issue with Content-Type text/plain on json.Encode in endpoints/openrtb2/amp_auction.go since changing to application/json breaks the AMP unit tests, and issues with the pointer receiver for MarshalJSON in usersync/cookie.go. Fix #3509. Signed-off-by: Dmitry S * add comment about legacy text/plain content type Signed-off-by: Dmitry S * fix semgrep dgryski issue with w.Write, add nosemgrep Signed-off-by: Dmitry S * add nosemgrep ignore for marshal-json-pointer-receiver --------- Signed-off-by: Dmitry S --- endpoints/events/event.go | 8 ++++---- endpoints/events/vtrack.go | 12 ++++++------ endpoints/openrtb2/amp_auction.go | 13 +++++++++---- endpoints/openrtb2/auction.go | 2 +- usersync/cookie.go | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/endpoints/events/event.go b/endpoints/events/event.go index b92b72f17ad..e202932aff8 100644 --- a/endpoints/events/event.go +++ b/endpoints/events/event.go @@ -70,7 +70,7 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou w.WriteHeader(http.StatusBadRequest) for _, err := range errs { - w.Write([]byte(fmt.Sprintf("invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "invalid request: %s\n", err.Error()) } return @@ -81,7 +81,7 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou if err != nil { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountIdParameter))) + fmt.Fprintf(w, "Account '%s' is required query parameter and can't be empty", AccountIdParameter) return } eventRequest.AccountID = accountId @@ -105,7 +105,7 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou w.WriteHeader(status) for _, message := range messages { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message))) + fmt.Fprintf(w, "Invalid request: %s\n", message) } return } @@ -113,7 +113,7 @@ func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprou // Check if events are enabled for the account if !account.Events.Enabled { w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(fmt.Sprintf("Account '%s' doesn't support events", eventRequest.AccountID))) + fmt.Fprintf(w, "Account '%s' doesn't support events", eventRequest.AccountID) return } diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go index 5d794651ba4..a2e185f4ba9 100644 --- a/endpoints/events/vtrack.go +++ b/endpoints/events/vtrack.go @@ -74,7 +74,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro // account id is required if accountId == "" { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountParameter))) + fmt.Fprintf(w, "Account '%s' is required query parameter and can't be empty", AccountParameter) return } @@ -82,7 +82,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro integrationType, err := getIntegrationType(r) if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("Invalid integration type: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid integration type: %s\n", err.Error()) return } @@ -92,7 +92,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro // check if there was any error while parsing puts request if err != nil { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) return } @@ -106,7 +106,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro w.WriteHeader(status) for _, message := range messages { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message))) + fmt.Fprintf(w, "Invalid request: %s\n", message) } return } @@ -118,7 +118,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro if len(errs) > 0 { w.WriteHeader(http.StatusInternalServerError) for _, err := range errs { - w.Write([]byte(fmt.Sprintf("Error(s) updating vast: %s\n", err.Error()))) + fmt.Fprintf(w, "Error(s) updating vast: %s\n", err.Error()) return } @@ -128,7 +128,7 @@ func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httpro if err != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Error serializing pbs cache response: %s\n", err.Error()))) + fmt.Fprintf(w, "Error serializing pbs cache response: %s\n", err.Error()) return } diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 6d14ed7d69d..1b53b182ab8 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -171,7 +171,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h if errortypes.ContainsFatalError(errL) { w.WriteHeader(http.StatusBadRequest) for _, err := range errortypes.FatalOnly(errL) { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } labels.RequestStatus = metrics.RequestStatusBadInput return @@ -224,7 +224,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h w.WriteHeader(httpStatus) labels.RequestStatus = metricsStatus for _, err := range errortypes.FatalOnly(errL) { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } ao.Errors = append(ao.Errors, acctIDErrs...) return @@ -237,7 +237,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h if errortypes.ContainsFatalError(errs) { w.WriteHeader(http.StatusBadRequest) for _, err := range errortypes.FatalOnly(errs) { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } labels.RequestStatus = metrics.RequestStatusBadInput return @@ -398,8 +398,13 @@ func sendAmpResponse( ao.AmpTargetingValues = targets // Fixes #231 - enc := json.NewEncoder(w) + enc := json.NewEncoder(w) // nosemgrep: json-encoder-needs-type enc.SetEscapeHTML(false) + // Explicitly set content type to text/plain, which had previously been + // the implied behavior from the time the project was launched. + // It's unclear why text/plain was chosen or if it was an oversight, + // nevertheless we will keep it as such for compatibility reasons. + w.Header().Set("Content-Type", "text/plain; charset=utf-8") // 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. diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 2aafe6808ef..780b9ebb0a1 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -2409,7 +2409,7 @@ func writeError(errs []error, w http.ResponseWriter, labels *metrics.Labels) boo w.WriteHeader(httpStatus) labels.RequestStatus = metricsStatus for _, err := range errs { - w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error()))) + fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) } rc = true } diff --git a/usersync/cookie.go b/usersync/cookie.go index b4bc821c9d7..cfec6a8ec9d 100644 --- a/usersync/cookie.go +++ b/usersync/cookie.go @@ -225,7 +225,7 @@ type cookieJson struct { OptOut bool `json:"optout,omitempty"` } -func (cookie *Cookie) MarshalJSON() ([]byte, error) { +func (cookie *Cookie) MarshalJSON() ([]byte, error) { // nosemgrep: marshal-json-pointer-receiver return jsonutil.Marshal(cookieJson{ UIDs: cookie.uids, OptOut: cookie.optOut, From a6267d76911d8baeb9d9e9b3ab634496102ed8d6 Mon Sep 17 00:00:00 2001 From: Aditya Mahendrakar Date: Mon, 25 Mar 2024 10:23:10 -0700 Subject: [PATCH 60/69] Make admin listener configurable (#3520) * Make admin listener configurable Admin listener is made configurable by adding `admin_enabled`. It is enabled by default. Making it configurable allows a host company to not run the listener on admin port if they desire to do so. * move Enabled to a separate Admin struct --- config/config.go | 5 +++++ server/server.go | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/config/config.go b/config/config.go index 6094065c489..b2204053fac 100644 --- a/config/config.go +++ b/config/config.go @@ -27,6 +27,7 @@ type Configuration struct { UnixSocketName string `mapstructure:"unix_socket_name"` Client HTTPClient `mapstructure:"http_client"` CacheClient HTTPClient `mapstructure:"http_client_cache"` + Admin Admin `mapstructure:"admin"` AdminPort int `mapstructure:"admin_port"` Compression Compression `mapstructure:"compression"` // GarbageCollectorThreshold allocates virtual memory (in bytes) which is not used by PBS but @@ -102,6 +103,9 @@ type Configuration struct { PriceFloors PriceFloors `mapstructure:"price_floors"` } +type Admin struct { + Enabled bool `mapstructure:"enabled"` +} type PriceFloors struct { Enabled bool `mapstructure:"enabled"` Fetcher PriceFloorFetcher `mapstructure:"fetcher"` @@ -884,6 +888,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("unix_socket_enable", false) // boolean which decide if the socket-server will be started. v.SetDefault("unix_socket_name", "prebid-server.sock") // path of the socket's file which must be listened. v.SetDefault("admin_port", 6060) + v.SetDefault("admin.enabled", true) // boolean to determine if admin listener will be started. v.SetDefault("garbage_collector_threshold", 0) v.SetDefault("status_response", "") v.SetDefault("datacenter", "") diff --git a/server/server.go b/server/server.go index dd4813adb7f..4993e996414 100644 --- a/server/server.go +++ b/server/server.go @@ -29,9 +29,6 @@ func Listen(cfg *config.Configuration, handler http.Handler, adminHandler http.H stopPrometheus := make(chan os.Signal) done := make(chan struct{}) - adminServer := newAdminServer(cfg, adminHandler) - go shutdownAfterSignals(adminServer, stopAdmin, done) - if cfg.UnixSocketEnable && len(cfg.UnixSocketName) > 0 { // start the unix_socket server if config enable-it. var ( socketListener net.Listener @@ -56,12 +53,17 @@ func Listen(cfg *config.Configuration, handler http.Handler, adminHandler http.H go runServer(mainServer, "Main", mainListener) } - var adminListener net.Listener - if adminListener, err = newTCPListener(adminServer.Addr, nil); err != nil { - glog.Errorf("Error listening for TCP connections on %s: %v for admin server", adminServer.Addr, err) - return + if cfg.Admin.Enabled { + adminServer := newAdminServer(cfg, adminHandler) + go shutdownAfterSignals(adminServer, stopAdmin, done) + + var adminListener net.Listener + if adminListener, err = newTCPListener(adminServer.Addr, nil); err != nil { + glog.Errorf("Error listening for TCP connections on %s: %v for admin server", adminServer.Addr, err) + return + } + go runServer(adminServer, "Admin", adminListener) } - go runServer(adminServer, "Admin", adminListener) if cfg.Metrics.Prometheus.Port != 0 { var ( @@ -70,7 +72,7 @@ func Listen(cfg *config.Configuration, handler http.Handler, adminHandler http.H ) go shutdownAfterSignals(prometheusServer, stopPrometheus, done) if prometheusListener, err = newTCPListener(prometheusServer.Addr, nil); err != nil { - glog.Errorf("Error listening for TCP connections on %s: %v for prometheus server", adminServer.Addr, err) + glog.Errorf("Error listening for TCP connections on %s: %v for prometheus server", prometheusServer.Addr, err) return } From 25a4466577deba21f1490d883c07679fe2716948 Mon Sep 17 00:00:00 2001 From: AlexBVolcy <74930484+AlexBVolcy@users.noreply.github.com> Date: Tue, 26 Mar 2024 13:19:51 -0400 Subject: [PATCH 61/69] Add Ext.Prebid.Analytics Support (#3563) * Setup and TODOs * updateReqWrapperForAnalytics setup * Add updateReqWrapper code to build. Update comments * Updates to handle copying * Add tests for updateReqWrapper function * Add full tests, remove unnecessary code * Remove some comments * Fix comment * Clean up * Address comments * Clone prep * fix test * Update tests, add clone request for update func * Improve tests, simplify update function * Remove secondMockAnalytics --- analytics/build/build.go | 56 ++++++++ analytics/build/build_test.go | 237 ++++++++++++++++++++++++++++++++-- openrtb_ext/request.go | 1 + 3 files changed, 282 insertions(+), 12 deletions(-) diff --git a/analytics/build/build.go b/analytics/build/build.go index b9746186e3f..fc47835b088 100644 --- a/analytics/build/build.go +++ b/analytics/build/build.go @@ -1,6 +1,8 @@ package build import ( + "encoding/json" + "github.com/benbjohnson/clock" "github.com/golang/glog" "github.com/prebid/prebid-server/v2/analytics" @@ -66,7 +68,11 @@ func (ea enabledAnalytics) LogAuctionObject(ao *analytics.AuctionObject, ac priv if cloneBidderReq != nil { ao.RequestWrapper = cloneBidderReq } + cloneReq := updateReqWrapperForAnalytics(ao.RequestWrapper, name, cloneBidderReq != nil) module.LogAuctionObject(ao) + if cloneReq != nil { + ao.RequestWrapper = cloneReq + } } } } @@ -77,7 +83,11 @@ func (ea enabledAnalytics) LogVideoObject(vo *analytics.VideoObject, ac privacy. if cloneBidderReq != nil { vo.RequestWrapper = cloneBidderReq } + cloneReq := updateReqWrapperForAnalytics(vo.RequestWrapper, name, cloneBidderReq != nil) module.LogVideoObject(vo) + if cloneReq != nil { + vo.RequestWrapper = cloneReq + } } } @@ -101,7 +111,11 @@ func (ea enabledAnalytics) LogAmpObject(ao *analytics.AmpObject, ac privacy.Acti if cloneBidderReq != nil { ao.RequestWrapper = cloneBidderReq } + cloneReq := updateReqWrapperForAnalytics(ao.RequestWrapper, name, cloneBidderReq != nil) module.LogAmpObject(ao) + if cloneReq != nil { + ao.RequestWrapper = cloneReq + } } } } @@ -144,3 +158,45 @@ func evaluateActivities(rw *openrtb_ext.RequestWrapper, ac privacy.ActivityContr cloneReq.RebuildRequest() return true, cloneReq } + +func updateReqWrapperForAnalytics(rw *openrtb_ext.RequestWrapper, adapterName string, isCloned bool) *openrtb_ext.RequestWrapper { + reqExt, _ := rw.GetRequestExt() + reqExtPrebid := reqExt.GetPrebid() + if reqExtPrebid == nil { + return nil + } + + var cloneReq *openrtb_ext.RequestWrapper + if !isCloned { + cloneReq = &openrtb_ext.RequestWrapper{BidRequest: ortb.CloneBidRequestPartial(rw.BidRequest)} + } else { + cloneReq = nil + } + + if len(reqExtPrebid.Analytics) == 0 { + return cloneReq + } + + // Remove the entire analytics object if the adapter module is not present + if _, ok := reqExtPrebid.Analytics[adapterName]; !ok { + reqExtPrebid.Analytics = nil + } else { + reqExtPrebid.Analytics = updatePrebidAnalyticsMap(reqExtPrebid.Analytics, adapterName) + } + reqExt.SetPrebid(reqExtPrebid) + rw.RebuildRequest() + + if cloneReq != nil { + cloneReq.RebuildRequest() + } + + return cloneReq +} + +func updatePrebidAnalyticsMap(extPrebidAnalytics map[string]json.RawMessage, adapterName string) map[string]json.RawMessage { + newMap := make(map[string]json.RawMessage) + if val, ok := extPrebidAnalytics[adapterName]; ok { + newMap[adapterName] = val + } + return newMap +} diff --git a/analytics/build/build_test.go b/analytics/build/build_test.go index b8723980219..5071f93a6f7 100644 --- a/analytics/build/build_test.go +++ b/analytics/build/build_test.go @@ -22,9 +22,10 @@ func TestSampleModule(t *testing.T) { var count int am := initAnalytics(&count) am.LogAuctionObject(&analytics.AuctionObject{ - Status: http.StatusOK, - Errors: nil, - Response: &openrtb2.BidResponse{}, + Status: http.StatusOK, + RequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getDefaultBidRequest()}, + Errors: nil, + Response: &openrtb2.BidResponse{}, }, privacy.ActivityControl{}) if count != 1 { t.Errorf("PBSAnalyticsModule failed at LogAuctionObject") @@ -46,12 +47,12 @@ func TestSampleModule(t *testing.T) { t.Errorf("PBSAnalyticsModule failed at LogCookieSyncObject") } - am.LogAmpObject(&analytics.AmpObject{}, privacy.ActivityControl{}) + am.LogAmpObject(&analytics.AmpObject{RequestWrapper: &openrtb_ext.RequestWrapper{}}, privacy.ActivityControl{}) if count != 4 { t.Errorf("PBSAnalyticsModule failed at LogAmpObject") } - am.LogVideoObject(&analytics.VideoObject{}, privacy.ActivityControl{}) + am.LogVideoObject(&analytics.VideoObject{RequestWrapper: &openrtb_ext.RequestWrapper{}}, privacy.ActivityControl{}) if count != 5 { t.Errorf("PBSAnalyticsModule failed at LogVideoObject") } @@ -182,9 +183,10 @@ func TestSampleModuleActivitiesAllowed(t *testing.T) { acAllowed := privacy.NewActivityControl(getActivityConfig("sampleModule", true, true, true)) ao := &analytics.AuctionObject{ - Status: http.StatusOK, - Errors: nil, - Response: &openrtb2.BidResponse{}, + Status: http.StatusOK, + RequestWrapper: &openrtb_ext.RequestWrapper{}, + Errors: nil, + Response: &openrtb2.BidResponse{}, } am.LogAuctionObject(ao, acAllowed) @@ -192,12 +194,12 @@ func TestSampleModuleActivitiesAllowed(t *testing.T) { t.Errorf("PBSAnalyticsModule failed at LogAuctionObject") } - am.LogAmpObject(&analytics.AmpObject{}, acAllowed) + am.LogAmpObject(&analytics.AmpObject{RequestWrapper: &openrtb_ext.RequestWrapper{}}, acAllowed) if count != 2 { t.Errorf("PBSAnalyticsModule failed at LogAmpObject") } - am.LogVideoObject(&analytics.VideoObject{}, acAllowed) + am.LogVideoObject(&analytics.VideoObject{RequestWrapper: &openrtb_ext.RequestWrapper{}}, acAllowed) if count != 3 { t.Errorf("PBSAnalyticsModule failed at LogVideoObject") } @@ -336,8 +338,8 @@ func getDefaultBidRequest() *openrtb2.BidRequest { return &openrtb2.BidRequest{ ID: "test_request", User: &openrtb2.User{ID: "user-id"}, - Device: &openrtb2.Device{IFA: "device-ifa", IP: "127.0.0.1"}} - + Device: &openrtb2.Device{IFA: "device-ifa", IP: "127.0.0.1"}, + } } func getActivityConfig(componentName string, allowReportAnalytics, allowTransmitUserFPD, allowTransmitPreciseGeo bool) *config.AccountPrivacy { @@ -388,3 +390,214 @@ func getActivityConfig(componentName string, allowReportAnalytics, allowTransmit }, } } + +type mockAnalytics struct { + lastLoggedAuctionBidRequest *openrtb2.BidRequest + lastLoggedAmpBidRequest *openrtb2.BidRequest + lastLoggedVideoBidRequest *openrtb2.BidRequest +} + +func (m *mockAnalytics) LogAuctionObject(ao *analytics.AuctionObject) { + m.lastLoggedAuctionBidRequest = ao.RequestWrapper.BidRequest +} + +func (m *mockAnalytics) LogAmpObject(ao *analytics.AmpObject) { + m.lastLoggedAmpBidRequest = ao.RequestWrapper.BidRequest +} + +func (m *mockAnalytics) LogVideoObject(vo *analytics.VideoObject) { + m.lastLoggedVideoBidRequest = vo.RequestWrapper.BidRequest +} + +func (m *mockAnalytics) LogCookieSyncObject(ao *analytics.CookieSyncObject) {} + +func (m *mockAnalytics) LogSetUIDObject(ao *analytics.SetUIDObject) {} + +func (m *mockAnalytics) LogNotificationEventObject(ao *analytics.NotificationEvent) {} + +func TestLogObject(t *testing.T) { + tests := []struct { + description string + givenRequestWrapper *openrtb_ext.RequestWrapper + givenEnabledAnalytics enabledAnalytics + givenActivityControl bool + givenAuctionObject *analytics.AuctionObject + givenAmpObject *analytics.AmpObject + givenVideoObject *analytics.VideoObject + expectedBidRequest1 *openrtb2.BidRequest + expectedBidRequest2 *openrtb2.BidRequest + }{ + { + description: "Multiple analytics modules, clone from evaluate activities, should expect both to have their information to be logged only -- auction", + givenEnabledAnalytics: enabledAnalytics{"adapter1": &mockAnalytics{}, "adapter2": &mockAnalytics{}}, + givenActivityControl: true, + givenAuctionObject: &analytics.AuctionObject{ + Status: http.StatusOK, + Errors: nil, + Response: &openrtb2.BidResponse{}, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + expectedBidRequest1: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true}}}}`)}, + expectedBidRequest2: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter2":{"client-analytics":false}}}}`)}, + }, + { + description: "Multiple analytics modules, no clone from evaluate activities, should expect both to have their information to be logged only -- amp", + givenEnabledAnalytics: enabledAnalytics{"adapter1": &mockAnalytics{}, "adapter2": &mockAnalytics{}}, + givenActivityControl: false, + givenAmpObject: &analytics.AmpObject{ + Status: http.StatusOK, + Errors: nil, + AuctionResponse: &openrtb2.BidResponse{}, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + expectedBidRequest1: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true}}}}`)}, + expectedBidRequest2: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter2":{"client-analytics":false}}}}`)}, + }, + { + description: "Single analytics module, clone from evaluate activities, should expect both to have their information to be logged only -- amp", + givenEnabledAnalytics: enabledAnalytics{"adapter1": &mockAnalytics{}}, + givenActivityControl: true, + givenAuctionObject: &analytics.AuctionObject{ + Status: http.StatusOK, + Errors: nil, + Response: &openrtb2.BidResponse{}, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + expectedBidRequest1: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true}}}}`)}, + }, + { + description: "Single analytics module, adapter name not found, expect entire analytics object to be nil -- video", + givenEnabledAnalytics: enabledAnalytics{"unknownAdapter": &mockAnalytics{}}, + givenActivityControl: true, + givenVideoObject: &analytics.VideoObject{ + Status: http.StatusOK, + Errors: nil, + Response: &openrtb2.BidResponse{}, + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "test_request", + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + expectedBidRequest1: &openrtb2.BidRequest{ + ID: "test_request", + Ext: nil, + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + ac := privacy.NewActivityControl(getActivityConfig("sampleModule", test.givenActivityControl, test.givenActivityControl, test.givenActivityControl)) + + var loggedBidReq1, loggedBidReq2 *openrtb2.BidRequest + switch { + case test.givenAuctionObject != nil: + test.givenEnabledAnalytics.LogAuctionObject(test.givenAuctionObject, ac) + loggedBidReq1 = test.givenEnabledAnalytics["adapter1"].(*mockAnalytics).lastLoggedAuctionBidRequest + if len(test.givenEnabledAnalytics) == 2 { + loggedBidReq2 = test.givenEnabledAnalytics["adapter2"].(*mockAnalytics).lastLoggedAuctionBidRequest + } + case test.givenAmpObject != nil: + test.givenEnabledAnalytics.LogAmpObject(test.givenAmpObject, ac) + loggedBidReq1 = test.givenEnabledAnalytics["adapter1"].(*mockAnalytics).lastLoggedAmpBidRequest + if len(test.givenEnabledAnalytics) == 2 { + loggedBidReq2 = test.givenEnabledAnalytics["adapter2"].(*mockAnalytics).lastLoggedAmpBidRequest + } + case test.givenVideoObject != nil: + test.givenEnabledAnalytics.LogVideoObject(test.givenVideoObject, ac) + loggedBidReq1 = test.givenEnabledAnalytics["unknownAdapter"].(*mockAnalytics).lastLoggedVideoBidRequest + } + + assert.Equal(t, test.expectedBidRequest1, loggedBidReq1) + if test.expectedBidRequest2 != nil { + assert.Equal(t, test.expectedBidRequest2, loggedBidReq2) + } + }) + } +} + +func TestUpdateReqWrapperForAnalytics(t *testing.T) { + tests := []struct { + description string + givenReqWrapper *openrtb_ext.RequestWrapper + givenAdapterName string + givenIsCloned bool + expectedUpdatedBidRequest *openrtb2.BidRequest + expectedCloneRequest *openrtb_ext.RequestWrapper + }{ + { + description: "Adapter1 so Adapter2 info should be removed from ext.prebid.analytics", + givenReqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + givenAdapterName: "adapter1", + givenIsCloned: false, + expectedUpdatedBidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true}}}}`), + }, + expectedCloneRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + { + description: "Adapter2 so Adapter1 info should be removed from ext.prebid.analytics", + givenReqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + givenAdapterName: "adapter2", + givenIsCloned: true, + expectedUpdatedBidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter2":{"client-analytics":false}}}}`), + }, + expectedCloneRequest: nil, + }, + { + description: "Given adapter not found in ext.prebid.analytics so remove entire object", + givenReqWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + givenAdapterName: "adapterNotFound", + givenIsCloned: false, + expectedUpdatedBidRequest: &openrtb2.BidRequest{}, + expectedCloneRequest: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + cloneReq := updateReqWrapperForAnalytics(test.givenReqWrapper, test.givenAdapterName, test.givenIsCloned) + assert.Equal(t, test.expectedUpdatedBidRequest, test.givenReqWrapper.BidRequest) + assert.Equal(t, test.expectedCloneRequest, cloneReq) + }) + } +} diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go index d5f2df09306..8533ccced9e 100644 --- a/openrtb_ext/request.go +++ b/openrtb_ext/request.go @@ -45,6 +45,7 @@ type ExtRequestPrebid struct { AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"` Aliases map[string]string `json:"aliases,omitempty"` AliasGVLIDs map[string]uint16 `json:"aliasgvlids,omitempty"` + Analytics map[string]json.RawMessage `json:"analytics,omitempty"` BidAdjustmentFactors map[string]float64 `json:"bidadjustmentfactors,omitempty"` BidAdjustments *ExtRequestPrebidBidAdjustments `json:"bidadjustments,omitempty"` BidderConfigs []BidderConfig `json:"bidderconfig,omitempty"` From 67c487d1bec5dbd737ab7b8cce7047e087436456 Mon Sep 17 00:00:00 2001 From: AlexBVolcy <74930484+AlexBVolcy@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:35:37 -0400 Subject: [PATCH 62/69] Make Targeting in Response Optional (#3574) --- endpoints/openrtb2/auction.go | 42 ++++----- endpoints/openrtb2/auction_test.go | 85 ++---------------- .../targeting-optional-all-false.json | 69 ++++++++++++++ ...targeting-optional-includeformat-only.json | 89 +++++++++++++++++++ .../targeting-optional-includeformat.json | 69 ++++++++++++++ exchange/exchange.go | 4 +- 6 files changed, 254 insertions(+), 104 deletions(-) create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-all-false.json create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat-only.json create mode 100644 endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat.json diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 780b9ebb0a1..9accae2e041 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -1697,36 +1697,28 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) []error { } func validateTargeting(t *openrtb_ext.ExtRequestTargeting) error { - if t == nil { - return nil - } - - if (t.IncludeWinners == nil || !*t.IncludeWinners) && (t.IncludeBidderKeys == nil || !*t.IncludeBidderKeys) { - return errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support") - } - - if t.PriceGranularity != nil { - if err := validatePriceGranularity(t.PriceGranularity); err != nil { - return err + if t != nil { + if t.PriceGranularity != nil { + if err := validatePriceGranularity(t.PriceGranularity); err != nil { + return err + } } - } - - if t.MediaTypePriceGranularity.Video != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Video); err != nil { - return err + if t.MediaTypePriceGranularity.Video != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Video); err != nil { + return err + } } - } - if t.MediaTypePriceGranularity.Banner != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Banner); err != nil { - return err + if t.MediaTypePriceGranularity.Banner != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Banner); err != nil { + return err + } } - } - if t.MediaTypePriceGranularity.Native != nil { - if err := validatePriceGranularity(t.MediaTypePriceGranularity.Native); err != nil { - return err + if t.MediaTypePriceGranularity.Native != nil { + if err := validatePriceGranularity(t.MediaTypePriceGranularity.Native); err != nil { + return err + } } } - return nil } diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index f51979db10d..5356815d81b 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -1941,9 +1941,13 @@ func TestValidateRequestExt(t *testing.T) { givenRequestExt: json.RawMessage(`{"prebid":{"cache":{"bids":{},"vastxml":{}}}}`), }, { - description: "prebid targeting", // test integration with validateTargeting - givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{}}}`), - expectedErrors: []string{"ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"}, + description: "prebid price granularity invalid", + givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{"pricegranularity":{"precision":-1,"ranges":[{"min":0,"max":20,"increment":0.1}]}}}}`), + expectedErrors: []string{"Price granularity error: precision must be non-negative"}, + }, + { + description: "prebid native media type price granualrity valid", + givenRequestExt: json.RawMessage(`{"prebid":{"targeting":{"mediatypepricegranularity":{"native":{"precision":3,"ranges":[{"max":20,"increment":4.5}]}}}}}`), }, { description: "valid multibid", @@ -1984,75 +1988,9 @@ func TestValidateTargeting(t *testing.T) { givenTargeting: nil, expectedError: nil, }, - { - name: "empty", - givenTargeting: &openrtb_ext.ExtRequestTargeting{}, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners nil, includebidderkeys false", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners nil, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "includewinners false, includebidderkeys nil", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners true, includebidderkeys nil", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "all false", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: errors.New("ext.prebid.targeting: At least one of includewinners or includebidderkeys must be enabled to enable targeting support"), - }, - { - name: "includewinners false, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(false), - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, - { - name: "includewinners false, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - IncludeBidderKeys: ptrutil.ToPtr(false), - }, - expectedError: nil, - }, - { - name: "includewinners true, includebidderkeys true", - givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), - IncludeBidderKeys: ptrutil.ToPtr(true), - }, - expectedError: nil, - }, { name: "price granularity ranges out of order", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), PriceGranularity: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), Ranges: []openrtb_ext.GranularityRange{ @@ -2066,7 +2004,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Video: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2081,7 +2018,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2096,7 +2032,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2111,7 +2046,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video and banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2132,7 +2066,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Video: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2147,7 +2080,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity banner incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2162,7 +2094,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2177,7 +2108,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity video correct and banner incorrect", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Banner: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), @@ -2198,7 +2128,6 @@ func TestValidateTargeting(t *testing.T) { { name: "media type price granularity native incorrect and banner correct", givenTargeting: &openrtb_ext.ExtRequestTargeting{ - IncludeWinners: ptrutil.ToPtr(true), MediaTypePriceGranularity: openrtb_ext.MediaTypePriceGranularity{ Native: &openrtb_ext.PriceGranularity{ Precision: ptrutil.ToPtr(2), diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-all-false.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-all-false.json new file mode 100644 index 00000000000..020a2ed849c --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-all-false.json @@ -0,0 +1,69 @@ +{ + "description": "Targeting flags are all set to false, request is still valid, but no targeting data should be present in bids", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 0.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "targeting": { + "includewinners": false, + "includebidderkeys": false, + "includeformat": false + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 0 + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 + } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat-only.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat-only.json new file mode 100644 index 00000000000..56eeaf4cea2 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat-only.json @@ -0,0 +1,89 @@ +{ + "description": "Targeting flags are all undefined besides includeformat, request is still valid, defaults should come in for other flags so targeting data should be present in bid", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 1.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "targeting": { + "includeformat": true + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 1.00, + "ext": { + "origbidcpm": 1, + "origbidcur": "USD", + "prebid": { + "meta": { + "adaptercode": "appnexus" + }, + "targeting": { + "hb_bidder": "appnexus", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host": "www.pbcserver.com", + "hb_cache_host_appnex": "www.pbcserver.com", + "hb_cache_path": "/pbcache/endpoint", + "hb_cache_path_appnex": "/pbcache/endpoint", + "hb_format": "banner", + "hb_format_appnexus": "banner", + "hb_pb": "1.00", + "hb_pb_appnexus": "1.00" + }, + "type": "banner" + } + } + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 + } \ No newline at end of file diff --git a/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat.json b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat.json new file mode 100644 index 00000000000..2d7b801e20f --- /dev/null +++ b/endpoints/openrtb2/sample-requests/valid-whole/exemplary/targeting-optional-includeformat.json @@ -0,0 +1,69 @@ +{ + "description": "Targeting flags are all set to false except for includeformat, request is still valid, but no targeting data should be present in bids", + "config": { + "mockBidders": [ + { + "bidderName": "appnexus", + "currency": "USD", + "price": 0.00 + } + ] + }, + "mockBidRequest": { + "id": "some-request-id", + "site": { + "page": "prebid.org" + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "format": [ + { + "w": 300, + "h": 250 + }, + { + "w": 300, + "h": 600 + } + ] + }, + "ext": { + "appnexus": { + "placementId": 12883451 + } + } + } + ], + "tmax": 500, + "ext": { + "prebid": { + "targeting": { + "includewinners": false, + "includebidderkeys": false, + "includeformat": true + } + } + } + }, + "expectedBidResponse": { + "id": "some-request-id", + "seatbid": [ + { + "bid": [ + { + "id": "appnexus-bid", + "impid": "some-impression-id", + "price": 0 + } + ], + "seat": "appnexus" + } + ], + "bidid": "test bid id", + "cur": "USD", + "nbr": 0 + }, + "expectedReturnCode": 200 + } \ No newline at end of file diff --git a/exchange/exchange.go b/exchange/exchange.go index ef38736388d..c73eea9f10c 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -460,7 +460,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog errs = append(errs, cacheErrs...) } - targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) + if targData.includeWinners || targData.includeBidderKeys || targData.includeFormat { + targData.setTargeting(auc, r.BidRequestWrapper.BidRequest.App != nil, bidCategory, r.Account.TruncateTargetAttribute, multiBidMap) + } } bidResponseExt = e.makeExtBidResponse(adapterBids, adapterExtra, *r, responseDebugAllow, requestExtPrebid.Passthrough, fledge, errs) } else { From c72ffe4db7067fa8f6658493ccdbf53a1585101c Mon Sep 17 00:00:00 2001 From: Veronika Solovei Date: Wed, 27 Mar 2024 11:20:40 -0700 Subject: [PATCH 63/69] Fix for NPE for nil request in analytics module (#3599) --- analytics/build/build.go | 3 +++ analytics/build/build_test.go | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/analytics/build/build.go b/analytics/build/build.go index fc47835b088..4cba9a3f1a6 100644 --- a/analytics/build/build.go +++ b/analytics/build/build.go @@ -160,6 +160,9 @@ func evaluateActivities(rw *openrtb_ext.RequestWrapper, ac privacy.ActivityContr } func updateReqWrapperForAnalytics(rw *openrtb_ext.RequestWrapper, adapterName string, isCloned bool) *openrtb_ext.RequestWrapper { + if rw == nil { + return nil + } reqExt, _ := rw.GetRequestExt() reqExtPrebid := reqExt.GetPrebid() if reqExtPrebid == nil { diff --git a/analytics/build/build_test.go b/analytics/build/build_test.go index 5071f93a6f7..d794c01ab8c 100644 --- a/analytics/build/build_test.go +++ b/analytics/build/build_test.go @@ -591,12 +591,22 @@ func TestUpdateReqWrapperForAnalytics(t *testing.T) { Ext: []byte(`{"prebid":{"analytics":{"adapter1":{"client-analytics":true},"adapter2":{"client-analytics":false}}}}`)}, }, }, + { + description: "Given request is nil, check there are no exceptions", + givenReqWrapper: nil, + givenAdapterName: "adapter1", + givenIsCloned: false, + expectedUpdatedBidRequest: nil, + expectedCloneRequest: nil, + }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { cloneReq := updateReqWrapperForAnalytics(test.givenReqWrapper, test.givenAdapterName, test.givenIsCloned) - assert.Equal(t, test.expectedUpdatedBidRequest, test.givenReqWrapper.BidRequest) + if test.givenReqWrapper != nil { + assert.Equal(t, test.expectedUpdatedBidRequest, test.givenReqWrapper.BidRequest) + } assert.Equal(t, test.expectedCloneRequest, cloneReq) }) } From cfb9d2eaaf24bacfe369ceb3864a80ab75c2b2f4 Mon Sep 17 00:00:00 2001 From: Nilesh Chate <97721111+pm-nilesh-chate@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:05:30 +0530 Subject: [PATCH 64/69] Fix: TestAmpBadRequests (#3546) --- endpoints/openrtb2/amp_auction_test.go | 91 +++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 7d87b65301d..08dec09e5dc 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "strconv" + "strings" "testing" "time" @@ -1028,20 +1029,39 @@ func TestAMPSiteExt(t *testing.T) { // TestBadRequests makes sure we return 400's on bad requests. func TestAmpBadRequests(t *testing.T) { - dir := "sample-requests/invalid-whole" + dir := "sample-requests/invalid-whole/" files, err := os.ReadDir(dir) assert.NoError(t, err, "Failed to read folder: %s", dir) - badRequests := make(map[string]json.RawMessage, len(files)) + mockAmpStoredReq := make(map[string]json.RawMessage, len(files)) + badRequests := make(map[string]testCase, len(files)) for index, file := range files { - badRequests[strconv.Itoa(100+index)] = readFile(t, "sample-requests/invalid-whole/"+file.Name()) + filename := file.Name() + fileData := readFile(t, dir+filename) + + test, err := parseTestData(fileData, filename) + if !assert.NoError(t, err) { + return + } + + if skipAmpTest(test) { + continue + } + + requestID := strconv.Itoa(100 + index) + test.Query = fmt.Sprintf("account=test_pub&tag_id=%s", requestID) + + badRequests[requestID] = test + mockAmpStoredReq[requestID] = test.BidRequest } + addAmpBadRequests(badRequests, mockAmpStoredReq) + endpoint, _ := NewAmpEndpoint( fakeUUIDGenerator{}, &mockAmpExchange{}, newParamsValidator(t), - &mockAmpStoredReqFetcher{badRequests}, + &mockAmpStoredReqFetcher{data: mockAmpStoredReq}, empty_fetcher.EmptyFetcher{}, &config.Configuration{MaxRequestSize: maxSize}, &metricsConfig.NilMetricsEngine{}, @@ -1053,16 +1073,71 @@ func TestAmpBadRequests(t *testing.T) { hooks.EmptyPlanBuilder{}, nil, ) - for requestID := range badRequests { - request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?tag_id=%s", requestID), nil) + + for _, test := range badRequests { + request := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp?%s", test.Query), nil) recorder := httptest.NewRecorder() endpoint(recorder, request, nil) - if recorder.Code != http.StatusBadRequest { - t.Errorf("Expected status %d. Got %d. Input was: %s", http.StatusBadRequest, recorder.Code, fmt.Sprintf("/openrtb2/auction/amp?config=%s", requestID)) + response := recorder.Body.String() + assert.Equal(t, test.ExpectedReturnCode, recorder.Code, test.Description) + assert.Contains(t, response, test.ExpectedErrorMessage, "Actual: %s \nExpected: %s. Description: %s \n", response, test.ExpectedErrorMessage, test.Description) + } +} + +func skipAmpTest(test testCase) bool { + bidRequest := openrtb2.BidRequest{} + if err := json.Unmarshal(test.BidRequest, &bidRequest); err == nil { + // request.app must not exist in AMP + if bidRequest.App != nil { + return true } + + // data for tag_id='%s' does not define the required imp array + // Invalid request: data for tag_id '%s' includes %d imp elements. Only one is allowed + if len(bidRequest.Imp) == 0 || len(bidRequest.Imp) > 1 { + return true + } + + if bidRequest.Device != nil && strings.Contains(string(bidRequest.Device.Ext), "interstitial") { + return true + } + } + + // request.ext.prebid.cache is initialised in AMP if it is not present in request + if strings.Contains(test.ExpectedErrorMessage, `Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the "bids" or "vastxml" properties`) || + strings.Contains(test.ExpectedErrorMessage, `Invalid request: ext.prebid.storedrequest.id must be a string`) { + return true + } + + return false +} + +func addAmpBadRequests(mapBadRequests map[string]testCase, mockAmpStoredReq map[string]json.RawMessage) { + mapBadRequests["201"] = testCase{ + Description: "missing-tag-id", + Query: "account=test_pub", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: AMP requests require an AMP tag_id\n", + } + mockAmpStoredReq["201"] = json.RawMessage(`{}`) + + mapBadRequests["202"] = testCase{ + Description: "request.app-present", + Query: "account=test_pub&tag_id=202", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: request.app must not exist in AMP stored requests.\n", + } + mockAmpStoredReq["202"] = json.RawMessage(`{"imp":[{}],"app":{}}`) + + mapBadRequests["203"] = testCase{ + Description: "request-with-2-imps", + Query: "account=test_pub&tag_id=203", + ExpectedReturnCode: http.StatusBadRequest, + ExpectedErrorMessage: "Invalid request: data for tag_id '203' includes 2 imp elements. Only one is allowed", } + mockAmpStoredReq["203"] = json.RawMessage(`{"imp":[{},{}]}`) } // TestAmpDebug makes sure we get debug information back when requested From 9070008fa4d16dc71f0a33087e84d2e74c82bd8f Mon Sep 17 00:00:00 2001 From: Rajesh Koilpillai Date: Mon, 1 Apr 2024 23:19:11 +0530 Subject: [PATCH 65/69] Update README.md (#3603) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77443318d89..c5b0e003aaa 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Bid Adapters transform OpenRTB requests and responses for communicating with a b Analytics Modules enable business intelligence tools to collect data from Prebid Server to provide publishers and hosts with valuable insights into their header bidding traffic. We welcome you to contribute a module for your platform. Refer to our guide on [building an analytics module](https://docs.prebid.org/prebid-server/developers/pbs-build-an-analytics-adapter.html) for further information. ### Auction Module -Auction Modules allow hosts to extend the behavior of Prebid Server at specfic spots in the auction pipeline using existing modules or by developing custom functionality. Auction Modules may provide creative validation, traffic optimization, and real time data services amoung many other potential uses. We welcome vendors and community members to contribute modules that publishers and hosts may find useful. Consult our guide on [building an auction module](https://docs.prebid.org/prebid-server/developers/add-a-module.html) for more information. +Auction Modules allow hosts to extend the behavior of Prebid Server at specfic spots in the auction pipeline using existing modules or by developing custom functionality. Auction Modules may provide creative validation, traffic optimization, and real time data services among many other potential uses. We welcome vendors and community members to contribute modules that publishers and hosts may find useful. Consult our guide on [building an auction module](https://docs.prebid.org/prebid-server/developers/add-a-module.html) for more information. ### Feature We welcome everyone to contribute to this project by implementing a specification or by proposing a new feature. Please review the [prioritized project board](https://github.com/orgs/prebid/projects/4), where you can select an issue labeled "Ready For Dev". To avoid redundant effort, kindly leave a comment on the issue stating your intention to take it on. To propose a feature, [open a new issue](https://github.com/prebid/prebid-server/issues/new/choose) with as much detail as possible for consideration by the Prebid Server Committee. From 47c9434db5a41c41bcb63cee369c7eae10db84e0 Mon Sep 17 00:00:00 2001 From: Jaydeep Mohite <30924180+pm-jaydeep-mohite@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:50:24 +0530 Subject: [PATCH 66/69] Floors: Update FloorsSchemaVersion data type from string to int (#3592) --- floors/floors_test.go | 8 +++---- floors/validate.go | 2 +- floors/validate_test.go | 47 ++++++++++++++++++++++++++++++++++---- openrtb_ext/floors.go | 2 +- openrtb_ext/floors_test.go | 4 ++-- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/floors/floors_test.go b/floors/floors_test.go index f5f71df4685..b9618bb86f2 100644 --- a/floors/floors_test.go +++ b/floors/floors_test.go @@ -91,7 +91,7 @@ func TestEnrichWithPriceFloors(t *testing.T) { expFloorVal float64 expFloorCur string expPriceFlrLoc string - expSchemaVersion string + expSchemaVersion int }{ { name: "Floors disabled in account config", @@ -178,7 +178,7 @@ func TestEnrichWithPriceFloors(t *testing.T) { Publisher: &openrtb2.Publisher{Domain: "www.website.com"}, }, Imp: []openrtb2.Imp{{ID: "1234", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}}}, - Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":11,"floormincur":"USD","data":{"currency":"USD","floorsschemaversion":"2","modelgroups":[{"modelweight":50,"modelversion":"version2","schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":11.01,"*|*|www.website1.com":17.01},"default":21},{"modelweight":50,"modelversion":"version11","skiprate":110,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`), + Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":11,"floormincur":"USD","data":{"currency":"USD","floorsschemaversion":2,"modelgroups":[{"modelweight":50,"modelversion":"version2","schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":11.01,"*|*|www.website1.com":17.01},"default":21},{"modelweight":50,"modelversion":"version11","skiprate":110,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|300x250|*":11.01,"*|300x250|www.website1.com":100.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`), }, }, account: testAccountConfig, @@ -186,7 +186,7 @@ func TestEnrichWithPriceFloors(t *testing.T) { expFloorVal: 11.01, expFloorCur: "USD", expPriceFlrLoc: openrtb_ext.RequestLocation, - expSchemaVersion: "2", + expSchemaVersion: 2, }, { name: "Rule selection with Site object, banner|300x600|www.website.com", @@ -385,7 +385,7 @@ func TestEnrichWithPriceFloors(t *testing.T) { assert.Equal(t, *requestExt.GetPrebid().Floors.Skipped, tc.Skipped, tc.name) } else { assert.Equal(t, requestExt.GetPrebid().Floors.PriceFloorLocation, tc.expPriceFlrLoc, tc.name) - if tc.expSchemaVersion != "" { + if tc.expSchemaVersion != 0 { assert.Equal(t, requestExt.GetPrebid().Floors.Data.FloorsSchemaVersion, tc.expSchemaVersion, tc.name) } } diff --git a/floors/validate.go b/floors/validate.go index 5dd843b13e0..245bf993e58 100644 --- a/floors/validate.go +++ b/floors/validate.go @@ -55,7 +55,7 @@ func validateFloorRulesAndLowerValidRuleKey(schema openrtb_ext.PriceFloorSchema, // validateFloorParams validates SchemaVersion, SkipRate and FloorMin func validateFloorParams(extFloorRules *openrtb_ext.PriceFloorRules) error { - if extFloorRules.Data != nil && len(extFloorRules.Data.FloorsSchemaVersion) > 0 && extFloorRules.Data.FloorsSchemaVersion != "2" { + if extFloorRules.Data != nil && extFloorRules.Data.FloorsSchemaVersion != 0 && extFloorRules.Data.FloorsSchemaVersion != 2 { return fmt.Errorf("Invalid FloorsSchemaVersion = '%v', supported version 2", extFloorRules.Data.FloorsSchemaVersion) } diff --git a/floors/validate_test.go b/floors/validate_test.go index 59d08afc5c0..ccb75c7a659 100644 --- a/floors/validate_test.go +++ b/floors/validate_test.go @@ -56,13 +56,12 @@ func TestValidateFloorParams(t *testing.T) { Err: errors.New("Invalid FloorMin = '-10', value should be >= 0"), }, { - name: "Invalid FloorSchemaVersion ", + name: "Invalid FloorSchemaVersion 2", floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ - FloorsSchemaVersion: "1", + FloorsSchemaVersion: 1, ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ ModelVersion: "Version 1", - - Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, Values: map[string]float64{ "banner|300x250|www.website.com": 1.01, "banner|300x600|*": 4.01, @@ -70,6 +69,46 @@ func TestValidateFloorParams(t *testing.T) { }}}, Err: errors.New("Invalid FloorsSchemaVersion = '1', supported version 2"), }, + { + name: "Invalid FloorSchemaVersion -2", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + FloorsSchemaVersion: -2, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x600|*": 4.01, + }, Default: 0.01}, + }}}, + Err: errors.New("Invalid FloorsSchemaVersion = '-2', supported version 2"), + }, + { + name: "Valid FloorSchemaVersion 0", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + FloorsSchemaVersion: 0, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x600|*": 4.01, + }, Default: 0.01}, + }}}, + }, + { + name: "Valid FloorSchemaVersion 2", + floorExt: &openrtb_ext.PriceFloorRules{Data: &openrtb_ext.PriceFloorData{ + FloorsSchemaVersion: 2, + ModelGroups: []openrtb_ext.PriceFloorModelGroup{{ + ModelVersion: "Version 1", + Schema: openrtb_ext.PriceFloorSchema{Fields: []string{"mediaType", "size", "domain"}, Delimiter: "|"}, + Values: map[string]float64{ + "banner|300x250|www.website.com": 1.01, + "banner|300x600|*": 4.01, + }, Default: 0.01}, + }}}, + }, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go index 92dab2acd90..12c04c8ad16 100644 --- a/openrtb_ext/floors.go +++ b/openrtb_ext/floors.go @@ -84,7 +84,7 @@ type PriceFloorEndpoint struct { type PriceFloorData struct { Currency string `json:"currency,omitempty"` SkipRate int `json:"skiprate,omitempty"` - FloorsSchemaVersion string `json:"floorsschemaversion,omitempty"` + FloorsSchemaVersion int `json:"floorsschemaversion,omitempty"` ModelTimestamp int `json:"modeltimestamp,omitempty"` ModelGroups []PriceFloorModelGroup `json:"modelgroups,omitempty"` FloorProvider string `json:"floorprovider,omitempty"` diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go index d6c35b9f7ef..687101bfa3a 100644 --- a/openrtb_ext/floors_test.go +++ b/openrtb_ext/floors_test.go @@ -327,7 +327,7 @@ func TestFloorRulesDeepCopy(t *testing.T) { Data: &PriceFloorData{ Currency: "INR", SkipRate: 0, - FloorsSchemaVersion: "2", + FloorsSchemaVersion: 2, ModelTimestamp: 123, ModelGroups: []PriceFloorModelGroup{ { @@ -370,7 +370,7 @@ func TestFloorRulesDeepCopy(t *testing.T) { Data: &PriceFloorData{ Currency: "INR", SkipRate: 0, - FloorsSchemaVersion: "2", + FloorsSchemaVersion: 2, ModelTimestamp: 123, ModelGroups: []PriceFloorModelGroup{ { From fb0384bb94352f13607d22d52781e8bd01ea328d Mon Sep 17 00:00:00 2001 From: psmrt <162991810+psmrt@users.noreply.github.com> Date: Wed, 3 Apr 2024 11:55:34 +0530 Subject: [PATCH 67/69] New adapter: Smrtconnect (#3571) Co-authored-by: pragnesh --- adapters/smrtconnect/smrtconnect.go | 144 +++++++++++ adapters/smrtconnect/smrtconnect_test.go | 28 +++ .../smrtconnecttest/exemplary/audio-app.json | 90 +++++++ .../smrtconnecttest/exemplary/audio-web.json | 90 +++++++ .../smrtconnecttest/exemplary/banner-app.json | 139 +++++++++++ .../exemplary/banner-multiple-bids.json | 227 ++++++++++++++++++ .../smrtconnecttest/exemplary/banner-web.json | 127 ++++++++++ .../smrtconnecttest/exemplary/native-app.json | 136 +++++++++++ .../smrtconnecttest/exemplary/native-web.json | 124 ++++++++++ .../smrtconnecttest/exemplary/video-app.json | 149 ++++++++++++ .../smrtconnecttest/exemplary/video-web.json | 147 ++++++++++++ .../supplemental/empty-seatbid-array.json | 119 +++++++++ .../invalid-aceex-ext-object.json | 29 +++ .../supplemental/invalid-response.json | 94 ++++++++ .../supplemental/status-code-bad-request.json | 92 +++++++ .../supplemental/status-code-no-content.json | 68 ++++++ .../supplemental/status-code-other-error.json | 78 ++++++ .../status-code-service-unavailable.json | 78 ++++++ exchange/adapter_builders.go | 2 + openrtb_ext/bidders.go | 2 + openrtb_ext/imp_smrtconnect.go | 5 + static/bidder-info/smrtconnect.yaml | 20 ++ static/bidder-params/smrtconnect.json | 14 ++ 23 files changed, 2002 insertions(+) create mode 100644 adapters/smrtconnect/smrtconnect.go create mode 100644 adapters/smrtconnect/smrtconnect_test.go create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json create mode 100644 adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json create mode 100644 adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json create mode 100644 openrtb_ext/imp_smrtconnect.go create mode 100644 static/bidder-info/smrtconnect.yaml create mode 100644 static/bidder-params/smrtconnect.json diff --git a/adapters/smrtconnect/smrtconnect.go b/adapters/smrtconnect/smrtconnect.go new file mode 100644 index 00000000000..dc7bf2545bd --- /dev/null +++ b/adapters/smrtconnect/smrtconnect.go @@ -0,0 +1,144 @@ +package smrtconnect + +import ( + "encoding/json" + "fmt" + "text/template" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/adapters" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type adapter struct { + endpoint *template.Template +} + +// Builder builds a new instance of the Smrtconnect adapter for the given bidder with the given config. +func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { + template, err := template.New("endpointTemplate").Parse(config.Endpoint) + if err != nil { + return nil, fmt.Errorf("unable to parse endpoint url template: %v", err) + } + + bidder := &adapter{ + endpoint: template, + } + return bidder, nil +} + +func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { + var requests []*adapters.RequestData + + requestCopy := *request + for _, imp := range request.Imp { + smrtconnectExt, err := getImpressionExt(&imp) + if err != nil { + return nil, []error{err} + } + + url, err := a.buildEndpointURL(smrtconnectExt) + if err != nil { + return nil, []error{err} + } + + requestCopy.Imp = []openrtb2.Imp{imp} + requestJSON, err := json.Marshal(requestCopy) + if err != nil { + return nil, []error{err} + } + + requestData := &adapters.RequestData{ + Method: "POST", + Uri: url, + Body: requestJSON, + } + requests = append(requests, requestData) + } + return requests, nil +} + +func getImpressionExt(imp *openrtb2.Imp) (*openrtb_ext.ExtSmrtconnect, error) { + var bidderExt adapters.ExtImpBidder + if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "ext.bidder not provided", + } + } + var smrtconnectExt openrtb_ext.ExtSmrtconnect + if err := json.Unmarshal(bidderExt.Bidder, &smrtconnectExt); err != nil { + return nil, &errortypes.BadInput{ + Message: "ext.bidder not provided", + } + } + imp.Ext = nil + return &smrtconnectExt, nil +} + +func (a *adapter) buildEndpointURL(params *openrtb_ext.ExtSmrtconnect) (string, error) { + endpointParams := macros.EndpointTemplateParams{SupplyId: params.SupplyId} + return macros.ResolveMacros(a.endpoint, endpointParams) +} + +func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) { + if adapters.IsResponseStatusCodeNoContent(responseData) { + return nil, nil + } + + if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil { + return nil, []error{err} + } + + var response openrtb2.BidResponse + if err := json.Unmarshal(responseData.Body, &response); err != nil { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Bad Server Response", + }} + } + + if len(response.SeatBid) == 0 { + return nil, []error{&errortypes.BadServerResponse{ + Message: "Empty SeatBid array", + }} + } + + var bidErrs []error + bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp)) + bidResponse.Currency = response.Cur + for _, seatBid := range response.SeatBid { + for i := range seatBid.Bid { + bidType, err := getBidType(seatBid.Bid[i]) + if err != nil { + // could not determinate media type, append an error and continue with the next bid. + bidErrs = append(bidErrs, err) + continue + } + bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{ + Bid: &seatBid.Bid[i], + BidType: bidType, + }) + } + } + return bidResponse, bidErrs +} + +func getBidType(bid openrtb2.Bid) (openrtb_ext.BidType, error) { + // determinate media type by bid response field mtype + switch bid.MType { + case openrtb2.MarkupBanner: + return openrtb_ext.BidTypeBanner, nil + case openrtb2.MarkupVideo: + return openrtb_ext.BidTypeVideo, nil + case openrtb2.MarkupAudio: + return openrtb_ext.BidTypeAudio, nil + case openrtb2.MarkupNative: + return openrtb_ext.BidTypeNative, nil + } + + return "", &errortypes.BadInput{ + Message: fmt.Sprintf("Could not define media type for impression: %s", bid.ImpID), + } +} diff --git a/adapters/smrtconnect/smrtconnect_test.go b/adapters/smrtconnect/smrtconnect_test.go new file mode 100644 index 00000000000..38d071d94d1 --- /dev/null +++ b/adapters/smrtconnect/smrtconnect_test.go @@ -0,0 +1,28 @@ +package smrtconnect + +import ( + "testing" + + "github.com/prebid/prebid-server/v2/adapters/adapterstest" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestJsonSamples(t *testing.T) { + bidder, buildErr := Builder(openrtb_ext.BidderSmrtconnect, config.Adapter{ + Endpoint: "http://test.smrtconnect.com/openrtb2/auction?supply_id={{.SupplyId}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 196}) + + if buildErr != nil { + t.Fatalf("Builder returned unexpected error %v", buildErr) + } + + adapterstest.RunJSONBidderTest(t, "smrtconnecttest", bidder) +} + +func TestEndpointTemplateMalformed(t *testing.T) { + _, buildErr := Builder(openrtb_ext.BidderSmrtconnect, config.Adapter{ + Endpoint: "{{Malformed}}"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 196}) + + assert.Error(t, buildErr) +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json new file mode 100644 index 00000000000..7f7c23d9eba --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-app.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "test-request-id", + "app": { + "bundle": "com.prebid" + }, + "device": { + "ifa":"ec943cb9-61ec-460f-a925-6489c3fcc4e3" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "smrtconnect", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "mtype": 3 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json new file mode 100644 index 00000000000..f30494a499d --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/audio-web.json @@ -0,0 +1,90 @@ +{ + "mockBidRequest": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "test-request-id", + "site": { + "page": "prebid.org" + }, + "user": { + "buyeruid": "be5e209ad46927520000000000000000" + }, + "imp": [ + { + "id": "test-imp-id", + "audio": { + "mimes": ["audio/mp4"], + "protocols": [9,10] + } + } + ] + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "test-request-id", + "seatbid": [ + { + "seat": "smrtconnect", + "bid": [{ + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.500000, + "adm": "some-test-ad", + "crid": "test-crid", + "mtype": 3 + }] + } + ], + "cur": "USD" + } + } + } + ], + + "expectedBidResponses": [ + { + "currency": "USD", + "bids": [ + { + "bid": { + "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800", + "impid": "test-imp-id", + "price": 0.5, + "adm": "some-test-ad", + "crid": "test-crid", + "mtype": 3 + }, + "type": "audio" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json new file mode 100644 index 00000000000..4111eb732ee --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-app.json @@ -0,0 +1,139 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w": 320, + "h": 50 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w": 320, + "h": 50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json new file mode 100644 index 00000000000..18391277a72 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-multiple-bids.json @@ -0,0 +1,227 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + }, + { + "id": "some-impression-id-2", + "tagid": "ogTAGID-2", + "banner": { + "w":300, + "h":250 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + }, + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id-2", + "tagid": "ogTAGID-2", + "banner": { + "w":300, + "h":250 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id-2", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup-2", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 300, + "h": 250, + "mtype": 1 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + }, + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup-2", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 300, + "h": 250, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json new file mode 100644 index 00000000000..69743fd6383 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/banner-web.json @@ -0,0 +1,127 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 320, + "h": 50, + "mtype": 1 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "1", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 320, + "h": 50, + "mtype": 1 + }, + "type": "banner" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json new file mode 100644 index 00000000000..63188c0cb51 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/native-app.json @@ -0,0 +1,136 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] + } + \ No newline at end of file diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json new file mode 100644 index 00000000000..dd44926e360 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/native-web.json @@ -0,0 +1,124 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + }, + "ext": { + "bidder": { + "host": "ep1", + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ipv6": "2607:fb90:f27:4512:d800:cb23:a603:e245", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "native": { + "ver":"1.1", + "request":"{\"adunit\":2,\"assets\":[{\"id\":3,\"img\":{\"h\":120,\"hmin\":0,\"type\":3,\"w\":180,\"wmin\":0},\"required\":1},{\"id\":0,\"required\":1,\"title\":{\"len\":25}},{\"data\":{\"len\":25,\"type\":1},\"id\":4,\"required\":1},{\"data\":{\"len\":140,\"type\":2},\"id\":6,\"required\":1}],\"context\":1,\"layout\":1,\"contextsubtype\":11,\"plcmtcnt\":1,\"plcmttype\":2,\"ver\":\"1.1\",\"ext\":{\"banner\":{\"w\":320,\"h\":50}}}" + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "mtype": 4 + } + ], + "seat": "acuityads" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "acuityads": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "mtype": 4 + }, + "type": "native" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json b/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json new file mode 100644 index 00000000000..88d3066619c --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/video-app.json @@ -0,0 +1,149 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2 + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids":[ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "crid": "20", + "adomain": [ + "awesome.com" + ], + "w": 1280, + "h": 720, + "mtype": 2 + }, + "type": "video" + } + ] + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json b/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json new file mode 100644 index 00000000000..cab53723aaf --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/exemplary/video-web.json @@ -0,0 +1,147 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + { + "bid": [ + { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + } + ], + "seat": "smrtconnect" + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "expectedBidResponses": [ + { + "bids": [ + { + "bid": { + "id": "a3ae1b4e2fc24a4fb45540082e98e161", + "impid": "some-impression-id", + "price": 3.5, + "adm": "awesome-markup", + "adomain": [ + "awesome.com" + ], + "crid": "20", + "w": 1280, + "h": 720, + "mtype": 2, + "ext": { + "prebid": { + "type": "video" + } + } + }, + "type":"video" + } + ] + } + ] +} \ No newline at end of file diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json b/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json new file mode 100644 index 00000000000..719bc762169 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/empty-seatbid-array.json @@ -0,0 +1,119 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + "httpCalls": [ + { + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": { + "id": "awesome-resp-id", + "seatbid": [ + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "smrtconnect": 154 + }, + "tmaxrequest": 1000 + } + } + } + } + ], + "mockResponse": { + "status": 200, + "body": "invalid response" + }, + "expectedMakeBidsErrors": [ + { + "value": "Empty SeatBid array", + "comparison": "literal" + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json new file mode 100644 index 00000000000..77752d01edf --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-aceex-ext-object.json @@ -0,0 +1,29 @@ +{ + "expectedMakeRequestsErrors": [ + { + "value": "ext.bidder not provided", + "comparison": "literal" + } + ], + "mockBidRequest": { + "id": "test-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "my-adcode", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": "Awesome" + } + ], + "site": { + "page": "test.com" + } + }, + "httpCalls": [] +} diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json new file mode 100644 index 00000000000..6ae0e5e4b34 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/invalid-response.json @@ -0,0 +1,94 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "banner": { + "w":320, + "h":50 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "device": { + "ua": "test-user-agent", + "ip": "123.123.123.123", + "language": "en", + "dnt": 0 + }, + "imp": [ + { + "id": "some-impression-id", + "banner": { + "w":320, + "h":50 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "id": "123456789", + "name": "Awesome App", + "bundle": "com.app.awesome", + "domain": "awesomeapp.com", + "cat": [ + "IAB22-1" + ], + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 200, + "body": "invalid response" + } + }], + "expectedMakeBidsErrors": [ + { + "value": "Bad Server Response", + "comparison": "literal" + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json new file mode 100644 index 00000000000..daaa957c6c5 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-bad-request.json @@ -0,0 +1,92 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + }, + "tagid": "ogTAGID" + } + ], + "app": { + "publisher": { + "id": "123456789" + }, + "cat": [ + "IAB22-1" + ], + "bundle": "com.app.awesome", + "name": "Awesome App", + "domain": "awesomeapp.com", + "id": "123456789" + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 400 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 400. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json new file mode 100644 index 00000000000..734899fb328 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-no-content.json @@ -0,0 +1,68 @@ +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [{ + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + }] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "imp": [{ + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + }], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 204 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [] +} \ No newline at end of file diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json new file mode 100644 index 00000000000..3f3ecf015be --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-other-error.json @@ -0,0 +1,78 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 306 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 306. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json new file mode 100644 index 00000000000..2e0212f13a8 --- /dev/null +++ b/adapters/smrtconnect/smrtconnecttest/supplemental/status-code-service-unavailable.json @@ -0,0 +1,78 @@ + +{ + "mockBidRequest": { + "id": "some-request-id", + "tmax": 1000, + "user": { + "buyeruid": "awesome-user" + }, + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": ["video/mp4"], + "w": 640, + "h": 480, + "minduration": 120, + "maxduration": 150 + }, + "ext": { + "bidder": { + "supply_id": "1" + } + } + } + ] + }, + + "httpCalls": [{ + "expectedRequest": { + "uri": "http://test.smrtconnect.com/openrtb2/auction?supply_id=1", + "body": { + "id": "some-request-id", + "imp": [ + { + "id": "some-impression-id", + "tagid": "ogTAGID", + "video": { + "mimes": [ + "video/mp4" + ], + "minduration": 120, + "maxduration": 150, + "w": 640, + "h": 480 + } + } + ], + "site": { + "page": "test.com", + "publisher": { + "id": "123456789" + } + }, + "user": { + "buyeruid": "awesome-user" + }, + "tmax": 1000 + } + }, + "mockResponse": { + "status": 503 + } + }], + "expectedBidResponses": [], + "expectedMakeBidsErrors": [ + { + "value": "Unexpected status code: 503. Run with request.debug = 1 for more info", + "comparison": "literal" + } + ] +} diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go index a85516667a4..416bd164445 100755 --- a/exchange/adapter_builders.go +++ b/exchange/adapter_builders.go @@ -164,6 +164,7 @@ import ( "github.com/prebid/prebid-server/v2/adapters/smartx" "github.com/prebid/prebid-server/v2/adapters/smartyads" "github.com/prebid/prebid-server/v2/adapters/smilewanted" + "github.com/prebid/prebid-server/v2/adapters/smrtconnect" "github.com/prebid/prebid-server/v2/adapters/sonobi" "github.com/prebid/prebid-server/v2/adapters/sovrn" "github.com/prebid/prebid-server/v2/adapters/sovrnXsp" @@ -371,6 +372,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder { openrtb_ext.BidderSmartx: smartx.Builder, openrtb_ext.BidderSmartyAds: smartyads.Builder, openrtb_ext.BidderSmileWanted: smilewanted.Builder, + openrtb_ext.BidderSmrtconnect: smrtconnect.Builder, openrtb_ext.BidderSonobi: sonobi.Builder, openrtb_ext.BidderSovrn: sovrn.Builder, openrtb_ext.BidderSovrnXsp: sovrnXsp.Builder, diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go index 71bed294dbe..ea74cf94b8c 100644 --- a/openrtb_ext/bidders.go +++ b/openrtb_ext/bidders.go @@ -182,6 +182,7 @@ var coreBidderNames []BidderName = []BidderName{ BidderSmartx, BidderSmartyAds, BidderSmileWanted, + BidderSmrtconnect, BidderSonobi, BidderSovrn, BidderSovrnXsp, @@ -465,6 +466,7 @@ const ( BidderSmartx BidderName = "smartx" BidderSmartyAds BidderName = "smartyads" BidderSmileWanted BidderName = "smilewanted" + BidderSmrtconnect BidderName = "smrtconnect" BidderSonobi BidderName = "sonobi" BidderSovrn BidderName = "sovrn" BidderSovrnXsp BidderName = "sovrnXsp" diff --git a/openrtb_ext/imp_smrtconnect.go b/openrtb_ext/imp_smrtconnect.go new file mode 100644 index 00000000000..c3e6b0cd7e9 --- /dev/null +++ b/openrtb_ext/imp_smrtconnect.go @@ -0,0 +1,5 @@ +package openrtb_ext + +type ExtSmrtconnect struct { + SupplyId string `json:"supply_id"` +} diff --git a/static/bidder-info/smrtconnect.yaml b/static/bidder-info/smrtconnect.yaml new file mode 100644 index 00000000000..7078248d3e9 --- /dev/null +++ b/static/bidder-info/smrtconnect.yaml @@ -0,0 +1,20 @@ +endpoint: "https://amp.smrtconnect.com/openrtb2/auction?supply_id={{.SupplyId}}" +# This bidder does not operate globally. Please consider setting "disabled: true" in European datacenters. +geoscope: + - "!EEA" +endpointCompression: gzip +maintainer: + email: "prebid@smrtconnect.com" +capabilities: + app: + mediaTypes: + - banner + - native + - video + - audio + site: + mediaTypes: + - banner + - native + - video + - audio \ No newline at end of file diff --git a/static/bidder-params/smrtconnect.json b/static/bidder-params/smrtconnect.json new file mode 100644 index 00000000000..6916d4567b0 --- /dev/null +++ b/static/bidder-params/smrtconnect.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Smrtconnect Params", + "description": "A schema which validates params accepted by the Smrtconnect", + "type": "object", + "properties": { + "supply_id": { + "type": "string", + "description": "Supply id", + "minLength": 1 + } + }, + "required": ["supply_id"] +} From e982bfebedb696f9bce57a3018cc7592cf62fec1 Mon Sep 17 00:00:00 2001 From: Nilesh Chate <97721111+pm-nilesh-chate@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:53:25 +0530 Subject: [PATCH 68/69] Privacy Sandbox: Topics in headers (#3393) --- config/account.go | 1 + config/config.go | 1 + config/config_test.go | 3 + endpoints/openrtb2/amp_auction.go | 12 +- endpoints/openrtb2/amp_auction_test.go | 86 ++- endpoints/openrtb2/auction.go | 59 +- endpoints/openrtb2/auction_test.go | 206 ++++- .../video_invalid_sample_negative_tmax.json | 87 +++ endpoints/openrtb2/video_auction.go | 15 +- endpoints/openrtb2/video_auction_test.go | 43 +- errortypes/code.go | 1 + errortypes/errortypes.go | 23 + errortypes/scope.go | 19 + errortypes/scope_test.go | 37 + exchange/exchange.go | 3 + privacysandbox/topics.go | 228 ++++++ privacysandbox/topics_test.go | 722 ++++++++++++++++++ 17 files changed, 1522 insertions(+), 24 deletions(-) create mode 100644 endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json create mode 100644 errortypes/scope.go create mode 100644 errortypes/scope_test.go create mode 100644 privacysandbox/topics.go create mode 100644 privacysandbox/topics_test.go diff --git a/config/account.go b/config/account.go index 72b6c07a81e..cd2a38ffb8d 100644 --- a/config/account.go +++ b/config/account.go @@ -341,6 +341,7 @@ type AccountPrivacy struct { } type PrivacySandbox struct { + TopicsDomain string `mapstructure:"topicsdomain"` CookieDeprecation CookieDeprecation `mapstructure:"cookiedeprecation"` } diff --git a/config/config.go b/config/config.go index b2204053fac..3b306ac03d7 100644 --- a/config/config.go +++ b/config/config.go @@ -1146,6 +1146,7 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("account_defaults.price_floors.fetch.max_age_sec", 86400) v.SetDefault("account_defaults.price_floors.fetch.period_sec", 3600) v.SetDefault("account_defaults.price_floors.fetch.max_schema_dims", 0) + v.SetDefault("account_defaults.privacy.privacysandbox.topicsdomain", "") v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false) v.SetDefault("account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800) diff --git a/config/config_test.go b/config/config_test.go index 3cad8fa12f5..601c3194cb8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -203,6 +203,7 @@ func TestDefaults(t *testing.T) { cmpInts(t, "account_defaults.price_floors.fetch.period_sec", 3600, cfg.AccountDefaults.PriceFloors.Fetcher.Period) cmpInts(t, "account_defaults.price_floors.fetch.max_age_sec", 86400, cfg.AccountDefaults.PriceFloors.Fetcher.MaxAge) cmpInts(t, "account_defaults.price_floors.fetch.max_schema_dims", 0, cfg.AccountDefaults.PriceFloors.Fetcher.MaxSchemaDims) + cmpStrings(t, "account_defaults.privacy.topicsdomain", "", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain) cmpBools(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.enabled", false, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) cmpInts(t, "account_defaults.privacy.privacysandbox.cookiedeprecation.ttl_sec", 604800, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) @@ -528,6 +529,7 @@ account_defaults: ipv4: anon_keep_bits: 20 privacysandbox: + topicsdomain: "test.com" cookiedeprecation: enabled: true ttl_sec: 86400 @@ -665,6 +667,7 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "account_defaults.privacy.ipv6.anon_keep_bits", 50, cfg.AccountDefaults.Privacy.IPv6Config.AnonKeepBits) cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 20, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) + cmpStrings(t, "account_defaults.privacy.topicsdomain", "test.com", cfg.AccountDefaults.Privacy.PrivacySandbox.TopicsDomain) cmpBools(t, "account_defaults.privacy.cookiedeprecation.enabled", true, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.Enabled) cmpInts(t, "account_defaults.privacy.cookiedeprecation.ttl_sec", 86400, cfg.AccountDefaults.Privacy.PrivacySandbox.CookieDeprecation.TTLSec) diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go index 1b53b182ab8..4a6574e8421 100644 --- a/endpoints/openrtb2/amp_auction.go +++ b/endpoints/openrtb2/amp_auction.go @@ -156,6 +156,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h w.Header().Set("AMP-Access-Control-Allow-Source-Origin", origin) w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) // There is no body for AMP requests, so we pass a nil body and ignore the return value. _, rejectErr := hookExecutor.ExecuteEntrypointStage(r, nilBody) @@ -230,6 +231,11 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h return } + // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). + if errs := deps.setFieldsImplicitly(r, reqWrapper, account); len(errs) > 0 { + errL = append(errL, errs...) + } + hasStoredResponses := len(storedAuctionResponses) > 0 errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false) errL = append(errL, errs...) @@ -441,6 +447,9 @@ func getExtBidResponse( warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage) } for _, v := range errortypes.WarningOnly(errs) { + if errortypes.ReadScope(v) == errortypes.ScopeDebug && !(reqWrapper != nil && reqWrapper.Test == 1) { + continue + } bidderErr := openrtb_ext.ExtBidderMessage{ Code: errortypes.ReadCode(v), Message: v.Error(), @@ -501,9 +510,6 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr // move to using the request wrapper req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal} - // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(httpRequest, req) - // Need to ensure cache and targeting are turned on e = initAmpTargetingAndCache(req) if errs = append(errs, e...); errortypes.ContainsFatalError(errs) { diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go index 08dec09e5dc..6e6e1ac855e 100644 --- a/endpoints/openrtb2/amp_auction_test.go +++ b/endpoints/openrtb2/amp_auction_test.go @@ -2052,7 +2052,7 @@ func TestAmpAuctionResponseHeaders(t *testing.T) { ) for _, test := range testCases { - httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil) + httpReq := httptest.NewRequest("GET", "/openrtb2/auction/amp"+test.requestURLArguments, nil) recorder := httptest.NewRecorder() endpoint(recorder, httpReq, nil) @@ -2479,3 +2479,87 @@ func TestSetSeatNonBid(t *testing.T) { }) } } + +func TestAmpAuctionDebugWarningsOnly(t *testing.T) { + testCases := []struct { + description string + requestURLArguments string + addRequestHeaders func(r *http.Request) + expectedStatus int + expectedWarnings map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage + }{ + { + description: "debug_enabled_request_with_invalid_Sec-Browsing-Topics_header", + requestURLArguments: "?tag_id=1&debug=1", + addRequestHeaders: func(r *http.Request) { + r.Header.Add("Sec-Browsing-Topics", "foo") + }, + expectedStatus: 200, + expectedWarnings: map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage{ + "general": { + { + Code: 10012, + Message: "Invalid field in Sec-Browsing-Topics header: foo", + }, + }, + }, + }, + { + description: "debug_disabled_request_with_invalid_Sec-Browsing-Topics_header", + requestURLArguments: "?tag_id=1", + addRequestHeaders: func(r *http.Request) { + r.Header.Add("Sec-Browsing-Topics", "foo") + }, + expectedStatus: 200, + expectedWarnings: nil, + }, + } + + storedRequests := map[string]json.RawMessage{ + "1": json.RawMessage(validRequest(t, "site.json")), + } + exchange := &nobidExchange{} + endpoint, _ := NewAmpEndpoint( + fakeUUIDGenerator{}, + exchange, + newParamsValidator(t), + &mockAmpStoredReqFetcher{storedRequests}, + empty_fetcher.EmptyFetcher{}, + &config.Configuration{ + MaxRequestSize: maxSize, + AccountDefaults: config.Account{ + Privacy: config.AccountPrivacy{ + PrivacySandbox: config.PrivacySandbox{ + TopicsDomain: "abc", + }, + }, + }, + }, + &metricsConfig.NilMetricsEngine{}, + analyticsBuild.New(&config.Analytics{}), + map[string]string{}, + []byte{}, + openrtb_ext.BuildBidderMap(), + empty_fetcher.EmptyFetcher{}, + hooks.EmptyPlanBuilder{}, + nil, + ) + + for _, test := range testCases { + httpReq := httptest.NewRequest("GET", fmt.Sprintf("/openrtb2/auction/amp"+test.requestURLArguments), nil) + test.addRequestHeaders(httpReq) + recorder := httptest.NewRecorder() + + endpoint(recorder, httpReq, nil) + + assert.Equal(t, test.expectedStatus, recorder.Result().StatusCode) + + // Parse Response + var response AmpResponse + if err := jsonutil.UnmarshalValid(recorder.Body.Bytes(), &response); err != nil { + t.Fatalf("Error unmarshalling response: %s", err.Error()) + } + + assert.Equal(t, test.expectedWarnings, response.ORTB2.Ext.Warnings) + } +} diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go index 9accae2e041..8c178ff3c0b 100644 --- a/endpoints/openrtb2/auction.go +++ b/endpoints/openrtb2/auction.go @@ -29,6 +29,7 @@ import ( "github.com/prebid/prebid-server/v2/hooks" "github.com/prebid/prebid-server/v2/ortb" "github.com/prebid/prebid-server/v2/privacy" + "github.com/prebid/prebid-server/v2/privacysandbox" "golang.org/x/net/publicsuffix" jsonpatch "gopkg.in/evanphx/json-patch.v4" @@ -61,6 +62,9 @@ const storedRequestTimeoutMillis = 50 const ampChannel = "amp" const appChannel = "app" const secCookieDeprecation = "Sec-Cookie-Deprecation" +const secBrowsingTopics = "Sec-Browsing-Topics" +const observeBrowsingTopics = "Observe-Browsing-Topics" +const observeBrowsingTopicsValue = "?1" var ( dntKey string = http.CanonicalHeaderKey("DNT") @@ -190,6 +194,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http }() w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) req, impExtInfoMap, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, account, errL := deps.parseRequest(r, &labels, hookExecutor) if errortypes.ContainsFatalError(errL) && writeError(errL, w, &labels) { @@ -393,6 +398,13 @@ func sendAuctionResponse( return labels, ao } +// 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 != "" { + w.Header().Set(observeBrowsingTopics, observeBrowsingTopicsValue) + } +} + // parseRequest turns the HTTP request into an OpenRTB request. This is guaranteed to return: // // - A context which times out appropriately, given the request. @@ -406,6 +418,7 @@ func sendAuctionResponse( func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metrics.Labels, hookExecutor hookexecution.HookStageExecutor) (req *openrtb_ext.RequestWrapper, impExtInfoMap map[string]exchange.ImpExtInfo, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImpId stored_responses.BidderImpReplaceImpID, account *config.Account, errs []error) { errs = nil var err error + var errL []error var r io.ReadCloser = httpRequest.Body reqContentEncoding := httputil.ContentEncoding(httpRequest.Header.Get("Content-Encoding")) if reqContentEncoding != "" { @@ -532,7 +545,9 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric } // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(httpRequest, req) + if errsL := deps.setFieldsImplicitly(httpRequest, req, account); len(errsL) > 0 { + errs = append(errs, errsL...) + } if err := ortb.SetDefaults(req); err != nil { errs = []error{err} @@ -547,13 +562,14 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request, labels *metric lmt.ModifyForIOS(req.BidRequest) //Stored auction responses should be processed after stored requests due to possible impression modification - storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errs = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher) - if len(errs) > 0 { + storedAuctionResponses, storedBidResponses, bidderImpReplaceImpId, errL = stored_responses.ProcessStoredResponses(ctx, req, deps.storedRespFetcher) + if len(errL) > 0 { + errs = append(errs, errL...) return nil, nil, nil, nil, nil, nil, errs } hasStoredResponses := len(storedAuctionResponses) > 0 - errL := deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) + errL = deps.validateRequest(account, httpRequest, req, false, hasStoredResponses, storedBidResponses, hasStoredBidRequest) if len(errL) > 0 { errs = append(errs, errL...) } @@ -876,7 +892,7 @@ func (deps *endpointDeps) validateRequest(account *config.Account, httpReq *http return append(errL, err) } - if err := validateOrFillCDep(httpReq, req, account); err != nil { + if err := validateOrFillCookieDeprecation(httpReq, req, account); err != nil { errL = append(errL, err) } @@ -1919,7 +1935,7 @@ func validateDevice(device *openrtb2.Device) error { return nil } -func validateOrFillCDep(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error { +func validateOrFillCookieDeprecation(httpReq *http.Request, req *openrtb_ext.RequestWrapper, account *config.Account) error { if account == nil || !account.Privacy.PrivacySandbox.CookieDeprecation.Enabled { return nil } @@ -2029,7 +2045,7 @@ func sanitizeRequest(r *openrtb_ext.RequestWrapper, ipValidator iputil.IPValidat // OpenRTB properties from the headers and other implicit info. // // This function _should not_ override any fields which were defined explicitly by the caller in the request. -func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { +func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error { sanitizeRequest(r, deps.privateNetworkIPValidator) setDeviceImplicitly(httpReq, r, deps.privateNetworkIPValidator) @@ -2041,6 +2057,9 @@ func (deps *endpointDeps) setFieldsImplicitly(httpReq *http.Request, r *openrtb_ } setAuctionTypeImplicitly(r) + + errs := setSecBrowsingTopicsImplicitly(httpReq, r, account) + return errs } // setDeviceImplicitly uses implicit info from httpReq to populate bidReq.Device @@ -2048,7 +2067,6 @@ func setDeviceImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, i setIPImplicitly(httpReq, r, ipValidtor) setUAImplicitly(httpReq, r) setDoNotTrackImplicitly(httpReq, r) - } // setAuctionTypeImplicitly sets the auction type to 1 if it wasn't on the request, @@ -2059,6 +2077,31 @@ func setAuctionTypeImplicitly(r *openrtb_ext.RequestWrapper) { } } +// setSecBrowsingTopicsImplicitly updates user.data with data from request header 'Sec-Browsing-Topics' +func setSecBrowsingTopicsImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper, account *config.Account) []error { + secBrowsingTopics := httpReq.Header.Get(secBrowsingTopics) + if secBrowsingTopics == "" { + return nil + } + + // host must configure privacy sandbox + if account == nil || account.Privacy.PrivacySandbox.TopicsDomain == "" { + return nil + } + + topics, errs := privacysandbox.ParseTopicsFromHeader(secBrowsingTopics) + if len(topics) == 0 { + return errs + } + + if r.User == nil { + r.User = &openrtb2.User{} + } + + r.User.Data = privacysandbox.UpdateUserDataWithTopics(r.User.Data, topics, account.Privacy.PrivacySandbox.TopicsDomain) + return errs +} + func setSiteImplicitly(httpReq *http.Request, r *openrtb_ext.RequestWrapper) { if r.Site == nil { r.Site = &openrtb2.Site{} diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go index 5356815d81b..af6f4a1b86a 100644 --- a/endpoints/openrtb2/auction_test.go +++ b/endpoints/openrtb2/auction_test.go @@ -13,6 +13,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "sort" "strings" "testing" "time" @@ -4660,13 +4661,13 @@ func TestValidateNativeAssetData(t *testing.T) { func TestAuctionResponseHeaders(t *testing.T) { testCases := []struct { description string - requestBody string + httpRequest *http.Request expectedStatus int expectedHeaders func(http.Header) }{ { description: "Success Response", - requestBody: validRequest(t, "site.json"), + httpRequest: httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))), expectedStatus: 200, expectedHeaders: func(h http.Header) { h.Set("X-Prebid", "pbs-go/unknown") @@ -4675,12 +4676,39 @@ func TestAuctionResponseHeaders(t *testing.T) { }, { description: "Failure Response", - requestBody: "{}", + httpRequest: httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}")), expectedStatus: 400, expectedHeaders: func(h http.Header) { h.Set("X-Prebid", "pbs-go/unknown") }, }, + { + description: "Success Response with Chrome BrowsingTopicsHeader", + httpRequest: func() *http.Request { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(validRequest(t, "site.json"))) + httpReq.Header.Add(secBrowsingTopics, "sample-value") + return httpReq + }(), + expectedStatus: 200, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Content-Type", "application/json") + h.Set("Observe-Browsing-Topics", "?1") + }, + }, + { + description: "Failure Response with Chrome BrowsingTopicsHeader", + httpRequest: func() *http.Request { + httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader("{}")) + httpReq.Header.Add(secBrowsingTopics, "sample-value") + return httpReq + }(), + expectedStatus: 400, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Observe-Browsing-Topics", "?1") + }, + }, } exchange := &nobidExchange{} @@ -4702,10 +4730,9 @@ func TestAuctionResponseHeaders(t *testing.T) { ) for _, test := range testCases { - httpReq := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(test.requestBody)) recorder := httptest.NewRecorder() - endpoint(recorder, httpReq, nil) + endpoint(recorder, test.httpRequest, nil) expectedHeaders := http.Header{} test.expectedHeaders(expectedHeaders) @@ -6048,7 +6075,7 @@ func fakeNormalizeBidderName(name string) (openrtb_ext.BidderName, bool) { return openrtb_ext.BidderName(strings.ToLower(name)), true } -func TestValidateOrFillCDep(t *testing.T) { +func TestValidateOrFillCookieDeprecation(t *testing.T) { type args struct { httpReq *http.Request req *openrtb_ext.RequestWrapper @@ -6280,7 +6307,7 @@ func TestValidateOrFillCDep(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := validateOrFillCDep(tt.args.httpReq, tt.args.req, &tt.args.account) + err := validateOrFillCookieDeprecation(tt.args.httpReq, tt.args.req, &tt.args.account) assert.Equal(t, tt.wantErr, err) if tt.args.req != nil { err := tt.args.req.RebuildRequest() @@ -6436,3 +6463,168 @@ func TestValidateRequestCookieDeprecation(t *testing.T) { assert.Equal(t, test.wantCDep, deviceExt.GetCDep()) } } + +func TestSetSecBrowsingTopicsImplicitly(t *testing.T) { + type args struct { + httpReq *http.Request + r *openrtb_ext.RequestWrapper + account *config.Account + } + tests := []struct { + name string + args args + wantUser *openrtb2.User + }{ + { + name: "empty HTTP request, no change in user data", + args: args{ + httpReq: &http.Request{}, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: nil, + }, + { + name: "valid topic in request but topicsdomain not configured by host, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: ""}}}, + }, + wantUser: nil, + }, + { + name: "valid topic in request and topicsdomain configured by host, topics copied to user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, ();p=P00000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + { + ID: "1", + }, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + }, + { + name: "valid empty topic in request, no change in user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"();p=P0000000000000000000000000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: nil, + }, + { + name: "request with a few valid topics (including duplicate topics, segIDs, matching segtax, segclass, etc) and a few invalid topics(different invalid format), only valid and unique topics copied/merged to/with user data", + args: args{ + httpReq: &http.Request{ + Header: http.Header{ + secBrowsingTopics: []string{"(1);v=chrome.1:1:2, (1 2);v=chrome.1:1:2,(4);v=chrome.1:1:2,();p=P0000000000,(4);v=chrome.1, 5);v=chrome.1, (6;v=chrome.1, ();v=chrome.1, ( );v=chrome.1, (1);v=chrome.1:1:2, (1 2 4 6 7 4567 ) ; v=chrome.1: 2 : 3,();p=P0000000000"}, + }, + }, + r: &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{ + User: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + }, + Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + }}, + account: &config.Account{Privacy: config.AccountPrivacy{PrivacySandbox: config.PrivacySandbox{TopicsDomain: "ads.pubmatic.com"}}}, + }, + wantUser: &openrtb2.User{ + Data: []openrtb2.Data{ + { + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + }, + Ext: json.RawMessage(`{"segtax":603,"segclass":"4"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "4"}, + {ID: "6"}, + {ID: "7"}, + {ID: "4567"}, + }, + Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setSecBrowsingTopicsImplicitly(tt.args.httpReq, tt.args.r, tt.args.account) + + // sequence is not guaranteed we're using a map to filter segids + sortUserData(tt.wantUser) + sortUserData(tt.args.r.User) + assert.Equal(t, tt.wantUser, tt.args.r.User, tt.name) + }) + } +} + +func sortUserData(user *openrtb2.User) { + if user != nil { + sort.Slice(user.Data, func(i, j int) bool { + if user.Data[i].Name == user.Data[j].Name { + return string(user.Data[i].Ext) < string(user.Data[j].Ext) + } + return user.Data[i].Name < user.Data[j].Name + }) + for g := range user.Data { + sort.Slice(user.Data[g].Segment, func(i, j int) bool { + return user.Data[g].Segment[i].ID < user.Data[g].Segment[j].ID + }) + } + } +} diff --git a/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json b/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json new file mode 100644 index 00000000000..e1dc02314f8 --- /dev/null +++ b/endpoints/openrtb2/sample-requests/video/video_invalid_sample_negative_tmax.json @@ -0,0 +1,87 @@ +{ + "description": "video request with negative tmax value. Expect error", + + "requestPayload": { + "tmax": -2, + "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22", + "podconfig": { + "durationrangesec": [ + 30 + ], + "requireexactduration": true, + "pods": [{ + "podid": 1, + "adpoddurationsec": 180, + "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6" + }, + { + "podid": 2, + "adpoddurationsec": 150, + "configid": "8b452b41-2681-4a20-9086-6f16ffad7773" + } + ] + }, + "site": { + "page": "prebid.com" + }, + "regs": { + "ext": { + "gdpr": 0 + } + }, + "user": { + "yob": 1991, + "gender": "F", + "keywords": "Hotels, Travelling", + "ext": { + "prebid": { + "buyeruids": { + "appnexus": "unique_id_an", + "rubicon": "unique_id_rubi" + } + } + } + }, + "device": { + "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2", + "ip": "123.145.167.10", + "devicetype": 1, + "ifa": "AA000DFE74168477C70D291f574D344790E0BB11", + "lmt": 44, + "os": "mac os", + "w": 640, + "h": 480, + "didsha1": "didsha1", + "didmd5": "didmd5", + "dpidsha1": "dpidsha1", + "dpidmd5": "dpidmd5", + "macsha1": "macsha1", + "macmd5": "macmd5" + }, + "includebrandcategory": { + "primaryadserver": 1, + "publisher": "" + }, + "video": { + "w": 640, + "h": 480, + "mimes": [ + "video/mp4" + ], + "protocols": [ + 2, 3, 5, 6 + ] + }, + "content": { + "episode": 6, + "title": "episodeName", + "series": "TvName", + "season": "season3", + "len": 900, + "livestream": 0 + }, + "cacheconfig": { + "ttl": 42 + } + } + } \ No newline at end of file diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go index 3ddfc31aa39..f74a4a224ae 100644 --- a/endpoints/openrtb2/video_auction.go +++ b/endpoints/openrtb2/video_auction.go @@ -165,6 +165,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re }() w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + setBrowsingTopicsHeader(w, r) lr := &io.LimitedReader{ R: r.Body, @@ -259,9 +260,6 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re // all code after this line should use the bidReqWrapper instead of bidReq directly bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq} - // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). - deps.setFieldsImplicitly(r, bidReqWrapper) - if err := ortb.SetDefaults(bidReqWrapper); err != nil { handleError(&labels, w, errL, &vo, &debugLog) return @@ -300,7 +298,13 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re return } - errL = deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) + // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). + if errs := deps.setFieldsImplicitly(r, bidReqWrapper, account); len(errs) > 0 { + errL = append(errL, errs...) + } + + errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) + errL = append(errL, errs...) if errortypes.ContainsFatalError(errL) { handleError(&labels, w, errL, &vo, &debugLog) return @@ -308,6 +312,8 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re activityControl = privacy.NewActivityControl(&account.Privacy) + warnings := errortypes.WarningOnly(errL) + secGPC := r.Header.Get("Sec-GPC") auctionRequest := &exchange.AuctionRequest{ BidRequestWrapper: bidReqWrapper, @@ -316,6 +322,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re RequestType: labels.RType, StartTime: start, LegacyLabels: labels, + Warnings: warnings, GlobalPrivacyControlHeader: secGPC, PubID: labels.PubID, HookExecutor: hookexecution.EmptyHookExecutor{}, diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go index 98d6ca35c49..cc522c91d25 100644 --- a/endpoints/openrtb2/video_auction_test.go +++ b/endpoints/openrtb2/video_auction_test.go @@ -1162,6 +1162,7 @@ func TestVideoAuctionResponseHeaders(t *testing.T) { testCases := []struct { description string givenTestFile string + givenHeader map[string]string expectedStatus int expectedHeaders func(http.Header) }{ @@ -1173,7 +1174,8 @@ func TestVideoAuctionResponseHeaders(t *testing.T) { h.Set("X-Prebid", "pbs-go/unknown") h.Set("Content-Type", "application/json") }, - }, { + }, + { description: "Failure Response", givenTestFile: "sample-requests/video/video_invalid_sample.json", expectedStatus: 500, @@ -1181,6 +1183,27 @@ func TestVideoAuctionResponseHeaders(t *testing.T) { h.Set("X-Prebid", "pbs-go/unknown") }, }, + { + description: "Success Response with header Observe-Browsing-Topics", + givenTestFile: "sample-requests/video/video_valid_sample.json", + givenHeader: map[string]string{secBrowsingTopics: "anyValue"}, + expectedStatus: 200, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Content-Type", "application/json") + h.Set("Observe-Browsing-Topics", "?1") + }, + }, + { + description: "Failure Response with header Observe-Browsing-Topics", + givenTestFile: "sample-requests/video/video_invalid_sample.json", + givenHeader: map[string]string{secBrowsingTopics: "anyValue"}, + expectedStatus: 500, + expectedHeaders: func(h http.Header) { + h.Set("X-Prebid", "pbs-go/unknown") + h.Set("Observe-Browsing-Topics", "?1") + }, + }, } exchange := &mockExchangeVideo{} @@ -1190,6 +1213,9 @@ func TestVideoAuctionResponseHeaders(t *testing.T) { requestBody := readVideoTestFile(t, test.givenTestFile) httpReq := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(requestBody)) + for k, v := range test.givenHeader { + httpReq.Header.Add(k, v) + } recorder := httptest.NewRecorder() endpoint.VideoAuctionEndpoint(recorder, httpReq, nil) @@ -1472,3 +1498,18 @@ func readVideoTestFile(t *testing.T, filename string) string { return string(getRequestPayload(t, requestData)) } + +func TestVideoRequestValidationFailed(t *testing.T) { + ex := &mockExchangeVideo{} + reqBody := readVideoTestFile(t, "sample-requests/video/video_invalid_sample_negative_tmax.json") + req := httptest.NewRequest("POST", "/openrtb2/video", strings.NewReader(reqBody)) + recorder := httptest.NewRecorder() + + deps := mockDeps(t, ex) + deps.VideoAuctionEndpoint(recorder, req, nil) + + errorMessage := recorder.Body.String() + + assert.Equal(t, 500, recorder.Code, "Should catch error in request") + assert.Equal(t, "Critical error while running the video endpoint: request.tmax must be nonnegative. Got -2", errorMessage, "Incorrect request validation message") +} diff --git a/errortypes/code.go b/errortypes/code.go index a30bb8e4bc0..a56df67c9c9 100644 --- a/errortypes/code.go +++ b/errortypes/code.go @@ -33,6 +33,7 @@ const ( FloorBidRejectionWarningCode InvalidBidResponseDSAWarningCode SecCookieDeprecationLenWarningCode + SecBrowsingTopicsWarningCode ) // Coder provides an error or warning code with severity. diff --git a/errortypes/errortypes.go b/errortypes/errortypes.go index d31c4166b06..7ca5668c290 100644 --- a/errortypes/errortypes.go +++ b/errortypes/errortypes.go @@ -251,3 +251,26 @@ func (err *FailedToMarshal) Code() int { func (err *FailedToMarshal) Severity() Severity { return SeverityFatal } + +// DebugWarning is a generic non-fatal error used in debug mode. Throughout the codebase, an error can +// only be a warning if it's of the type defined below +type DebugWarning struct { + Message string + WarningCode int +} + +func (err *DebugWarning) Error() string { + return err.Message +} + +func (err *DebugWarning) Code() int { + return err.WarningCode +} + +func (err *DebugWarning) Severity() Severity { + return SeverityWarning +} + +func (err *DebugWarning) Scope() Scope { + return ScopeDebug +} diff --git a/errortypes/scope.go b/errortypes/scope.go new file mode 100644 index 00000000000..b97284358f5 --- /dev/null +++ b/errortypes/scope.go @@ -0,0 +1,19 @@ +package errortypes + +type Scope int + +const ( + ScopeAny Scope = iota + ScopeDebug +) + +type Scoped interface { + Scope() Scope +} + +func ReadScope(err error) Scope { + if e, ok := err.(Scoped); ok { + return e.Scope() + } + return ScopeAny +} diff --git a/errortypes/scope_test.go b/errortypes/scope_test.go new file mode 100644 index 00000000000..7d90d5585fd --- /dev/null +++ b/errortypes/scope_test.go @@ -0,0 +1,37 @@ +package errortypes + +import ( + "errors" + "testing" +) + +func TestReadScope(t *testing.T) { + tests := []struct { + name string + err error + want Scope + }{ + { + name: "scope-debug", + err: &DebugWarning{Message: "scope is debug"}, + want: ScopeDebug, + }, + { + name: "scope-any", + err: &Warning{Message: "scope is any"}, + want: ScopeAny, + }, + { + name: "default-error", + err: errors.New("default error"), + want: ScopeAny, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ReadScope(tt.err); got != tt.want { + t.Errorf("ReadScope() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/exchange/exchange.go b/exchange/exchange.go index c73eea9f10c..e688b3e0a9b 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -488,6 +488,9 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog } for _, warning := range r.Warnings { + if errortypes.ReadScope(warning) == errortypes.ScopeDebug && !responseDebugAllow { + continue + } generalWarning := openrtb_ext.ExtBidderMessage{ Code: errortypes.ReadCode(warning), Message: warning.Error(), diff --git a/privacysandbox/topics.go b/privacysandbox/topics.go new file mode 100644 index 00000000000..4c129d0a535 --- /dev/null +++ b/privacysandbox/topics.go @@ -0,0 +1,228 @@ +package privacysandbox + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type Topic struct { + SegTax int `json:"segtax,omitempty"` + SegClass string `json:"segclass,omitempty"` + SegIDs []int `json:"segids,omitempty"` +} + +// ParseTopicsFromHeader parses the Sec-Browsing-Topics header data into Topics object +func ParseTopicsFromHeader(secBrowsingTopics string) ([]Topic, []error) { + topics := make([]Topic, 0, 10) + var warnings []error + + for _, field := range strings.Split(secBrowsingTopics, ",") { + field = strings.TrimSpace(field) + if field == "" || strings.HasPrefix(field, "();p=") { + continue + } + + if len(topics) < 10 { + if topic, ok := parseTopicSegment(field); ok { + topics = append(topics, topic) + } else { + warnings = append(warnings, formatWarning(field)) + } + } else { + warnings = append(warnings, formatWarning(field+" discarded due to limit reached.")) + } + } + + return topics, warnings +} + +// parseTopicSegment parses a single topic segment from the header into Topics object +func parseTopicSegment(field string) (Topic, bool) { + segment := strings.Split(field, ";") + if len(segment) != 2 { + return Topic{}, false + } + + segmentsIDs := strings.TrimSpace(segment[0]) + if len(segmentsIDs) < 3 || segmentsIDs[0] != '(' || segmentsIDs[len(segmentsIDs)-1] != ')' { + return Topic{}, false + } + + segtax, segclass := parseSegTaxSegClass(segment[1]) + if segtax == 0 || segclass == "" { + return Topic{}, false + } + + segIDs, err := parseSegmentIDs(segmentsIDs[1 : len(segmentsIDs)-1]) + if err != nil { + return Topic{}, false + } + + return Topic{ + SegTax: segtax, + SegClass: segclass, + SegIDs: segIDs, + }, true +} + +func parseSegTaxSegClass(seg string) (int, string) { + taxanomyModel := strings.Split(seg, ":") + if len(taxanomyModel) != 3 { + return 0, "" + } + + // taxanomyModel[0] is v=browser_version, we don't need it + taxanomyVer := strings.TrimSpace(taxanomyModel[1]) + taxanomy, err := strconv.Atoi(taxanomyVer) + if err != nil || taxanomy < 1 || taxanomy > 10 { + return 0, "" + } + + segtax := 600 + (taxanomy - 1) + segclass := strings.TrimSpace(taxanomyModel[2]) + return segtax, segclass +} + +// parseSegmentIDs parses the segment ids from the header string into int array +func parseSegmentIDs(segmentsIDs string) ([]int, error) { + var selectedSegmentIDs []int + for _, segmentID := range strings.Fields(segmentsIDs) { + segmentID = strings.TrimSpace(segmentID) + selectedSegmentID, err := strconv.Atoi(segmentID) + if err != nil || selectedSegmentID <= 0 { + return selectedSegmentIDs, errors.New("invalid segment id") + } + selectedSegmentIDs = append(selectedSegmentIDs, selectedSegmentID) + } + + return selectedSegmentIDs, nil +} + +func UpdateUserDataWithTopics(userData []openrtb2.Data, headerData []Topic, topicsDomain string) []openrtb2.Data { + if topicsDomain == "" { + return userData + } + + // headerDataMap groups segIDs by segtax and segclass for faster lookup and tracking of new segIDs yet to be added to user.data + // tracking is done by removing segIDs from segIDsMap once they are added to user.data, ensuring that headerDataMap will always have unique segtax-segclass-segIDs + // the only drawback of tracking via deleting segtax-segclass from headerDataMap is that this would not track duplicate entries within user.data which is fine because we are only merging header data with the provided user.data + headerDataMap := createHeaderDataMap(headerData) + + for i, data := range userData { + ext := &Topic{} + err := json.Unmarshal(data.Ext, ext) + if err != nil { + continue + } + + if ext.SegTax == 0 || ext.SegClass == "" { + continue + } + + if newSegIDs := findNewSegIDs(data.Name, topicsDomain, *ext, data.Segment, headerDataMap); newSegIDs != nil { + for _, segID := range newSegIDs { + userData[i].Segment = append(userData[i].Segment, openrtb2.Segment{ID: strconv.Itoa(segID)}) + } + + delete(headerDataMap[ext.SegTax], ext.SegClass) + } + } + + for segTax, segClassMap := range headerDataMap { + for segClass, segIDs := range segClassMap { + if len(segIDs) != 0 { + data := openrtb2.Data{ + Name: topicsDomain, + } + + var err error + data.Ext, err = jsonutil.Marshal(Topic{SegTax: segTax, SegClass: segClass}) + if err != nil { + continue + } + + for segID := range segIDs { + data.Segment = append(data.Segment, openrtb2.Segment{ + ID: strconv.Itoa(segID), + }) + } + + userData = append(userData, data) + } + } + } + + return userData +} + +// createHeaderDataMap creates a map of header data (segtax-segclass-segIDs) for faster lookup +// topicsdomain is not needed as we are only interested data from one domain configured in host config +func createHeaderDataMap(headerData []Topic) map[int]map[string]map[int]struct{} { + headerDataMap := make(map[int]map[string]map[int]struct{}) + + for _, topic := range headerData { + segClassMap, ok := headerDataMap[topic.SegTax] + if !ok { + segClassMap = make(map[string]map[int]struct{}) + headerDataMap[topic.SegTax] = segClassMap + } + + segIDsMap, ok := segClassMap[topic.SegClass] + if !ok { + segIDsMap = make(map[int]struct{}) + segClassMap[topic.SegClass] = segIDsMap + } + + for _, segID := range topic.SegIDs { + segIDsMap[segID] = struct{}{} + } + } + + return headerDataMap +} + +// findNewSegIDs merge unique segIDs in single user.data if request.user.data and header data match. i.e. segclass, segtax and topicsdomain match +func findNewSegIDs(dataName, topicsDomain string, userData Topic, userDataSegments []openrtb2.Segment, headerDataMap map[int]map[string]map[int]struct{}) []int { + if dataName != topicsDomain { + return nil + } + + segClassMap, exists := headerDataMap[userData.SegTax] + if !exists { + return nil + } + + segIDsMap, exists := segClassMap[userData.SegClass] + if !exists { + return nil + } + + // remove existing segIDs entries + for _, segID := range userDataSegments { + if id, err := strconv.Atoi(segID.ID); err == nil { + delete(segIDsMap, id) + } + } + + // collect remaining segIDs + segIDs := make([]int, 0, len(segIDsMap)) + for segID := range segIDsMap { + segIDs = append(segIDs, segID) + } + + return segIDs +} + +func formatWarning(msg string) error { + return &errortypes.DebugWarning{ + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + Message: fmt.Sprintf("Invalid field in Sec-Browsing-Topics header: %s", msg), + } +} diff --git a/privacysandbox/topics_test.go b/privacysandbox/topics_test.go new file mode 100644 index 00000000000..90a46f770c6 --- /dev/null +++ b/privacysandbox/topics_test.go @@ -0,0 +1,722 @@ +package privacysandbox + +import ( + "encoding/json" + "sort" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/errortypes" + "github.com/stretchr/testify/assert" +) + +func TestParseTopicsFromHeader(t *testing.T) { + type args struct { + secBrowsingTopics string + } + tests := []struct { + name string + args args + wantTopic []Topic + wantError []error + }{ + { + name: "empty header", + args: args{secBrowsingTopics: " "}, + wantTopic: []Topic{}, + wantError: nil, + }, + { + name: "invalid header value", + args: args{secBrowsingTopics: "some-sec-cookie-value"}, + wantTopic: []Topic{}, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: some-sec-cookie-value", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with only finish padding", + args: args{secBrowsingTopics: "();p=P0000000000000000000000000000000"}, + wantTopic: []Topic{}, + wantError: nil, + }, + { + name: "header with one valid field", + args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1}, + }, + }, + wantError: nil, + }, + { + name: "header without finish padding", + args: args{secBrowsingTopics: "(1);v=chrome.1:1:2"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1}, + }, + }, + wantError: nil, + }, + { + name: "header with more than 10 valid field, should return only 10", + args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (2);v=chrome.1:1:2, (3);v=chrome.1:1:2, (4);v=chrome.1:1:2, (5);v=chrome.1:1:2, (6);v=chrome.1:1:2, (7);v=chrome.1:1:2, (8);v=chrome.1:1:2, (9);v=chrome.1:1:2, (10);v=chrome.1:1:2, (11);v=chrome.1:1:2, (12);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{2}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{3}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{4}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{5}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{6}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{7}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{8}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{9}, + }, + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{10}, + }, + }, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (11);v=chrome.1:1:2 discarded due to limit reached.", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (12);v=chrome.1:1:2 discarded due to limit reached.", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with one valid field having multiple segIDs", + args: args{secBrowsingTopics: "(1 2);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1, 2}, + }, + }, + wantError: nil, + }, + { + name: "header with two valid fields having different taxonomies", + args: args{secBrowsingTopics: "(1);v=chrome.1:1:2, (1);v=chrome.1:2:2, ();p=P0000000000"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1}, + }, + { + SegTax: 601, + SegClass: "2", + SegIDs: []int{1}, + }, + }, + wantError: nil, + }, + { + name: "header with one valid field and another invalid field (w/o segIDs), should return only one valid field", + args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, ();v=chrome.1:2:3, ();p=P0000000000"}, + wantTopic: []Topic{ + { + SegTax: 601, + SegClass: "3", + SegIDs: []int{1}, + }, + }, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: ();v=chrome.1:2:3", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with two valid fields having different model version", + args: args{secBrowsingTopics: "(1);v=chrome.1:2:3, (2);v=chrome.1:2:3, ();p=P0000000000"}, + wantTopic: []Topic{ + { + SegTax: 601, + SegClass: "3", + SegIDs: []int{1}, + }, + { + SegTax: 601, + SegClass: "3", + SegIDs: []int{2}, + }, + }, + wantError: nil, + }, + { + name: "header with one valid fields and two invalid fields (one with taxanomy < 0 and another with taxanomy > 10), should return only one valid field", + args: args{secBrowsingTopics: "(1);v=chrome.1:11:2, (1);v=chrome.1:5:6, (1);v=chrome.1:0:2, ();p=P0000000000"}, + wantTopic: []Topic{ + { + SegTax: 604, + SegClass: "6", + SegIDs: []int{1}, + }, + }, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:11:2", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1:0:2", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with with valid fields having special characters (whitespaces, etc)", + args: args{secBrowsingTopics: "(1 2 4 6 7 4567 ) ; v=chrome.1: 1 : 2, (1);v=chrome.1, ();p=P0000000000"}, + wantTopic: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1, 2, 4, 6, 7, 4567}, + }, + }, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1);v=chrome.1", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with one valid field having a negative segId, drop field", + args: args{secBrowsingTopics: "(1 -3);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{}, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1 -3);v=chrome.1:1:2", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with one valid field having a segId=0, drop field", + args: args{secBrowsingTopics: "(1 0);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{}, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1 0);v=chrome.1:1:2", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + { + name: "header with one valid field having a segId value more than MaxInt, drop field", + args: args{secBrowsingTopics: "(1 9223372036854775808);v=chrome.1:1:2, ();p=P00000000000"}, + wantTopic: []Topic{}, + wantError: []error{ + &errortypes.DebugWarning{ + Message: "Invalid field in Sec-Browsing-Topics header: (1 9223372036854775808);v=chrome.1:1:2", + WarningCode: errortypes.SecBrowsingTopicsWarningCode, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTopic, gotError := ParseTopicsFromHeader(tt.args.secBrowsingTopics) + assert.Equal(t, tt.wantTopic, gotTopic) + assert.Equal(t, tt.wantError, gotError) + }) + } +} + +func TestUpdateUserDataWithTopics(t *testing.T) { + type args struct { + userData []openrtb2.Data + headerData []Topic + topicsDomain string + } + tests := []struct { + name string + args args + want []openrtb2.Data + }{ + { + name: "empty topics, empty user data, no change in user data", + args: args{ + userData: nil, + headerData: nil, + }, + want: nil, + }, + { + name: "empty topics, non-empty user data, no change in user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + }, + headerData: nil, + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + }, + }, + { + name: "topicsDomain empty, no change in user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1, 2}, + }, + }, + topicsDomain: "", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + }, + }, + { + name: "non-empty topics, empty user data, topics from header copied to user data", + args: args{ + userData: nil, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{1, 2}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, non-empty user data, topics from header copied to user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, user data with invalid data.ext field, topics from header copied to user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{`), + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "data1", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, user data with invalid topic details (invalid segtax and segclass), topics from header copied to user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{"segtax":0,"segclass":""}`), + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{"segtax":0,"segclass":""}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, user data with non matching topic details (different topicdomains, segtax and segclass), topics from header copied to user data", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + ID: "2", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "5"}, + {ID: "6"}, + }, + Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), + }, + { + ID: "3", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "7"}, + {ID: "8"}, + }, + Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`), + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{3, 4}, + }, + { + SegTax: 602, + SegClass: "2", + SegIDs: []int{3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "chrome.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + ID: "2", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "5"}, + {ID: "6"}, + }, + Ext: json.RawMessage(`{"segtax":601,"segclass":"3"}`), + }, + { + ID: "3", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "7"}, + {ID: "8"}, + }, + Ext: json.RawMessage(`{"segtax":602,"segclass":"4"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":602,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, user data with same topic details (matching segtax and segclass), topics from header merged with user data (filter unique segIDs)", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{2, 3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + { + name: "non-empty topics, user data with duplicate topic details (matching segtax and segclass and segIDs), topics from header merged with user data (filter unique segIDs), user.data will not be deduped", + args: args{ + userData: []openrtb2.Data{ + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + headerData: []Topic{ + { + SegTax: 600, + SegClass: "2", + SegIDs: []int{2, 3, 4}, + }, + }, + topicsDomain: "ads.pubmatic.com", + }, + want: []openrtb2.Data{ + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + {ID: "4"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + { + ID: "1", + Name: "ads.pubmatic.com", + Segment: []openrtb2.Segment{ + {ID: "1"}, + {ID: "2"}, + {ID: "3"}, + }, + Ext: json.RawMessage(`{"segtax":600,"segclass":"2"}`), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := UpdateUserDataWithTopics(tt.args.userData, tt.args.headerData, tt.args.topicsDomain) + sort.Slice(got, func(i, j int) bool { + if got[i].Name == got[j].Name { + return string(got[i].Ext) < string(got[j].Ext) + } + return got[i].Name < got[j].Name + }) + sort.Slice(tt.want, func(i, j int) bool { + if tt.want[i].Name == tt.want[j].Name { + return string(tt.want[i].Ext) < string(tt.want[j].Ext) + } + return tt.want[i].Name < tt.want[j].Name + }) + + for g := range got { + sort.Slice(got[g].Segment, func(i, j int) bool { + return got[g].Segment[i].ID < got[g].Segment[j].ID + }) + } + assert.Equal(t, tt.want, got, tt.name) + }) + } +} From a35a6687f25561857a21648e2423db6d2d6373f0 Mon Sep 17 00:00:00 2001 From: onetag-dev <38786435+onetag-dev@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:14:51 +0200 Subject: [PATCH 69/69] Onetag: add redirect userSync support (#3612) Co-authored-by: lorenzob --- static/bidder-info/onetag.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/bidder-info/onetag.yaml b/static/bidder-info/onetag.yaml index bc52faffe1b..00b835269d2 100644 --- a/static/bidder-info/onetag.yaml +++ b/static/bidder-info/onetag.yaml @@ -19,4 +19,7 @@ userSync: iframe: url: "https://onetag-sys.com/usync/?redir={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}" userMacro: "${USER_TOKEN}" -endpointCompression: "GZIP" \ No newline at end of file + redirect: + url: "https://onetag-sys.com/usync/?tag=img&redir={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}" + userMacro: "${USER_TOKEN}" +endpointCompression: "GZIP"