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=