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)
}
}
}