diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 2e4e552b9e6..03e3043bbbb 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -5,6 +5,11 @@ import (
"encoding/json"
"errors"
"fmt"
+ "io/ioutil"
+ "math/rand"
+ "net/http"
+ "strconv"
+
"github.com/PubMatic-OpenWrap/prebid-server/analytics"
"github.com/PubMatic-OpenWrap/prebid-server/config"
"github.com/PubMatic-OpenWrap/prebid-server/gdpr"
@@ -17,10 +22,6 @@ import (
"github.com/buger/jsonparser"
"github.com/golang/glog"
"github.com/julienschmidt/httprouter"
- "io/ioutil"
- "math/rand"
- "net/http"
- "strconv"
)
func NewCookieSyncEndpoint(
diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go
index d82c1e8e5d7..e93dac50118 100644
--- a/endpoints/events/vtrack.go
+++ b/endpoints/events/vtrack.go
@@ -3,6 +3,7 @@ package events
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"io/ioutil"
@@ -15,6 +16,7 @@ import (
"github.com/PubMatic-OpenWrap/prebid-server/analytics"
"github.com/PubMatic-OpenWrap/prebid-server/config"
"github.com/PubMatic-OpenWrap/prebid-server/errortypes"
+ "github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext"
"github.com/PubMatic-OpenWrap/prebid-server/prebid_cache_client"
"github.com/PubMatic-OpenWrap/prebid-server/stored_requests"
"github.com/golang/glog"
@@ -308,3 +310,197 @@ func ModifyVastXmlJSON(externalUrl string, data json.RawMessage, bidid, bidder,
}
return json.RawMessage(vast)
}
+
+//InjectVideoEventTrackers injects the video tracking events
+//Returns VAST xml contains as first argument. Second argument indicates whether the trackers are injected and last argument indicates if there is any error in injecting the trackers
+func InjectVideoEventTrackers(trackerURL, vastXML string, bid *openrtb.Bid, bidder, accountID string, timestamp int64, bidRequest *openrtb.BidRequest) ([]byte, bool, error) {
+ // parse VAST
+ doc := etree.NewDocument()
+ err := doc.ReadFromString(vastXML)
+ if nil != err {
+ err = fmt.Errorf("Error parsing VAST XML. '%v'", err.Error())
+ glog.Errorf(err.Error())
+ return []byte(vastXML), false, err // false indicates events trackers are not injected
+ }
+
+ //Maintaining BidRequest Impression Map (Copied from exchange.go#applyCategoryMapping)
+ //TODO: It should be optimized by forming once and reusing
+ impMap := make(map[string]*openrtb.Imp)
+ for i := range bidRequest.Imp {
+ impMap[bidRequest.Imp[i].ID] = &bidRequest.Imp[i]
+ }
+
+ eventURLMap := GetVideoEventTracking(trackerURL, bid, bidder, accountID, timestamp, bidRequest, doc, impMap)
+ trackersInjected := false
+ // return if if no tracking URL
+ if len(eventURLMap) == 0 {
+ return []byte(vastXML), false, errors.New("Event URLs are not found")
+ }
+
+ creatives := FindCreatives(doc)
+
+ if adm := strings.TrimSpace(bid.AdM); adm == "" || strings.HasPrefix(adm, "http") {
+ // determine which creative type to be created based on linearity
+ if imp, ok := impMap[bid.ImpID]; ok && nil != imp.Video {
+ // create creative object
+ creatives = doc.FindElements("VAST/Ad/Wrapper/Creatives")
+ // var creative *etree.Element
+ // if len(creatives) > 0 {
+ // creative = creatives[0] // consider only first creative
+ // } else {
+ creative := doc.CreateElement("Creative")
+ creatives[0].AddChild(creative)
+
+ // }
+
+ switch imp.Video.Linearity {
+ case openrtb.VideoLinearityLinearInStream:
+ creative.AddChild(doc.CreateElement("Linear"))
+ case openrtb.VideoLinearityNonLinearOverlay:
+ creative.AddChild(doc.CreateElement("NonLinearAds"))
+ default: // create both type of creatives
+ creative.AddChild(doc.CreateElement("Linear"))
+ creative.AddChild(doc.CreateElement("NonLinearAds"))
+ }
+ creatives = creative.ChildElements() // point to actual cratives
+ }
+ }
+ for _, creative := range creatives {
+ trackingEvents := creative.SelectElement("TrackingEvents")
+ if nil == trackingEvents {
+ trackingEvents = creative.CreateElement("TrackingEvents")
+ creative.AddChild(trackingEvents)
+ }
+ // Inject
+ for event, url := range eventURLMap {
+ trackingEle := trackingEvents.CreateElement("Tracking")
+ trackingEle.CreateAttr("event", event)
+ trackingEle.SetText(fmt.Sprintf("%s", url))
+ trackersInjected = true
+ }
+ }
+
+ out := []byte(vastXML)
+ var wErr error
+ if trackersInjected {
+ out, wErr = doc.WriteToBytes()
+ trackersInjected = trackersInjected && nil == wErr
+ if nil != wErr {
+ glog.Errorf("%v", wErr.Error())
+ }
+ }
+ return out, trackersInjected, wErr
+}
+
+// GetVideoEventTracking returns map containing key as event name value as associaed video event tracking URL
+// By default PBS will expect [EVENT_ID] macro in trackerURL to inject event information
+// [EVENT_ID] will be injected with one of the following values
+// firstQuartile, midpoint, thirdQuartile, complete
+// If your company can not use [EVENT_ID] and has its own macro. provide config.TrackerMacros implementation
+// and ensure that your macro is part of trackerURL configuration
+func GetVideoEventTracking(trackerURL string, bid *openrtb.Bid, bidder string, accountId string, timestamp int64, req *openrtb.BidRequest, doc *etree.Document, impMap map[string]*openrtb.Imp) map[string]string {
+ eventURLMap := make(map[string]string)
+ if "" == strings.TrimSpace(trackerURL) {
+ return eventURLMap
+ }
+
+ // lookup custom macros
+ var customMacroMap map[string]string
+ if nil != req.Ext {
+ reqExt := new(openrtb_ext.ExtRequest)
+ err := json.Unmarshal(req.Ext, &reqExt)
+ if err == nil {
+ customMacroMap = reqExt.Prebid.Macros
+ } else {
+ glog.Warningf("Error in unmarshling req.Ext.Prebid.Vast: [%s]", err.Error())
+ }
+ }
+
+ for _, event := range trackingEvents {
+ eventURL := trackerURL
+ // lookup in custom macros
+ if nil != customMacroMap {
+ for customMacro, value := range customMacroMap {
+ eventURL = replaceMacro(eventURL, customMacro, value)
+ }
+ }
+ // replace standard macros
+ eventURL = replaceMacro(eventURL, VASTAdTypeMacro, string(openrtb_ext.BidTypeVideo))
+ if nil != req && nil != req.App {
+ // eventURL = replaceMacro(eventURL, VASTAppBundleMacro, req.App.Bundle)
+ eventURL = replaceMacro(eventURL, VASTDomainMacro, req.App.Bundle)
+ if nil != req.App.Publisher {
+ eventURL = replaceMacro(eventURL, PBSAccountMacro, req.App.Publisher.ID)
+ }
+ }
+ if nil != req && nil != req.Site {
+ eventURL = replaceMacro(eventURL, VASTDomainMacro, req.Site.Domain)
+ eventURL = replaceMacro(eventURL, VASTPageURLMacro, req.Site.Page)
+ if nil != req.Site.Publisher {
+ eventURL = replaceMacro(eventURL, PBSAccountMacro, req.Site.Publisher.ID)
+ }
+ }
+
+ if len(bid.ADomain) > 0 {
+ //eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, strings.Join(bid.ADomain, ","))
+ domain, err := extractDomain(bid.ADomain[0])
+ if nil == err {
+ eventURL = replaceMacro(eventURL, PBSAdvertiserNameMacro, domain)
+ } else {
+ glog.Warningf("Unable to extract domain from '%s'. [%s]", bid.ADomain[0], err.Error())
+ }
+ }
+
+ eventURL = replaceMacro(eventURL, PBSBidderMacro, bidder)
+ eventURL = replaceMacro(eventURL, PBSBidIDMacro, bid.ID)
+ // replace [EVENT_ID] macro with PBS defined event ID
+ eventURL = replaceMacro(eventURL, PBSEventIDMacro, eventIDMap[event])
+
+ if imp, ok := impMap[bid.ImpID]; ok {
+ eventURL = replaceMacro(eventURL, PBSAdUnitIDMacro, imp.TagID)
+ }
+ eventURLMap[event] = eventURL
+ }
+ return eventURLMap
+}
+
+func replaceMacro(trackerURL, macro, value string) string {
+ macro = strings.TrimSpace(macro)
+ if strings.HasPrefix(macro, "[") && strings.HasSuffix(macro, "]") && len(strings.TrimSpace(value)) > 0 {
+ trackerURL = strings.ReplaceAll(trackerURL, macro, url.QueryEscape(value))
+ } else {
+ glog.Warningf("Invalid macro '%v'. Either empty or missing prefix '[' or suffix ']", macro)
+ }
+ return trackerURL
+}
+
+//FindCreatives finds Linear, NonLinearAds fro InLine and Wrapper Type of creatives
+//from input doc - VAST Document
+//NOTE: This function is temporarily seperated to reuse in ctv_auction.go. Because, in case of ctv
+//we generate bid.id
+func FindCreatives(doc *etree.Document) []*etree.Element {
+ // Find Creatives of Linear and NonLinear Type
+ // Injecting Tracking Events for Companion is not supported here
+ creatives := doc.FindElements("VAST/Ad/InLine/Creatives/Creative/Linear")
+ creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear")...)
+ creatives = append(creatives, doc.FindElements("VAST/Ad/InLine/Creatives/Creative/NonLinearAds")...)
+ creatives = append(creatives, doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/NonLinearAds")...)
+ return creatives
+}
+
+func extractDomain(rawURL string) (string, error) {
+ if !strings.HasPrefix(rawURL, "http") {
+ rawURL = "http://" + rawURL
+ }
+ // decode rawURL
+ rawURL, err := url.QueryUnescape(rawURL)
+ if nil != err {
+ return "", err
+ }
+ url, err := url.Parse(rawURL)
+ if nil != err {
+ return "", err
+ }
+ // remove www if present
+ return strings.TrimPrefix(url.Hostname(), "www."), nil
+}
diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go
index 1766f2e2e0d..5f42cfe8093 100644
--- a/endpoints/events/vtrack_test.go
+++ b/endpoints/events/vtrack_test.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net/http/httptest"
+ "net/url"
"strings"
"testing"
@@ -690,3 +691,560 @@ func getVTrackRequestData(wi bool, wic bool) (db []byte, e error) {
return data.Bytes(), e
}
+
+func TestInjectVideoEventTrackers(t *testing.T) {
+ type args struct {
+ externalURL string
+ bid *openrtb.Bid
+ req *openrtb.BidRequest
+ }
+ type want struct {
+ eventURLs map[string][]string
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "linear_creative",
+ args: args{
+ externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ bid: &openrtb.Bid{
+ AdM: `
+
+
+
+ http://example.com/tracking/midpoint
+ http://example.com/tracking/thirdQuartile
+ http://example.com/tracking/complete
+ http://partner.tracking.url
+
+
+ `,
+ },
+ req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ },
+ want: want{
+ eventURLs: map[string][]string{
+ // "firstQuartile": {"http://example.com/tracking/firstQuartile?k1=v1&k2=v2", "http://company.tracker.com?eventId=1004&appbundle=abc"},
+ // "midpoint": {"http://example.com/tracking/midpoint", "http://company.tracker.com?eventId=1003&appbundle=abc"},
+ // "thirdQuartile": {"http://example.com/tracking/thirdQuartile", "http://company.tracker.com?eventId=1005&appbundle=abc"},
+ // "complete": {"http://example.com/tracking/complete", "http://company.tracker.com?eventId=1006&appbundle=abc"},
+ "firstQuartile": {"http://example.com/tracking/firstQuartile?k1=v1&k2=v2", "http://company.tracker.com?eventId=4&appbundle=abc"},
+ "midpoint": {"http://example.com/tracking/midpoint", "http://company.tracker.com?eventId=3&appbundle=abc"},
+ "thirdQuartile": {"http://example.com/tracking/thirdQuartile", "http://company.tracker.com?eventId=5&appbundle=abc"},
+ "complete": {"http://example.com/tracking/complete", "http://company.tracker.com?eventId=6&appbundle=abc"},
+ "start": {"http://company.tracker.com?eventId=2&appbundle=abc", "http://partner.tracking.url"},
+ },
+ },
+ },
+ {
+ name: "non_linear_creative",
+ args: args{
+ externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ bid: &openrtb.Bid{ // Adm contains to TrackingEvents tag
+ AdM: `
+
+
+ http://something.com
+
+
+ `,
+ },
+ req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ },
+ want: want{
+ eventURLs: map[string][]string{
+ // "firstQuartile": {"http://something.com", "http://company.tracker.com?eventId=1004&appbundle=abc"},
+ // "midpoint": {"http://company.tracker.com?eventId=1003&appbundle=abc"},
+ // "thirdQuartile": {"http://company.tracker.com?eventId=1005&appbundle=abc"},
+ // "complete": {"http://company.tracker.com?eventId=1006&appbundle=abc"},
+ "firstQuartile": {"http://something.com", "http://company.tracker.com?eventId=4&appbundle=abc"},
+ "midpoint": {"http://company.tracker.com?eventId=3&appbundle=abc"},
+ "thirdQuartile": {"http://company.tracker.com?eventId=5&appbundle=abc"},
+ "complete": {"http://company.tracker.com?eventId=6&appbundle=abc"},
+ "start": {"http://company.tracker.com?eventId=2&appbundle=abc"},
+ },
+ },
+ }, {
+ name: "no_traker_url_configured", // expect no injection
+ args: args{
+ externalURL: "",
+ bid: &openrtb.Bid{ // Adm contains to TrackingEvents tag
+ AdM: `
+
+
+ `,
+ },
+ req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ },
+ want: want{
+ eventURLs: map[string][]string{},
+ },
+ },
+ {
+ name: "wrapper_vast_xml_from_partner", // expect we are injecting trackers inside wrapper
+ args: args{
+ externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ bid: &openrtb.Bid{ // Adm contains to TrackingEvents tag
+ AdM: `
+
+
+ iabtechlab
+ http://somevasturl
+
+
+
+
+
+ `,
+ },
+ req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ },
+ want: want{
+ eventURLs: map[string][]string{
+ // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"},
+ // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"},
+ // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"},
+ // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"},
+ "firstQuartile": {"http://company.tracker.com?eventId=4&appbundle=abc"},
+ "midpoint": {"http://company.tracker.com?eventId=3&appbundle=abc"},
+ "thirdQuartile": {"http://company.tracker.com?eventId=5&appbundle=abc"},
+ "complete": {"http://company.tracker.com?eventId=6&appbundle=abc"},
+ "start": {"http://company.tracker.com?eventId=2&appbundle=abc"},
+ },
+ },
+ },
+ // {
+ // name: "vast_tag_uri_response_from_partner",
+ // args: args{
+ // externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ // bid: &openrtb.Bid{ // Adm contains to TrackingEvents tag
+ // AdM: ``,
+ // },
+ // req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ // },
+ // want: want{
+ // eventURLs: map[string][]string{
+ // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"},
+ // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"},
+ // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"},
+ // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"},
+ // },
+ // },
+ // },
+ // {
+ // name: "adm_empty",
+ // args: args{
+ // externalURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ // bid: &openrtb.Bid{ // Adm contains to TrackingEvents tag
+ // AdM: "",
+ // NURL: "nurl_contents",
+ // },
+ // req: &openrtb.BidRequest{App: &openrtb.App{Bundle: "abc"}},
+ // },
+ // want: want{
+ // eventURLs: map[string][]string{
+ // "firstQuartile": {"http://company.tracker.com?eventId=firstQuartile&appbundle=abc"},
+ // "midpoint": {"http://company.tracker.com?eventId=midpoint&appbundle=abc"},
+ // "thirdQuartile": {"http://company.tracker.com?eventId=thirdQuartile&appbundle=abc"},
+ // "complete": {"http://company.tracker.com?eventId=complete&appbundle=abc"},
+ // },
+ // },
+ // },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ vast := ""
+ if nil != tc.args.bid {
+ vast = tc.args.bid.AdM // original vast
+ }
+ // bind this bid id with imp object
+ tc.args.req.Imp = []openrtb.Imp{{ID: "123", Video: &openrtb.Video{}}}
+ tc.args.bid.ImpID = tc.args.req.Imp[0].ID
+ accountID := ""
+ timestamp := int64(0)
+ biddername := "test_bidder"
+ injectedVast, injected, ierr := InjectVideoEventTrackers(tc.args.externalURL, vast, tc.args.bid, biddername, accountID, timestamp, tc.args.req)
+
+ if !injected {
+ // expect no change in input vast if tracking events are not injected
+ assert.Equal(t, vast, string(injectedVast))
+ assert.NotNil(t, ierr)
+ } else {
+ assert.Nil(t, ierr)
+ }
+ actualVastDoc := etree.NewDocument()
+
+ err := actualVastDoc.ReadFromBytes(injectedVast)
+ if nil != err {
+ assert.Fail(t, err.Error())
+ }
+
+ // fmt.Println(string(injectedVast))
+ actualTrackingEvents := actualVastDoc.FindElements("VAST/Ad/InLine/Creatives/Creative/Linear/TrackingEvents/Tracking")
+ actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/InLine/Creatives/Creative/NonLinearAds/TrackingEvents/Tracking")...)
+ actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear/TrackingEvents/Tracking")...)
+ actualTrackingEvents = append(actualTrackingEvents, actualVastDoc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/NonLinearAds/TrackingEvents/Tracking")...)
+
+ totalURLCount := 0
+ for event, URLs := range tc.want.eventURLs {
+
+ for _, expectedURL := range URLs {
+ present := false
+ for _, te := range actualTrackingEvents {
+ if te.SelectAttr("event").Value == event && te.Text() == expectedURL {
+ present = true
+ totalURLCount++
+ break // expected URL present. check for next expected URL
+ }
+ }
+ if !present {
+ assert.Fail(t, "Expected tracker URL '"+expectedURL+"' is not present")
+ }
+ }
+ }
+ // ensure all total of events are injected
+ assert.Equal(t, totalURLCount, len(actualTrackingEvents), fmt.Sprintf("Expected '%v' event trackers. But found '%v'", len(tc.want.eventURLs), len(actualTrackingEvents)))
+
+ })
+ }
+}
+
+func TestGetVideoEventTracking(t *testing.T) {
+ type args struct {
+ trackerURL string
+ bid *openrtb.Bid
+ bidder string
+ accountId string
+ timestamp int64
+ req *openrtb.BidRequest
+ doc *etree.Document
+ }
+ type want struct {
+ trackerURLMap map[string]string
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "valid_scenario",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ bid: &openrtb.Bid{
+ // AdM: vastXMLWith2Creatives,
+ },
+ req: &openrtb.BidRequest{
+ App: &openrtb.App{
+ Bundle: "someappbundle",
+ },
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=someappbundle",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=someappbundle",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=someappbundle",
+ // "complete": "http://company.tracker.com?eventId=complete&appbundle=someappbundle"},
+ "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=someappbundle",
+ "midpoint": "http://company.tracker.com?eventId=3&appbundle=someappbundle",
+ "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=someappbundle",
+ "start": "http://company.tracker.com?eventId=2&appbundle=someappbundle",
+ "complete": "http://company.tracker.com?eventId=6&appbundle=someappbundle"},
+ },
+ },
+ {
+ name: "no_macro_value", // expect no replacement
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ bid: &openrtb.Bid{},
+ req: &openrtb.BidRequest{
+ App: &openrtb.App{}, // no app bundle value
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=[DOMAIN]",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=[DOMAIN]",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=[DOMAIN]",
+ // "complete": "http://company.tracker.com?eventId=complete&appbundle=[DOMAIN]"},
+ "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=[DOMAIN]",
+ "midpoint": "http://company.tracker.com?eventId=3&appbundle=[DOMAIN]",
+ "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=[DOMAIN]",
+ "start": "http://company.tracker.com?eventId=2&appbundle=[DOMAIN]",
+ "complete": "http://company.tracker.com?eventId=6&appbundle=[DOMAIN]"},
+ },
+ },
+ {
+ name: "prefer_company_value_for_standard_macro",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]",
+ req: &openrtb.BidRequest{
+ App: &openrtb.App{
+ Bundle: "myapp", // do not expect this value
+ },
+ Imp: []openrtb.Imp{},
+ Ext: []byte(`{"prebid":{
+ "macros": {
+ "[DOMAIN]": "my_custom_value"
+ }
+ }}`),
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=my_custom_value",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=my_custom_value",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=my_custom_value",
+ // "complete": "http://company.tracker.com?eventId=complete&appbundle=my_custom_value"},
+ "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=my_custom_value",
+ "midpoint": "http://company.tracker.com?eventId=3&appbundle=my_custom_value",
+ "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=my_custom_value",
+ "start": "http://company.tracker.com?eventId=2&appbundle=my_custom_value",
+ "complete": "http://company.tracker.com?eventId=6&appbundle=my_custom_value"},
+ },
+ }, {
+ name: "multireplace_macro",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]&appbundle=[DOMAIN]¶meter2=[DOMAIN]",
+ req: &openrtb.BidRequest{
+ App: &openrtb.App{
+ Bundle: "myapp123",
+ },
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile&appbundle=myapp123¶meter2=myapp123",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint&appbundle=myapp123¶meter2=myapp123",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile&appbundle=myapp123¶meter2=myapp123",
+ // "complete": "http://company.tracker.com?eventId=complete&appbundle=myapp123¶meter2=myapp123"},
+ "firstQuartile": "http://company.tracker.com?eventId=4&appbundle=myapp123¶meter2=myapp123",
+ "midpoint": "http://company.tracker.com?eventId=3&appbundle=myapp123¶meter2=myapp123",
+ "thirdQuartile": "http://company.tracker.com?eventId=5&appbundle=myapp123¶meter2=myapp123",
+ "start": "http://company.tracker.com?eventId=2&appbundle=myapp123¶meter2=myapp123",
+ "complete": "http://company.tracker.com?eventId=6&appbundle=myapp123¶meter2=myapp123"},
+ },
+ },
+ {
+ name: "custom_macro_without_prefix_and_suffix",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]",
+ req: &openrtb.BidRequest{
+ Ext: []byte(`{"prebid":{
+ "macros": {
+ "CUSTOM_MACRO": "my_custom_value"
+ }
+ }}`),
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]",
+ // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"},
+ "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]",
+ "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]",
+ "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]",
+ "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]",
+ "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"},
+ },
+ },
+ {
+ name: "empty_macro",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]",
+ req: &openrtb.BidRequest{
+ Ext: []byte(`{"prebid":{
+ "macros": {
+ "": "my_custom_value"
+ }
+ }}`),
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]",
+ // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"},
+ "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]",
+ "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]",
+ "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]",
+ "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]",
+ "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"},
+ },
+ },
+ {
+ name: "macro_is_case_sensitive",
+ args: args{
+ trackerURL: "http://company.tracker.com?eventId=[EVENT_ID]¶m1=[CUSTOM_MACRO]",
+ req: &openrtb.BidRequest{
+ Ext: []byte(`{"prebid":{
+ "macros": {
+ "": "my_custom_value"
+ }
+ }}`),
+ Imp: []openrtb.Imp{},
+ },
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ // "firstQuartile": "http://company.tracker.com?eventId=firstQuartile¶m1=[CUSTOM_MACRO]",
+ // "midpoint": "http://company.tracker.com?eventId=midpoint¶m1=[CUSTOM_MACRO]",
+ // "thirdQuartile": "http://company.tracker.com?eventId=thirdQuartile¶m1=[CUSTOM_MACRO]",
+ // "complete": "http://company.tracker.com?eventId=complete¶m1=[CUSTOM_MACRO]"},
+ "firstQuartile": "http://company.tracker.com?eventId=4¶m1=[CUSTOM_MACRO]",
+ "midpoint": "http://company.tracker.com?eventId=3¶m1=[CUSTOM_MACRO]",
+ "thirdQuartile": "http://company.tracker.com?eventId=5¶m1=[CUSTOM_MACRO]",
+ "start": "http://company.tracker.com?eventId=2¶m1=[CUSTOM_MACRO]",
+ "complete": "http://company.tracker.com?eventId=6¶m1=[CUSTOM_MACRO]"},
+ },
+ },
+ {
+ name: "empty_tracker_url",
+ args: args{trackerURL: " ", req: &openrtb.BidRequest{Imp: []openrtb.Imp{}}},
+ want: want{trackerURLMap: make(map[string]string)},
+ },
+ {
+ name: "all_macros", // expect encoding for WRAPPER_IMPRESSION_ID macro
+ args: args{
+ trackerURL: "https://company.tracker.com?operId=8&e=[EVENT_ID]&p=[PBS-ACCOUNT]&pid=[PROFILE_ID]&v=[PROFILE_VERSION]&ts=[UNIX_TIMESTAMP]&pn=[PBS-BIDDER]&advertiser_id=[ADVERTISER_NAME]&sURL=[DOMAIN]&pfi=[PLATFORM]&af=[ADTYPE]&iid=[WRAPPER_IMPRESSION_ID]&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=[AD_UNIT]&bidid=[PBS-BIDID]",
+ req: &openrtb.BidRequest{
+ App: &openrtb.App{Bundle: "com.someapp.com", Publisher: &openrtb.Publisher{ID: "5890"}},
+ Ext: []byte(`{
+ "prebid": {
+ "macros": {
+ "[PROFILE_ID]": "100",
+ "[PROFILE_VERSION]": "2",
+ "[UNIX_TIMESTAMP]": "1234567890",
+ "[PLATFORM]": "7",
+ "[WRAPPER_IMPRESSION_ID]": "abc~!@#$%^&&*()_+{}|:\"<>?[]\\;',./"
+ }
+ }
+ }`),
+ Imp: []openrtb.Imp{
+ {TagID: "/testadunit/1", ID: "imp_1"},
+ },
+ },
+ bid: &openrtb.Bid{ADomain: []string{"http://a.com/32?k=v", "b.com"}, ImpID: "imp_1", ID: "test_bid_id"},
+ bidder: "test_bidder:234",
+ },
+ want: want{
+ trackerURLMap: map[string]string{
+ "firstQuartile": "https://company.tracker.com?operId=8&e=4&p=5890&pid=100&v=2&ts=1234567890&pn=test_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id",
+ "midpoint": "https://company.tracker.com?operId=8&e=3&p=5890&pid=100&v=2&ts=1234567890&pn=test_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id",
+ "thirdQuartile": "https://company.tracker.com?operId=8&e=5&p=5890&pid=100&v=2&ts=1234567890&pn=test_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id",
+ "complete": "https://company.tracker.com?operId=8&e=6&p=5890&pid=100&v=2&ts=1234567890&pn=test_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id",
+ "start": "https://company.tracker.com?operId=8&e=2&p=5890&pid=100&v=2&ts=1234567890&pn=test_bidder%3A234&advertiser_id=a.com&sURL=com.someapp.com&pfi=7&af=video&iid=abc~%21%40%23%24%25%5E%26%26%2A%28%29_%2B%7B%7D%7C%3A%22%3C%3E%3F%5B%5D%5C%3B%27%2C.%2F&pseq=[PODSEQUENCE]&adcnt=[ADCOUNT]&cb=[CACHEBUSTING]&au=%2Ftestadunit%2F1&bidid=test_bid_id"},
+ },
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+
+ if nil == tc.args.bid {
+ tc.args.bid = &openrtb.Bid{}
+ }
+
+ impMap := map[string]*openrtb.Imp{}
+
+ for _, imp := range tc.args.req.Imp {
+ impMap[imp.ID] = &imp
+ }
+
+ eventURLMap := GetVideoEventTracking(tc.args.trackerURL, tc.args.bid, tc.args.bidder, tc.args.accountId, tc.args.timestamp, tc.args.req, tc.args.doc, impMap)
+
+ for event, eurl := range tc.want.trackerURLMap {
+
+ u, _ := url.Parse(eurl)
+ expectedValues, _ := url.ParseQuery(u.RawQuery)
+ u, _ = url.Parse(eventURLMap[event])
+ actualValues, _ := url.ParseQuery(u.RawQuery)
+ for k, ev := range expectedValues {
+ av := actualValues[k]
+ for i := 0; i < len(ev); i++ {
+ assert.Equal(t, ev[i], av[i], fmt.Sprintf("Expected '%v' for '%v'. but found %v", ev[i], k, av[i]))
+ }
+ }
+
+ // error out if extra query params
+ if len(expectedValues) != len(actualValues) {
+ assert.Equal(t, expectedValues, actualValues, fmt.Sprintf("Expected '%v' query params but found '%v'", len(expectedValues), len(actualValues)))
+ break
+ }
+ }
+
+ // check if new quartile pixels are covered inside test
+ assert.Equal(t, tc.want.trackerURLMap, eventURLMap)
+ })
+ }
+}
+
+func TestReplaceMacro(t *testing.T) {
+ type args struct {
+ trackerURL string
+ macro string
+ value string
+ }
+ type want struct {
+ trackerURL string
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {name: "empty_tracker_url", args: args{trackerURL: "", macro: "[TEST]", value: "testme"}, want: want{trackerURL: ""}},
+ {name: "tracker_url_with_macro", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=testme"}},
+ {name: "tracker_url_with_invalid_macro", args: args{trackerURL: "http://something.com?test=TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=TEST]"}},
+ {name: "tracker_url_with_repeating_macro", args: args{trackerURL: "http://something.com?test=[TEST]&test1=[TEST]", macro: "[TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=testme&test1=testme"}},
+ {name: "empty_macro", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}},
+ {name: "macro_without_[", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "TEST]", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}},
+ {name: "macro_without_]", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST", value: "testme"}, want: want{trackerURL: "http://something.com?test=[TEST]"}},
+ {name: "empty_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: ""}, want: want{trackerURL: "http://something.com?test=[TEST]"}},
+ {name: "nested_macro_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "[TEST][TEST]"}, want: want{trackerURL: "http://something.com?test=%5BTEST%5D%5BTEST%5D"}},
+ {name: "url_as_macro_value", args: args{trackerURL: "http://something.com?test=[TEST]", macro: "[TEST]", value: "http://iamurl.com"}, want: want{trackerURL: "http://something.com?test=http%3A%2F%2Fiamurl.com"}},
+ {name: "macro_with_spaces", args: args{trackerURL: "http://something.com?test=[TEST]", macro: " [TEST] ", value: "http://iamurl.com"}, want: want{trackerURL: "http://something.com?test=http%3A%2F%2Fiamurl.com"}},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ trackerURL := replaceMacro(tc.args.trackerURL, tc.args.macro, tc.args.value)
+ assert.Equal(t, tc.want.trackerURL, trackerURL)
+ })
+ }
+
+}
+
+func TestExtractDomain(t *testing.T) {
+ testCases := []struct {
+ description string
+ url string
+ expectedDomain string
+ expectedErr error
+ }{
+ {description: "a.com", url: "a.com", expectedDomain: "a.com", expectedErr: nil},
+ {description: "a.com/123", url: "a.com/123", expectedDomain: "a.com", expectedErr: nil},
+ {description: "http://a.com/123", url: "http://a.com/123", expectedDomain: "a.com", expectedErr: nil},
+ {description: "https://a.com/123", url: "https://a.com/123", expectedDomain: "a.com", expectedErr: nil},
+ {description: "c.b.a.com", url: "c.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil},
+ {description: "url_encoded_http://c.b.a.com", url: "http%3A%2F%2Fc.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil},
+ {description: "url_encoded_with_www_http://c.b.a.com", url: "http%3A%2F%2Fwww.c.b.a.com", expectedDomain: "c.b.a.com", expectedErr: nil},
+ }
+ for _, test := range testCases {
+ t.Run(test.description, func(t *testing.T) {
+ domain, err := extractDomain(test.url)
+ assert.Equal(t, test.expectedDomain, domain)
+ assert.Equal(t, test.expectedErr, err)
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv/response/adpod_generator_test.go b/endpoints/openrtb2/ctv/response/adpod_generator_test.go
index ac18b316c85..932de32d6e0 100644
--- a/endpoints/openrtb2/ctv/response/adpod_generator_test.go
+++ b/endpoints/openrtb2/ctv/response/adpod_generator_test.go
@@ -1,12 +1,13 @@
package response
import (
+ "sort"
"testing"
- "github.com/stretchr/testify/assert"
-
"github.com/PubMatic-OpenWrap/openrtb"
+ "github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/constant"
"github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/types"
+ "github.com/stretchr/testify/assert"
)
func Test_findUniqueCombinations(t *testing.T) {
@@ -85,3 +86,311 @@ func Test_findUniqueCombinations(t *testing.T) {
})
}
}
+
+func TestAdPodGenerator_getMaxAdPodBid(t *testing.T) {
+ type fields struct {
+ request *openrtb.BidRequest
+ impIndex int
+ }
+ type args struct {
+ results []*highestCombination
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want *types.AdPodBid
+ }{
+ {
+ name: `EmptyResults`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: nil,
+ },
+ want: nil,
+ },
+ {
+ name: `AllBidsFiltered`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ filteredBids: map[string]*filteredBid{
+ `bid-1`: {bid: &types.Bid{Bid: &openrtb.Bid{ID: `bid-1`}}, reasonCode: constant.CTVRCCategoryExclusion},
+ `bid-2`: {bid: &types.Bid{Bid: &openrtb.Bid{ID: `bid-2`}}, reasonCode: constant.CTVRCCategoryExclusion},
+ `bid-3`: {bid: &types.Bid{Bid: &openrtb.Bid{ID: `bid-3`}}, reasonCode: constant.CTVRCCategoryExclusion},
+ },
+ },
+ },
+ },
+ want: nil,
+ },
+ {
+ name: `SingleResponse`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-1`}},
+ {Bid: &openrtb.Bid{ID: `bid-2`}},
+ {Bid: &openrtb.Bid{ID: `bid-3`}},
+ },
+ bidIDs: []string{`bid-1`, `bid-2`, `bid-3`},
+ price: 20,
+ nDealBids: 0,
+ categoryScore: map[string]int{
+ `cat-1`: 1,
+ `cat-2`: 1,
+ },
+ domainScore: map[string]int{
+ `domain-1`: 1,
+ `domain-2`: 1,
+ },
+ filteredBids: map[string]*filteredBid{
+ `bid-4`: {bid: &types.Bid{Bid: &openrtb.Bid{ID: `bid-4`}}, reasonCode: constant.CTVRCCategoryExclusion},
+ },
+ },
+ },
+ },
+ want: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-1`}},
+ {Bid: &openrtb.Bid{ID: `bid-2`}},
+ {Bid: &openrtb.Bid{ID: `bid-3`}},
+ },
+ Cat: []string{`cat-1`, `cat-2`},
+ ADomain: []string{`domain-1`, `domain-2`},
+ Price: 20,
+ },
+ },
+ {
+ name: `MultiResponse-AllNonDealBids`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-11`}},
+ },
+ bidIDs: []string{`bid-11`},
+ price: 10,
+ nDealBids: 0,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ bidIDs: []string{`bid-21`},
+ price: 20,
+ nDealBids: 0,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ bidIDs: []string{`bid-31`},
+ price: 10,
+ nDealBids: 0,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-41`}},
+ },
+ bidIDs: []string{`bid-41`},
+ price: 15,
+ nDealBids: 0,
+ },
+ },
+ },
+ want: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ Cat: []string{},
+ ADomain: []string{},
+ Price: 20,
+ },
+ },
+ {
+ name: `MultiResponse-AllDealBids-SameCount`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-11`}},
+ },
+ bidIDs: []string{`bid-11`},
+ price: 10,
+ nDealBids: 1,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ bidIDs: []string{`bid-21`},
+ price: 20,
+ nDealBids: 1,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ bidIDs: []string{`bid-31`},
+ price: 10,
+ nDealBids: 1,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-41`}},
+ },
+ bidIDs: []string{`bid-41`},
+ price: 15,
+ nDealBids: 1,
+ },
+ },
+ },
+ want: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ Cat: []string{},
+ ADomain: []string{},
+ Price: 20,
+ },
+ },
+ {
+ name: `MultiResponse-AllDealBids-DifferentCount`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-11`}},
+ },
+ bidIDs: []string{`bid-11`},
+ price: 10,
+ nDealBids: 2,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ bidIDs: []string{`bid-21`},
+ price: 20,
+ nDealBids: 1,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ bidIDs: []string{`bid-31`},
+ price: 10,
+ nDealBids: 3,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-41`}},
+ },
+ bidIDs: []string{`bid-41`},
+ price: 15,
+ nDealBids: 2,
+ },
+ },
+ },
+ want: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ Cat: []string{},
+ ADomain: []string{},
+ Price: 10,
+ },
+ },
+ {
+ name: `MultiResponse-Mixed-DealandNonDealBids`,
+ fields: fields{
+ request: &openrtb.BidRequest{ID: `req-1`, Imp: []openrtb.Imp{{ID: `imp-1`}}},
+ impIndex: 0,
+ },
+ args: args{
+ results: []*highestCombination{
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-11`}},
+ },
+ bidIDs: []string{`bid-11`},
+ price: 10,
+ nDealBids: 2,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-21`}},
+ },
+ bidIDs: []string{`bid-21`},
+ price: 20,
+ nDealBids: 0,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ bidIDs: []string{`bid-31`},
+ price: 10,
+ nDealBids: 3,
+ },
+ {
+ bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-41`}},
+ },
+ bidIDs: []string{`bid-41`},
+ price: 15,
+ nDealBids: 0,
+ },
+ },
+ },
+ want: &types.AdPodBid{
+ Bids: []*types.Bid{
+ {Bid: &openrtb.Bid{ID: `bid-31`}},
+ },
+ Cat: []string{},
+ ADomain: []string{},
+ Price: 10,
+ },
+ },
+ // TODO: Add test cases.
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ o := &AdPodGenerator{
+ request: tt.fields.request,
+ impIndex: tt.fields.impIndex,
+ }
+ got := o.getMaxAdPodBid(tt.args.results)
+ if nil != got {
+ sort.Strings(got.ADomain)
+ sort.Strings(got.Cat)
+ }
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
diff --git a/endpoints/openrtb2/ctv_auction.go b/endpoints/openrtb2/ctv_auction.go
index 670bb4c438a..4e9d5b46438 100644
--- a/endpoints/openrtb2/ctv_auction.go
+++ b/endpoints/openrtb2/ctv_auction.go
@@ -7,6 +7,7 @@ import (
"fmt"
"math"
"net/http"
+ "net/url"
"sort"
"strconv"
"strings"
@@ -17,6 +18,7 @@ import (
accountService "github.com/PubMatic-OpenWrap/prebid-server/account"
"github.com/PubMatic-OpenWrap/prebid-server/analytics"
"github.com/PubMatic-OpenWrap/prebid-server/config"
+ "github.com/PubMatic-OpenWrap/prebid-server/endpoints/events"
"github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/combination"
"github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/constant"
"github.com/PubMatic-OpenWrap/prebid-server/endpoints/openrtb2/ctv/impressions"
@@ -828,6 +830,15 @@ func getAdPodBidCreative(video *openrtb.Video, adpod *types.AdPodBid) *string {
continue
}
+ // adjust bidid in video event trackers and update
+ adjustBidIDInVideoEventTrackers(adDoc, bid.Bid)
+ adm, err := adDoc.WriteToString()
+ if nil != err {
+ util.JLogf("ERROR, %v", err.Error())
+ } else {
+ bid.AdM = adm
+ }
+
vastTag := adDoc.SelectElement(constant.VASTElement)
//Get Actual VAST Version
@@ -853,6 +864,7 @@ func getAdPodBidCreative(video *openrtb.Video, adpod *types.AdPodBid) *string {
}
vast.CreateAttr(constant.VASTVersionAttribute, constant.VASTVersionsStr[int(version)])
+
bidAdM, err := doc.WriteToString()
if nil != err {
fmt.Printf("ERROR, %v", err.Error())
@@ -907,3 +919,38 @@ func addTargetingKey(bid *openrtb.Bid, key openrtb_ext.TargetingKey, value strin
}
return err
}
+
+func adjustBidIDInVideoEventTrackers(doc *etree.Document, bid *openrtb.Bid) {
+ // adjusment: update bid.id with ctv module generated bid.id
+ creatives := events.FindCreatives(doc)
+ for _, creative := range creatives {
+ trackingEvents := creative.FindElements("TrackingEvents/Tracking")
+ if nil != trackingEvents {
+ // update bidid= value with ctv generated bid id for this bid
+ for _, trackingEvent := range trackingEvents {
+ u, e := url.Parse(trackingEvent.Text())
+ if nil == e {
+ values, e := url.ParseQuery(u.RawQuery)
+ // only do replacment if operId=8
+ if nil == e && nil != values["bidid"] && nil != values["operId"] && values["operId"][0] == "8" {
+ values.Set("bidid", bid.ID)
+ } else {
+ continue
+ }
+
+ //OTT-183: Fix
+ if nil != values["operId"] && values["operId"][0] == "8" {
+ operID := values.Get("operId")
+ values.Del("operId")
+ values.Add("_operId", operID) // _ (underscore) will keep it as first key
+ }
+
+ u.RawQuery = values.Encode() // encode sorts query params by key. _ must be first (assuing no other query param with _)
+ // replace _operId with operId
+ u.RawQuery = strings.ReplaceAll(u.RawQuery, "_operId", "operId")
+ trackingEvent.SetText(u.String())
+ }
+ }
+ }
+ }
+}
diff --git a/endpoints/openrtb2/ctv_auction_test.go b/endpoints/openrtb2/ctv_auction_test.go
index 1e6a0907c72..db3438accfb 100644
--- a/endpoints/openrtb2/ctv_auction_test.go
+++ b/endpoints/openrtb2/ctv_auction_test.go
@@ -2,8 +2,12 @@ package openrtb2
import (
"encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
"testing"
+ "github.com/PubMatic-OpenWrap/etree"
"github.com/PubMatic-OpenWrap/openrtb"
"github.com/PubMatic-OpenWrap/prebid-server/openrtb_ext"
"github.com/stretchr/testify/assert"
@@ -56,3 +60,92 @@ func TestAddTargetingKeys(t *testing.T) {
}
assert.Equal(t, "Invalid bid", addTargetingKey(nil, openrtb_ext.HbCategoryDurationKey, "some value").Error())
}
+
+func TestAdjustBidIDInVideoEventTrackers(t *testing.T) {
+ type args struct {
+ modifiedBid *openrtb.Bid
+ }
+ type want struct {
+ eventURLMap map[string]string
+ }
+
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "replace_with_custom_ctv_bid_id",
+ want: want{
+ eventURLMap: map[string]string{
+ "thirdQuartile": "https://thirdQuartile.com?operId=8&key1=value1&bidid=1-bid_123",
+ "complete": "https://complete.com?operId=8&key1=value1&bidid=1-bid_123&key2=value2",
+ "firstQuartile": "https://firstQuartile.com?operId=8&key1=value1&bidid=1-bid_123&key2=value2",
+ "midpoint": "https://midpoint.com?operId=8&key1=value1&bidid=1-bid_123&key2=value2",
+ "someevent": "https://othermacros?bidid=bid_123&abc=pqr",
+ },
+ },
+ args: args{
+ modifiedBid: &openrtb.Bid{
+ ID: "1-bid_123",
+ AdM: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ doc := etree.NewDocument()
+ doc.ReadFromString(test.args.modifiedBid.AdM)
+ adjustBidIDInVideoEventTrackers(doc, test.args.modifiedBid)
+ events := doc.FindElements("VAST/Ad/Wrapper/Creatives/Creative/Linear/TrackingEvents/Tracking")
+ for _, event := range events {
+ evntName := event.SelectAttr("event").Value
+ expectedURL, _ := url.Parse(test.want.eventURLMap[evntName])
+ expectedValues := expectedURL.Query()
+ actualURL, _ := url.Parse(event.Text())
+ actualValues := actualURL.Query()
+ for k, ev := range expectedValues {
+ av := actualValues[k]
+ for i := 0; i < len(ev); i++ {
+ assert.Equal(t, ev[i], av[i], fmt.Sprintf("Expected '%v' for '%v' [Event = %v]. but found %v", ev[i], k, evntName, av[i]))
+ }
+ }
+
+ // check if operId=8 is first param
+ if evntName != "someevent" {
+ assert.True(t, strings.HasPrefix(actualURL.RawQuery, "operId=8"), "operId=8 must be first query param")
+ }
+ }
+ }
+}
diff --git a/exchange/auction_test.go b/exchange/auction_test.go
index ee064fcb6f1..1a54e25f2cb 100644
--- a/exchange/auction_test.go
+++ b/exchange/auction_test.go
@@ -42,6 +42,20 @@ func TestMakeVASTNurl(t *testing.T) {
assert.Equal(t, expect, vast)
}
+func TestMakeVASTAdmContainsURI(t *testing.T) {
+ const url = "http://myvast.com/1.xml"
+ const expect = `` +
+ `prebid.org wrapper` +
+ `` +
+ `` +
+ ``
+ bid := &openrtb.Bid{
+ AdM: url,
+ }
+ vast := makeVAST(bid)
+ assert.Equal(t, expect, vast)
+}
+
func TestBuildCacheString(t *testing.T) {
testCases := []struct {
description string
diff --git a/go.mod b/go.mod
index 4ecff51bfd4..b3ba4d5b384 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
github.com/DATA-DOG/go-sqlmock v1.3.0
github.com/NYTimes/gziphandler v1.1.1
github.com/OneOfOne/xxhash v1.2.5 // indirect
- github.com/PubMatic-OpenWrap/etree v1.0.1
+ github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4
github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
diff --git a/go.sum b/go.sum
index 6f09434f477..58f9c772b72 100644
--- a/go.sum
+++ b/go.sum
@@ -8,10 +8,16 @@ github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/PubMatic-OpenWrap/etree v1.0.1 h1:Q8sZ99MuXKmAx2v4XThKjwlstgadZffiRbNwUG0Ey1U=
github.com/PubMatic-OpenWrap/etree v1.0.1/go.mod h1:5Y8qgcuDoy3XXG907UXkGnVTwihF16rXyJa4zRT7hOE=
+github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4 h1:EhiijwjoKTx7FVP8p2wwC/z4n5l4c8l2CGmsrFv2uhI=
+github.com/PubMatic-OpenWrap/etree v1.0.2-0.20210129100623-8f30cfecf9f4/go.mod h1:5Y8qgcuDoy3XXG907UXkGnVTwihF16rXyJa4zRT7hOE=
github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible h1:BGwndVLu0ncwweHnofXzLo+SnRMe04Bq3KFfELLzif4=
github.com/PubMatic-OpenWrap/openrtb v11.0.1-0.20200228131822-5216ebe65c0c+incompatible/go.mod h1:Ply/+GFe6FLkPMLV8Yh8xW0MpqclQyVf7m4PRsnaLDY=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/beevik/etree v1.1.1-0.20200718192613-4a2f8b9d084c h1:uYq6BD31fkfeNKQmfLj7ODcEfkb5JLsKrXVSqgnfGg8=
+github.com/beevik/etree v1.1.1-0.20200718192613-4a2f8b9d084c/go.mod h1:0yGO2rna3S9DkITDWHY1bMtcY4IJ4w+4S+EooZUR0bE=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=