diff --git a/injector/injector.go b/injector/injector.go new file mode 100644 index 00000000000..8f902273257 --- /dev/null +++ b/injector/injector.go @@ -0,0 +1,401 @@ +package injector + +import ( + "encoding/xml" + "fmt" + "io" + "strings" + + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/metrics" +) + +const ( + emptyAdmResponse = `prebid.org wrapper` +) + +const ( + companionStartTag = "" + companionEndTag = "" + nonLinearStartTag = "" + nonLinearEndTag = "" + videoClickStartTag = "" + videoClickEndTag = "" + trackingEventStartTag = "" + trackingEventEndTag = "" + clickTrackingStartTag = "" + impressionStartTag = "" + errorStartTag = "" + nonLinearClickTrackingStartTag = "" + companionClickThroughStartTag = "" + tracking = "tracking" + companionclickthrough = "companionclickthrough" + nonlinearclicktracking = "nonlinearclicktracking" + impression = "impression" + err = "error" + clicktracking = "clicktracking" + adId = "adid" + trackingStartTag = `" +) + +const ( + inlineCase = "InLine" + wrapperCase = "Wrapper" + creativeCase = "Creative" + linearCase = "Linear" + nonLinearCase = "NonLinear" + videoClicksCase = "VideoClicks" + nonLinearAdsCase = "NonLinearAds" + trackingEventsCase = "TrackingEvents" + impressionCase = "Impression" + errorCase = "Error" + companionCase = "Companion" + companionAdsCase = "CompanionAds" +) + +type Injector interface { + InjectTracker(vastXML string, NURL string) string +} + +type VASTEvents struct { + Errors []string + Impressions []string + VideoClicks []string + NonLinearClickTracking []string + CompanionClickThrough []string + TrackingEvents map[string][]string +} + +type InjectionState struct { + injectTracker bool + injectVideoClicks bool + inlineWrapperTagFound bool + wrapperTagFound bool + impressionTagFound bool + errorTagFound bool + creativeId string + isCreative bool + companionTagFound bool + nonLinearTagFound bool +} + +type TrackerInjector struct { + replacer macros.Replacer + events VASTEvents + me metrics.MetricsEngine + provider *macros.MacroProvider +} + +var trimRunes = "\t\r\b\n " + +func NewTrackerInjector(replacer macros.Replacer, provider *macros.MacroProvider, events VASTEvents) *TrackerInjector { + return &TrackerInjector{ + replacer: replacer, + provider: provider, + events: events, + } +} + +func (trackerinjector *TrackerInjector) InjectTracker(vastXML string, NURL string) (string, error) { + if vastXML == "" && NURL == "" { + // TODO Log a adapter..requests.badserverresponse + return vastXML, fmt.Errorf("invalid Vast XML") + } + + if vastXML == "" { + return fmt.Sprintf(emptyAdmResponse, NURL), nil + } + + var outputXML strings.Builder + encoder := xml.NewEncoder(&outputXML) + state := &InjectionState{ + injectTracker: false, + injectVideoClicks: false, + inlineWrapperTagFound: false, + wrapperTagFound: false, + impressionTagFound: false, + errorTagFound: false, + creativeId: "", + isCreative: false, + companionTagFound: false, + nonLinearTagFound: false, + } + + reader := strings.NewReader(vastXML) + decoder := xml.NewDecoder(reader) + + for { + rawToken, err := decoder.RawToken() + if err != nil { + if err == io.EOF { + break + } else { + return "", fmt.Errorf("XML processing error: %w", err) + } + } + + switch token := rawToken.(type) { + case xml.StartElement: + err = trackerinjector.handleStartElement(token, state, &outputXML, encoder) + case xml.EndElement: + err = trackerinjector.handleEndElement(token, state, &outputXML, encoder) + case xml.CharData: + charData := strings.Trim(string(token), trimRunes) + if len(charData) != 0 { + err = encoder.Flush() + outputXML.WriteString("") + } + default: + err = encoder.EncodeToken(rawToken) + } + + if err != nil { + return "", fmt.Errorf("XML processing error: %w", err) + } + } + + if err := encoder.Flush(); err != nil { + return "", fmt.Errorf("XML processing error: %w", err) + } + + if !state.inlineWrapperTagFound { + // Todo log adapter..requests.badserverresponse metrics + return vastXML, fmt.Errorf("invalid VastXML, inline/wrapper tag not found") + } + return outputXML.String(), nil +} + +func (trackerinjector *TrackerInjector) handleStartElement(token xml.StartElement, state *InjectionState, outputXML *strings.Builder, encoder *xml.Encoder) error { + var err error + switch token.Name.Local { + case wrapperCase: + state.wrapperTagFound = true + if err = encoder.EncodeToken(token); err != nil { + return err + } + case creativeCase: + state.isCreative = true + for _, attr := range token.Attr { + if strings.ToLower(attr.Name.Local) == adId { + state.creativeId = attr.Value + } + } + if err = encoder.EncodeToken(token); err != nil { + return err + } + case linearCase: + state.injectVideoClicks = true + state.injectTracker = true + if err = encoder.EncodeToken(token); err != nil { + return err + } + case videoClicksCase: + state.injectVideoClicks = false + if err = encoder.EncodeToken(token); err != nil { + return err + } + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addClickTrackingEvent(outputXML, state.creativeId, false) + case nonLinearAdsCase: + state.injectTracker = true + if err = encoder.EncodeToken(token); err != nil { + return err + } + case trackingEventsCase: + if state.isCreative { + state.injectTracker = false + if err = encoder.EncodeToken(token); err != nil { + return err + } + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addTrackingEvent(outputXML, state.creativeId, false) + } + default: + if err = encoder.EncodeToken(token); err != nil { + return err + } + } + return nil +} + +func (trackerinjector *TrackerInjector) handleEndElement(token xml.EndElement, state *InjectionState, outputXML *strings.Builder, encoder *xml.Encoder) error { + var err error + switch token.Name.Local { + case impressionCase: + if err = encoder.EncodeToken(token); err != nil { + return err + } + if err = encoder.Flush(); err != nil { + return err + } + if !state.impressionTagFound { + trackerinjector.addImpressionTrackingEvent(outputXML) + state.impressionTagFound = true + } + case errorCase: + if err = encoder.EncodeToken(token); err != nil { + return err + } + if err = encoder.Flush(); err != nil { + return err + } + if !state.errorTagFound { + trackerinjector.addErrorTrackingEvent(outputXML) + state.errorTagFound = true + } + case nonLinearAdsCase: + if state.injectTracker { + state.injectTracker = false + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addTrackingEvent(outputXML, state.creativeId, true) + if !state.nonLinearTagFound && state.wrapperTagFound { + trackerinjector.addNonLinearClickTrackingEvent(outputXML, state.creativeId, true) + } + if err = encoder.EncodeToken(token); err != nil { + return err + } + } + case linearCase: + if state.injectVideoClicks { + state.injectVideoClicks = false + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addClickTrackingEvent(outputXML, state.creativeId, true) + } + if state.injectTracker { + state.injectTracker = false + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addTrackingEvent(outputXML, state.creativeId, true) + } + encoder.EncodeToken(token) + case inlineCase, wrapperCase: + state.wrapperTagFound = false + state.inlineWrapperTagFound = true + if err = encoder.Flush(); err != nil { + return err + } + if !state.impressionTagFound { + trackerinjector.addImpressionTrackingEvent(outputXML) + } + state.impressionTagFound = false + if !state.errorTagFound { + trackerinjector.addErrorTrackingEvent(outputXML) + } + state.errorTagFound = false + if err = encoder.EncodeToken(token); err != nil { + return err + } + case nonLinearCase: + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addNonLinearClickTrackingEvent(outputXML, state.creativeId, false) + state.nonLinearTagFound = true + if err = encoder.EncodeToken(token); err != nil { + return err + } + case companionCase: + state.companionTagFound = true + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addCompanionClickThroughEvent(outputXML, state.creativeId, false) + if err = encoder.EncodeToken(token); err != nil { + return err + } + case creativeCase: + state.isCreative = false + if err = encoder.EncodeToken(token); err != nil { + return err + } + case companionAdsCase: + if !state.companionTagFound && state.wrapperTagFound { + if err = encoder.Flush(); err != nil { + return err + } + trackerinjector.addCompanionClickThroughEvent(outputXML, state.creativeId, true) + } + if err = encoder.EncodeToken(token); err != nil { + return err + } + default: + if err = encoder.EncodeToken(token); err != nil { + return err + } + } + return nil +} + +func (trackerinjector *TrackerInjector) addTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { + if addParentTag { + outputXML.WriteString(trackingEventStartTag) + } + for typ, urls := range trackerinjector.events.TrackingEvents { + trackerinjector.writeTrackingEvent(urls, outputXML, fmt.Sprintf(trackingStartTag, typ), trackingEndTag, creativeId, typ, tracking) + } + if addParentTag { + outputXML.WriteString(trackingEventEndTag) + } +} + +func (trackerinjector *TrackerInjector) addClickTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { + if addParentTag { + outputXML.WriteString(videoClickStartTag) + } + trackerinjector.writeTrackingEvent(trackerinjector.events.VideoClicks, outputXML, clickTrackingStartTag, clickTrackingEndTag, creativeId, "", clicktracking) + if addParentTag { + outputXML.WriteString(videoClickEndTag) + } +} + +func (trackerinjector *TrackerInjector) addImpressionTrackingEvent(outputXML *strings.Builder) { + trackerinjector.writeTrackingEvent(trackerinjector.events.Impressions, outputXML, impressionStartTag, impressionEndTag, "", "", impression) +} + +func (trackerinjector *TrackerInjector) addErrorTrackingEvent(outputXML *strings.Builder) { + trackerinjector.writeTrackingEvent(trackerinjector.events.Errors, outputXML, errorStartTag, errorEndTag, "", "", err) +} + +func (trackerinjector *TrackerInjector) addNonLinearClickTrackingEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { + if addParentTag { + outputXML.WriteString(nonLinearStartTag) + } + trackerinjector.writeTrackingEvent(trackerinjector.events.NonLinearClickTracking, outputXML, nonLinearClickTrackingStartTag, nonLinearClickTrackingEndTag, creativeId, "", nonlinearclicktracking) + if addParentTag { + outputXML.WriteString(nonLinearEndTag) + } +} + +func (trackerinjector *TrackerInjector) addCompanionClickThroughEvent(outputXML *strings.Builder, creativeId string, addParentTag bool) { + if addParentTag { + outputXML.WriteString(companionStartTag) + } + trackerinjector.writeTrackingEvent(trackerinjector.events.CompanionClickThrough, outputXML, companionClickThroughStartTag, companionClickThroughEndTag, creativeId, "", companionclickthrough) + if addParentTag { + outputXML.WriteString(companionEndTag) + } +} + +func (trackerinjector *TrackerInjector) writeTrackingEvent(urls []string, outputXML *strings.Builder, startTag, endTag, creativeId, eventType, vastEvent string) { + trackerinjector.provider.PopulateEventMacros(creativeId, eventType, vastEvent) + for _, url := range urls { + outputXML.WriteString(startTag) + trackerinjector.replacer.Replace(outputXML, url, trackerinjector.provider) + outputXML.WriteString(endTag) + } +} diff --git a/injector/injector_test.go b/injector/injector_test.go new file mode 100644 index 00000000000..2c9dceba154 --- /dev/null +++ b/injector/injector_test.go @@ -0,0 +1,455 @@ +package injector + +import ( + "errors" + "strings" + "testing" + + "github.com/prebid/openrtb/v20/openrtb2" + "github.com/prebid/prebid-server/v2/exchange/entities" + "github.com/prebid/prebid-server/v2/macros" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/ptrutil" + "github.com/stretchr/testify/assert" +) + +var reqWrapper = &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "123", + Site: &openrtb2.Site{ + Domain: "testdomain", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + Page: "pageurltest", + }, + App: &openrtb2.App{ + Domain: "testdomain", + Bundle: "testbundle", + Publisher: &openrtb2.Publisher{ + Domain: "publishertestdomain", + ID: "testpublisherID", + }, + }, + Device: &openrtb2.Device{ + Lmt: ptrutil.ToPtr(int8(1)), + }, + User: &openrtb2.User{Ext: []byte(`{"consent":"1" }`)}, + Ext: []byte(`{"prebid":{"channel": {"name":"test1"},"macros":{"CUSTOMMACR1":"value1"}}}`), + }, +} + +func TestInjectTracker(t *testing.T) { + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Errors: []string{"http://errortracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + Impressions: []string{"http://impressiontracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + VideoClicks: []string{"http://videoclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + NonLinearClickTracking: []string{"http://nonlinearclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + CompanionClickThrough: []string{"http://companionclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + type args struct { + vastXML string + NURL string + } + tests := []struct { + name string + args args + want string + wantError error + }{ + { + name: "Empty vastXML and NURL present", + args: args{ + vastXML: "", + NURL: "www.nurl.com", + }, + want: `prebid.org wrapper`, + wantError: nil, + }, + { + name: "Empty vastXML and empty NURL", + args: args{ + vastXML: "", + NURL: "", + }, + want: "", + wantError: errors.New("invalid Vast XML"), + }, + { + name: "No Inline/Wrapper tag present", + args: args{ + vastXML: ``, + NURL: "", + }, + want: ``, + wantError: errors.New("invalid VastXML, inline/wrapper tag not found"), + }, + { + name: "Invalid Vast XML, parsing error", + args: args{ + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressionInline Simple AdIAB Sample CompanyAD CONTENT description category846500:00:16`, + NURL: "", + }, + want: ``, + wantError: errors.New("XML processing error: xml: end tag does not match start tag "), + }, + { + name: "Inline Linear vastXML, no existing event tracker", + args: args{ + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impressionInline Simple AdIAB Sample CompanyAD CONTENT description category846500:00:16`, + NURL: "", + }, + want: ``, + }, + { + name: "Non Linear vastXML, no existing event tracker", + args: args{ + NURL: "", + vastXML: `iabtechlab8465`, + }, + want: ``, + }, + { + name: "Wrapper Liner vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlabhttp://example.com/errorhttp://example.com/track/impression`, + }, + want: ``, + }, + { + name: "Wapper companion vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlab`, + }, + want: ``, + }, + { + name: "Wapper no companion vastXML", + args: args{ + NURL: "", + vastXML: `iabtechlab`, + }, + want: ``, + }, + { + name: "Inline Non Linear empty", + args: args{ + NURL: "", + vastXML: `iabtechlaba532d16d-4d7f-4440-bd29-2ec0e693fc80iabtechlab video ad`, + }, + want: ``, + }, + { + name: "Wrapper linear and non linear", + args: args{ + NURL: "", + vastXML: `Test Ad Server`, + }, + want: ``, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ti.InjectTracker(tt.args.vastXML, tt.args.NURL) + assert.Equal(t, tt.want, got, tt.name) + if tt.wantError != nil { + assert.EqualError(t, err, tt.wantError.Error()) + } + }) + } +} + +func TestAddClickTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + VideoClicks: []string{"http://videoclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addClickTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddImpressionTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "Add impression tag", + addParentTag: true, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Impressions: []string{"http://impressiontracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addImpressionTrackingEvent(&outputXML) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddErrorTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "Add impression tag", + addParentTag: true, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + Errors: []string{"http://errortracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addErrorTrackingEvent(&outputXML) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddNonLinearClickTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + NonLinearClickTracking: []string{"http://nonlinearclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addNonLinearClickTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddCompanionClickThroughEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + CompanionClickThrough: []string{"http://companionclicktracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}, + }, + ) + ti.addCompanionClickThroughEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestAddTrackingEvent(t *testing.T) { + tests := []struct { + name string + addParentTag bool + expected string + }{ + { + name: "With Parent Tag", + addParentTag: true, + expected: "", + }, + { + name: "Without Parent Tag", + addParentTag: false, + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + ti.addTrackingEvent(&outputXML, "testCreativeId", tt.addParentTag) + assert.Equal(t, tt.expected, outputXML.String(), tt.name) + }) + } +} + +func TestWriteTrackingEvent(t *testing.T) { + tests := []struct { + name string + urls []string + startTag string + endTag string + creativeId string + eventType string + vastEvent string + expectedXML string + }{ + { + name: "Single URL", + urls: []string{"http://tracker.com"}, + startTag: "", + endTag: "", + creativeId: "123", + eventType: "start", + vastEvent: "tracking", + expectedXML: "http://tracker.com", + }, + { + name: "Multiple URL", + urls: []string{"http://tracker1.com", "http://tracker2.com"}, + startTag: "", + endTag: "", + creativeId: "123", + eventType: "start", + vastEvent: "tracking", + expectedXML: "http://tracker1.comhttp://tracker2.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var outputXML strings.Builder + b := macros.NewProvider(reqWrapper) + b.PopulateBidMacros(&entities.PbsOrtbBid{ + Bid: &openrtb2.Bid{ + ID: "bid123", + }, + }, "testSeat") + ti := NewTrackerInjector( + macros.NewStringIndexBasedReplacer(), + b, + VASTEvents{ + TrackingEvents: map[string][]string{"firstQuartile": {"http://eventracker1.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-APPBUNDLE##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o6=##PBS-LIMITADTRACKING##¯o7=##PBS-GDPRCONSENT##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1CUST1##¯o10=##PBS-MACRO-CUSTOMMACR1CUST2##"}}, + }, + ) + ti.writeTrackingEvent(tt.urls, &outputXML, tt.startTag, tt.endTag, tt.creativeId, tt.eventType, tt.vastEvent) + assert.Equal(t, tt.expectedXML, outputXML.String(), tt.name) + }) + } +} diff --git a/macros/provider.go b/macros/provider.go index 3cae540e22a..0b4ef3eacb3 100644 --- a/macros/provider.go +++ b/macros/provider.go @@ -33,19 +33,19 @@ const ( CustomMacroPrefix = "PBS-MACRO-" ) -type macroProvider struct { +type MacroProvider struct { // macros map stores macros key values macros map[string]string } // NewBuilder returns the instance of macro buidler -func NewProvider(reqWrapper *openrtb_ext.RequestWrapper) *macroProvider { - macroProvider := ¯oProvider{macros: map[string]string{}} +func NewProvider(reqWrapper *openrtb_ext.RequestWrapper) *MacroProvider { + macroProvider := &MacroProvider{macros: map[string]string{}} macroProvider.populateRequestMacros(reqWrapper) return macroProvider } -func (b *macroProvider) populateRequestMacros(reqWrapper *openrtb_ext.RequestWrapper) { +func (b *MacroProvider) populateRequestMacros(reqWrapper *openrtb_ext.RequestWrapper) { b.macros[MacroKeyTimestamp] = strconv.Itoa(int(time.Now().Unix())) reqExt, err := reqWrapper.GetRequestExt() if err == nil && reqExt != nil { @@ -114,11 +114,11 @@ func (b *macroProvider) populateRequestMacros(reqWrapper *openrtb_ext.RequestWra } -func (b *macroProvider) GetMacro(key string) string { +func (b *MacroProvider) GetMacro(key string) string { return url.QueryEscape(b.macros[key]) } -func (b *macroProvider) PopulateBidMacros(bid *entities.PbsOrtbBid, seat string) { +func (b *MacroProvider) PopulateBidMacros(bid *entities.PbsOrtbBid, seat string) { if bid.Bid != nil { if bid.GeneratedBidID != "" { b.macros[MacroKeyBidID] = bid.GeneratedBidID @@ -129,7 +129,7 @@ func (b *macroProvider) PopulateBidMacros(bid *entities.PbsOrtbBid, seat string) b.macros[MacroKeyBidder] = seat } -func (b *macroProvider) PopulateEventMacros(vastCreativeID, eventType, vastEvent string) { +func (b *MacroProvider) PopulateEventMacros(vastCreativeID, eventType, vastEvent string) { b.macros[MacroKeyVastCRTID] = vastCreativeID b.macros[MacroKeyEventType] = eventType b.macros[MacroKeyVastEvent] = vastEvent diff --git a/macros/provider_test.go b/macros/provider_test.go index b3f5c9a88a9..3da56ea7826 100644 --- a/macros/provider_test.go +++ b/macros/provider_test.go @@ -199,7 +199,7 @@ func TestPopulateRequestMacros(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := ¯oProvider{ + b := &MacroProvider{ macros: map[string]string{}, } b.populateRequestMacros(tt.args.reqWrapper) @@ -300,7 +300,7 @@ func TestPopulateBidMacros(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := ¯oProvider{ + b := &MacroProvider{ macros: map[string]string{}, } b.PopulateBidMacros(tt.args.bid, tt.args.seat) @@ -401,7 +401,7 @@ func TestPopulateEventMacros(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := ¯oProvider{ + b := &MacroProvider{ macros: map[string]string{}, } b.PopulateEventMacros(tt.args.vastCreativeID, tt.args.eventType, tt.args.vastEvent) diff --git a/macros/replacer.go b/macros/replacer.go index e1a13d27636..a28be0cd21b 100644 --- a/macros/replacer.go +++ b/macros/replacer.go @@ -1,7 +1,9 @@ package macros +import "strings" + type Replacer interface { // Replace the macros and returns replaced string // if any error the error will be returned - Replace(url string, macroProvider *macroProvider) (string, error) + Replace(result *strings.Builder, url string, macroProvider *MacroProvider) } diff --git a/macros/string_index_based_replacer.go b/macros/string_index_based_replacer.go index 32748c1ed76..da0c89322e2 100644 --- a/macros/string_index_based_replacer.go +++ b/macros/string_index_based_replacer.go @@ -65,14 +65,12 @@ func constructTemplate(url string) urlMetaTemplate { // If input string is not found in cache then template metadata will be created. // Iterates over start and end indexes of the template arrays and extracts macro name from the input string. // Gets the value of the extracted macro from the macroProvider. Replaces macro with corresponding value. -func (s *stringIndexBasedReplacer) Replace(url string, macroProvider *macroProvider) (string, error) { - tmplt := s.getTemplate(url) - - var result strings.Builder +func (s *stringIndexBasedReplacer) Replace(result *strings.Builder, url string, macroProvider *MacroProvider) { + template := s.getTemplate(url) currentIndex := 0 delimLen := len(delimiter) - for i, index := range tmplt.startingIndices { - macro := url[index : tmplt.endingIndices[i]+1] + for i, index := range template.startingIndices { + macro := url[index : template.endingIndices[i]+1] // copy prev part result.WriteString(url[currentIndex : index-delimLen]) value := macroProvider.GetMacro(macro) @@ -82,7 +80,6 @@ func (s *stringIndexBasedReplacer) Replace(url string, macroProvider *macroProvi currentIndex = index + len(macro) + delimLen } result.WriteString(url[currentIndex:]) - return result.String(), nil } func (s *stringIndexBasedReplacer) getTemplate(url string) urlMetaTemplate { diff --git a/macros/string_index_based_replacer_test.go b/macros/string_index_based_replacer_test.go index eb81a1520e9..6e09db1e15a 100644 --- a/macros/string_index_based_replacer_test.go +++ b/macros/string_index_based_replacer_test.go @@ -1,6 +1,7 @@ package macros import ( + "strings" "testing" "github.com/prebid/openrtb/v20/openrtb2" @@ -13,7 +14,7 @@ func TestStringIndexBasedReplace(t *testing.T) { type args struct { url string - getMacroProvider func() *macroProvider + getMacroProvider func() *MacroProvider } tests := []struct { name string @@ -25,69 +26,61 @@ func TestStringIndexBasedReplace(t *testing.T) { name: "success", args: args{ url: "http://tracker.com?macro1=##PBS-BIDID##¯o2=##PBS-APPBUNDLE##¯o3=##PBS-DOMAIN##¯o4=##PBS-PUBDOMAIN##¯o5=##PBS-PAGEURL##¯o6=##PBS-ACCOUNTID##¯o7=##PBS-LIMITADTRACKING##¯o8=##PBS-GDPRCONSENT##¯o9=##PBS-MACRO-CUSTOMMACR1##¯o10=##PBS-BIDDER##¯o11=##PBS-INTEGRATION##¯o12=##PBS-VASTCRTID##¯o15=##PBS-AUCTIONID##¯o16=##PBS-CHANNEL##¯o17=##PBS-EVENTTYPE##¯o18=##PBS-VASTEVENT##", - getMacroProvider: func() *macroProvider { + getMacroProvider: func() *MacroProvider { macroProvider := NewProvider(req) macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") return macroProvider }, }, - want: "http://tracker.com?macro1=bidId123¯o2=testbundle¯o3=testdomain¯o4=publishertestdomain¯o5=pageurltest¯o6=testpublisherID¯o7=10¯o8=yes¯o9=value1¯o10=test¯o11=¯o12=123¯o15=123¯o16=test1¯o17=vast¯o18=firstQuartile", - wantErr: false, + want: "http://tracker.com?macro1=bidId123¯o2=testbundle¯o3=testdomain¯o4=publishertestdomain¯o5=pageurltest¯o6=testpublisherID¯o7=10¯o8=yes¯o9=value1¯o10=test¯o11=¯o12=123¯o15=123¯o16=test1¯o17=vast¯o18=firstQuartile", }, { name: "url does not have macro", args: args{ url: "http://tracker.com", - getMacroProvider: func() *macroProvider { + getMacroProvider: func() *MacroProvider { macroProvider := NewProvider(req) macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") return macroProvider }, }, - want: "http://tracker.com", - wantErr: false, + want: "http://tracker.com", }, { name: "macro not found", args: args{ url: "http://tracker.com?macro1=##PBS-test1##", - getMacroProvider: func() *macroProvider { + getMacroProvider: func() *MacroProvider { macroProvider := NewProvider(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}) macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") return macroProvider }, }, - want: "http://tracker.com?macro1=", - wantErr: false, + want: "http://tracker.com?macro1=", }, { name: "tracker url is empty", args: args{ url: "", - getMacroProvider: func() *macroProvider { + getMacroProvider: func() *MacroProvider { macroProvider := NewProvider(&openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{}}) macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") return macroProvider }, }, - want: "", - wantErr: false, + want: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { replacer := NewStringIndexBasedReplacer() - got, err := replacer.Replace(tt.args.url, tt.args.getMacroProvider()) - if tt.wantErr { - assert.Error(t, err, tt.name) - } else { - assert.NoError(t, err, tt.name) - assert.Equal(t, tt.want, got, tt.name) - } + builder := strings.Builder{} + replacer.Replace(&builder, tt.args.url, tt.args.getMacroProvider()) + assert.Equal(t, tt.want, builder.String(), tt.name) }) } } @@ -132,15 +125,13 @@ var bid *openrtb2.Bid = &openrtb2.Bid{ID: "bidId123", CID: "campaign_1", CrID: " func BenchmarkStringIndexBasedReplacer(b *testing.B) { replacer := NewStringIndexBasedReplacer() + builder := &strings.Builder{} for n := 0; n < b.N; n++ { for _, url := range benchmarkURL { macroProvider := NewProvider(req) macroProvider.PopulateBidMacros(&entities.PbsOrtbBid{Bid: bid}, "test") macroProvider.PopulateEventMacros("123", "vast", "firstQuartile") - _, err := replacer.Replace(url, macroProvider) - if err != nil { - b.Errorf("Fail to replace macro in tracker") - } + replacer.Replace(builder, url, macroProvider) } } }