From de76b1fcaffdcc5785af80220998cdd0a546a6d7 Mon Sep 17 00:00:00 2001 From: steffenmllr Date: Thu, 11 Jan 2024 10:20:07 +0100 Subject: [PATCH] Init agma --- analytics/agma/README.md | 28 ++ analytics/agma/agma_module.go | 267 ++++++++++++ analytics/agma/agma_module_test.go | 648 +++++++++++++++++++++++++++++ analytics/agma/model.go | 50 +++ analytics/agma/model_test.go | 46 ++ analytics/agma/sender.go | 84 ++++ analytics/agma/sender_test.go | 116 ++++++ analytics/build/build.go | 14 + analytics/build/build_test.go | 35 +- config/config.go | 38 +- config/config_test.go | 34 +- 11 files changed, 1356 insertions(+), 4 deletions(-) create mode 100644 analytics/agma/README.md create mode 100644 analytics/agma/agma_module.go create mode 100644 analytics/agma/agma_module_test.go create mode 100644 analytics/agma/model.go create mode 100644 analytics/agma/model_test.go create mode 100644 analytics/agma/sender.go create mode 100644 analytics/agma/sender_test.go diff --git a/analytics/agma/README.md b/analytics/agma/README.md new file mode 100644 index 00000000000..ca006db88f9 --- /dev/null +++ b/analytics/agma/README.md @@ -0,0 +1,28 @@ +# agma Analytics + +In order to use the Agma Analytics Adapter, please adjust the accounts with the data provided by agma (https://www.agma-mmc.de). + +## Configuration + +```yaml +analytics: + agma: + # Required: enable the module + enabled: true + # Required: set the accounts you want to track + accounts: + - code: "my-code" # Required: provied by agma + publisher_id: "123" # Required: Exchange specific publisher_id + site_app_id: "openrtb2-site.id-or-app.id" # optional: scope to the publisher with an openrtb2 Site object id or App object id + # Optional properties (advanced configuration) + endpoint: + url: "https://pbs-go.agma-analytics.de/v1/prebid-server" + timeout: "2s" + gzip: true + buffer: # Flush events when (first condition reached) + # Size of the buffer in bytes + size: "2MB" # greater than 2MB (size using SI standard eg. "44kB", "17MB") + count : 100 # greater than 100 events + timeout: "15m" # greater than 15 minutes (parsed as golang duration) + +``` diff --git a/analytics/agma/agma_module.go b/analytics/agma/agma_module.go new file mode 100644 index 00000000000..23c23dc0851 --- /dev/null +++ b/analytics/agma/agma_module.go @@ -0,0 +1,267 @@ +package agma + +import ( + "bytes" + "errors" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/benbjohnson/clock" + "github.com/docker/go-units" + "github.com/golang/glog" + "github.com/prebid/go-gdpr/vendorconsent" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" +) + +type httpSender = func(payload []byte) error + +const ( + agmaGVLID = 1122 + analyticsPurpose = 7 +) + +type AgamLogger struct { + sender httpSender + clock clock.Clock + accounts []config.AgmaAnalyticsAccount + eventCount int64 + maxEventCount int64 + maxBufferByteSize int64 + maxDuration time.Duration + mux sync.RWMutex + sigTermCh chan os.Signal + buffer bytes.Buffer + bufferCh chan []byte +} + +func newAgmaLogger(cfg config.AgmaAnalytics, sender httpSender, clock clock.Clock) (*AgamLogger, error) { + pSize, err := units.FromHumanSize(cfg.Buffers.BufferSize) + if err != nil { + return nil, err + } + pDuration, err := time.ParseDuration(cfg.Buffers.Timeout) + if err != nil { + return nil, err + } + if len(cfg.Accounts) == 0 { + return nil, errors.New("Please configure at least one account for Agma Analytics") + } + + buffer := bytes.Buffer{} + buffer.Write([]byte("[")) + + return &AgamLogger{ + sender: sender, + clock: clock, + accounts: cfg.Accounts, + maxBufferByteSize: pSize, + eventCount: 0, + maxEventCount: int64(cfg.Buffers.EventCount), + maxDuration: pDuration, + buffer: buffer, + bufferCh: make(chan []byte), + sigTermCh: make(chan os.Signal, 1), + }, nil +} + +func NewModule(httpClient *http.Client, cfg config.AgmaAnalytics, clock clock.Clock) (analytics.Module, error) { + sender, err := createHttpSender(httpClient, cfg.Endpoint) + if err != nil { + return nil, err + } + + m, err := newAgmaLogger(cfg, sender, clock) + if err != nil { + return nil, err + } + + signal.Notify(m.sigTermCh, os.Interrupt, syscall.SIGTERM) + + go m.start() + + return m, nil +} + +func (l *AgamLogger) start() { + ticker := l.clock.Ticker(l.maxDuration) + for { + select { + case <-l.sigTermCh: + glog.Infof("[AgmaAnalytics] Received Close, trying to flush buffer") + l.flush() + return + case event := <-l.bufferCh: + l.bufferEvent(event) + if l.isFull() { + l.flush() + } + case <-ticker.C: + l.flush() + } + } +} + +func (l *AgamLogger) bufferEvent(data []byte) { + l.mux.Lock() + defer l.mux.Unlock() + + l.buffer.Write(data) + l.buffer.WriteByte(',') + l.eventCount++ +} + +func (l *AgamLogger) isFull() bool { + l.mux.RLock() + defer l.mux.RUnlock() + return l.eventCount >= l.maxEventCount || int64(l.buffer.Len()) >= l.maxBufferByteSize +} + +func (l *AgamLogger) flush() { + l.mux.Lock() + + if l.eventCount == 0 || l.buffer.Len() == 0 { + l.mux.Unlock() + return + } + + // Close the json array, remove last , + l.buffer.Truncate(l.buffer.Len() - 1) + _, err := l.buffer.Write([]byte("]")) + if err != nil { + l.reset() + l.mux.Unlock() + glog.Warning("[AgmaAnalytics] fail to close the json array") + return + } + + payload := make([]byte, l.buffer.Len()) + _, err = l.buffer.Read(payload) + if err != nil { + l.reset() + l.mux.Unlock() + glog.Warning("[AgmaAnalytics] fail to copy the buffer") + return + } + + go l.sender(payload) + + l.reset() + l.mux.Unlock() +} + +func (l *AgamLogger) reset() { + l.buffer.Reset() + l.buffer.Write([]byte("[")) + l.eventCount = 0 +} + +func (l *AgamLogger) shouldTrackEvent(requestWrapper *openrtb_ext.RequestWrapper) (bool, string) { + userExt, err := requestWrapper.GetUserExt() + if err != nil || userExt == nil { + return false, "" + } + consent := userExt.GetConsent() + if consent == nil { + return false, "" + } + consentStr := *consent + parsedConsent, err := vendorconsent.ParseString(consentStr) + if err != nil { + return false, "" + } + + analyticsAllowed := parsedConsent.PurposeAllowed(analyticsPurpose) + agmaAllowed := parsedConsent.VendorConsent(agmaGVLID) + if !analyticsAllowed || !agmaAllowed { + return false, "" + } + publisherId := "" + appSiteId := "" + if requestWrapper.Site != nil { + if requestWrapper.Site.Publisher != nil { + publisherId = requestWrapper.Site.Publisher.ID + } + appSiteId = requestWrapper.Site.ID + } + if requestWrapper.App != nil { + if requestWrapper.App.Publisher != nil { + publisherId = requestWrapper.App.Publisher.ID + } + appSiteId = requestWrapper.App.ID + } + + if publisherId == "" && appSiteId == "" { + return false, "" + } + + for _, account := range l.accounts { + if account.PublisherId == publisherId { + if account.SiteAppId == "" { + return true, account.Code + } + if account.SiteAppId == appSiteId { + return true, account.Code + } + } + } + + return false, "" +} + +func (l *AgamLogger) LogAuctionObject(event *analytics.AuctionObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAuction, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing auction object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgamLogger) LogAmpObject(event *analytics.AmpObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeAmp, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing amp object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgamLogger) LogVideoObject(event *analytics.VideoObject) { + if event == nil || event.Status != http.StatusOK || event.RequestWrapper == nil { + return + } + shouldTrack, code := l.shouldTrackEvent(event.RequestWrapper) + if !shouldTrack { + return + } + data, err := serializeAnayltics(event.RequestWrapper, EventTypeVideo, code, event.StartTime) + if err != nil { + glog.Errorf("[AgmaAnalytics] Error serializing video object: %v", err) + return + } + l.bufferCh <- data +} + +func (l *AgamLogger) LogCookieSyncObject(event *analytics.CookieSyncObject) {} +func (l *AgamLogger) LogNotificationEventObject(event *analytics.NotificationEvent) {} +func (l *AgamLogger) LogSetUIDObject(event *analytics.SetUIDObject) {} diff --git a/analytics/agma/agma_module_test.go b/analytics/agma/agma_module_test.go new file mode 100644 index 00000000000..d3d08278e11 --- /dev/null +++ b/analytics/agma/agma_module_test.go @@ -0,0 +1,648 @@ +package agma + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "sync" + "syscall" + "testing" + "time" + + "github.com/benbjohnson/clock" + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var agmaConsent = "CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A" + +var mockValidAuctionObject = analytics.AuctionObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidVideoObject = analytics.VideoObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "track-me-app", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidAmpObject = analytics.AmpObject{ + Status: http.StatusOK, + StartTime: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC), + RequestWrapper: &openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "track-me-site", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + Device: &openrtb2.Device{ + UA: "ua", + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }, +} + +var mockValidAccounts = []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "track-me-app", + }, + { + PublisherId: "track-me", + Code: "abcd", + SiteAppId: "track-me-site", + }, +} + +type MockedSender struct { + mock.Mock +} + +func (m *MockedSender) Send(payload []byte) error { + args := m.Called(payload) + return args.Error(0) +} + +func TestConfigParsingError(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + shouldFail bool + }{ + { + name: "Test with invalid/empty URL", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "%%2815197306101420000%29", + Timeout: "1s", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with invalid timout", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1x", + Gzip: false, + }, + }, + shouldFail: true, + }, + { + name: "Test with no accounts", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "1s", + Gzip: false, + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{}, + }, + shouldFail: true, + }, + } + clockMock := clock.NewMock() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := NewModule(&http.Client{}, tc.config, clockMock) + if tc.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestShouldTrackEvent(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + // no userExt + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-not", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // no userExt + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No agma + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIwgAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) + + // Constent: No Prupose 7 + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "CP4LywcP4LywcLRAAAENCZCAAIAAAAAAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A"}`), + }, + }, + }) + + assert.False(t, shouldTrack) + assert.Equal(t, "", code) +} + +func TestShouldTrackMultipleAccounts(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me-a", + Code: "abc", + }, + { + PublisherId: "track-me-b", + Code: "123", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + shouldTrack, code := logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + App: &openrtb2.App{ + ID: "com.app.test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-a", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "abc", code) + + shouldTrack, code = logger.shouldTrackEvent(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + Site: &openrtb2.Site{ + ID: "site-test", + Publisher: &openrtb2.Publisher{ + ID: "track-me-b", + }, + }, + User: &openrtb2.User{ + Ext: json.RawMessage(`{"consent": "` + agmaConsent + `"}`), + }, + }, + }) + + assert.True(t, shouldTrack) + assert.Equal(t, "123", code) +} + +func TestShouldNotTrackLog(t *testing.T) { + testCases := []struct { + name string + config config.AgmaAnalytics + }{ + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "do-not-track", + Code: "abc", + }, + }, + }, + }, + { + name: "Test with do-not-track PublisherId", + config: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1, + BufferSize: "1Kb", + Timeout: "1s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + SiteAppId: "do-not-track", + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(tc.config, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + clockMock.Add(2 * time.Minute) + mockedSender.AssertNumberOfCalls(t, "Send", 0) + assert.Zero(t, logger.eventCount) + }) + } +} + +func TestRaceAllEvents(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + clockMock.Add(10 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(3), logger.eventCount) + logger.mux.RUnlock() +} + +func TestFlushOnSigterm(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 10000, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + done := make(chan struct{}) + go func() { + logger.start() + close(done) + }() + + logger.LogAuctionObject(&mockValidAuctionObject) + logger.LogVideoObject(&mockValidVideoObject) + logger.LogAmpObject(&mockValidAmpObject) + + logger.sigTermCh <- syscall.SIGTERM + <-done + + time.Sleep(100 * time.Millisecond) + + mockedSender.AssertCalled(t, "Send", mock.Anything) +} + +func TestRaceBufferCount(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100Mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + assert.Zero(t, logger.eventCount) + + // Test EventCount Buffer + logger.LogAuctionObject(&mockValidAuctionObject) + + clockMock.Add(1 * time.Millisecond) + + logger.mux.RLock() + assert.Equal(t, int64(1), logger.eventCount) + logger.mux.RUnlock() + + assert.Equal(t, false, logger.isFull()) + + // add 1 more + logger.LogAuctionObject(&mockValidAuctionObject) + clockMock.Add(1 * time.Millisecond) + + // should trigger send and flash the buffer + mockedSender.AssertCalled(t, "Send", mock.Anything) + + logger.mux.RLock() + assert.Equal(t, int64(0), logger.eventCount) + logger.mux.RUnlock() +} + +func TestBufferSize(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "20Kb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 50; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Millisecond) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestBufferTime(t *testing.T) { + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8000/event", + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 1000, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "track-me", + Code: "abc", + }, + }, + } + mockedSender := new(MockedSender) + mockedSender.On("Send", mock.Anything).Return(nil) + clockMock := clock.NewMock() + logger, err := newAgmaLogger(cfg, mockedSender.Send, clockMock) + assert.NoError(t, err) + + go logger.start() + + for i := 0; i < 5; i++ { + logger.LogAuctionObject(&mockValidAuctionObject) + } + clockMock.Add(10 * time.Minute) + mockedSender.AssertCalled(t, "Send", mock.Anything) + mockedSender.AssertNumberOfCalls(t, "Send", 1) +} + +func TestRaceEnd2End(t *testing.T) { + var mu sync.Mutex + + requestBodyAsString := "" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check for reponse + requestBody, err := io.ReadAll(r.Body) + mu.Lock() + requestBodyAsString = string(requestBody) + mu.Unlock() + if err != nil { + http.Error(w, "Error reading request body", 500) + return + } + + w.WriteHeader(http.StatusOK) + })) + cfg := config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: server.URL, + Timeout: "5s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + EventCount: 2, + BufferSize: "100mb", + Timeout: "5m", + }, + Accounts: mockValidAccounts, + } + + clockMock := clock.NewMock() + clockMock.Set(time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)) + + logger, err := NewModule(&http.Client{}, cfg, clockMock) + assert.NoError(t, err) + + logger.LogAmpObject(&mockValidAmpObject) + logger.LogAmpObject(&mockValidAmpObject) + + time.Sleep(250 * time.Millisecond) + + expected := "[{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A\"}},\"created_at\":\"2023-02-01T00:00:00Z\"},{\"type\":\"amp\",\"id\":\"some-id\",\"code\":\"abcd\",\"site\":{\"id\":\"track-me-site\",\"publisher\":{\"id\":\"track-me\"}},\"device\":{\"ua\":\"ua\"},\"user\":{\"ext\":{\"consent\": \"CP4LywcP4LywcLRAAAENCZCAAAIAAAIAAAAAIxQAQIxAAAAA.II7Nd_X__bX9n-_7_6ft0eY1f9_r37uQzDhfNs-8F3L_W_LwX32E7NF36tq4KmR4ku1bBIQNtHMnUDUmxaolVrzHsak2cpyNKJ_JkknsZe2dYGF9Pn9lD-YKZ7_5_9_f52T_9_9_-39z3_9f___dv_-__-vjf_599n_v9fV_78_Kf9______-____________8A\"}},\"created_at\":\"2023-02-01T00:00:00Z\"}]" + + mu.Lock() + actual := requestBodyAsString + mu.Unlock() + + assert.Equal(t, expected, actual) +} diff --git a/analytics/agma/model.go b/analytics/agma/model.go new file mode 100644 index 00000000000..412bc62de31 --- /dev/null +++ b/analytics/agma/model.go @@ -0,0 +1,50 @@ +package agma + +import ( + "fmt" + "time" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/prebid/prebid-server/v2/util/jsonutil" +) + +type EventType string + +const ( + EventTypeAuction EventType = "auction" + EventTypeAmp EventType = "amp" + EventTypeVideo EventType = "video" +) + +type logObject struct { + EventType EventType `json:"type"` + RequestId string `json:"id"` + AccountCode string `json:"code"` + Site *openrtb2.Site `json:"site,omitempty"` + App *openrtb2.App `json:"app,omitempty"` + Device *openrtb2.Device `json:"device,omitempty"` + User *openrtb2.User `json:"user,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +func serializeAnayltics( + requestwrapper *openrtb_ext.RequestWrapper, + eventType EventType, + code string, + createdAt time.Time, +) ([]byte, error) { + if requestwrapper == nil || requestwrapper.BidRequest == nil { + return nil, fmt.Errorf("requestwrapper or BidRequest object nil") + } + return jsonutil.Marshal(&logObject{ + EventType: eventType, + RequestId: requestwrapper.ID, + AccountCode: code, + Site: requestwrapper.BidRequest.Site, + App: requestwrapper.BidRequest.App, + Device: requestwrapper.BidRequest.Device, + User: requestwrapper.BidRequest.User, + CreatedAt: createdAt, + }) +} diff --git a/analytics/agma/model_test.go b/analytics/agma/model_test.go new file mode 100644 index 00000000000..729ecb1490e --- /dev/null +++ b/analytics/agma/model_test.go @@ -0,0 +1,46 @@ +package agma + +import ( + "testing" + "time" + + "github.com/prebid/openrtb/v19/openrtb2" + "github.com/prebid/prebid-server/v2/openrtb_ext" + "github.com/stretchr/testify/assert" +) + +func TestCheckForNil(t *testing.T) { + code := "test" + _, err := serializeAnayltics(nil, EventTypeAuction, code, time.Now()) + assert.Error(t, err) +} + +func TestSerializeAuctionObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeAuction, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"auction\"") +} + +func TestSerializeVideoObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeVideo, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"video\"") +} + +func TestSerializeAmpObject(t *testing.T) { + data, err := serializeAnayltics(&openrtb_ext.RequestWrapper{ + BidRequest: &openrtb2.BidRequest{ + ID: "some-id", + }, + }, EventTypeAmp, "test", time.Now()) + assert.NoError(t, err) + assert.Contains(t, string(data), "\"type\":\"amp\"") +} diff --git a/analytics/agma/sender.go b/analytics/agma/sender.go new file mode 100644 index 00000000000..6cdc5ea1526 --- /dev/null +++ b/analytics/agma/sender.go @@ -0,0 +1,84 @@ +package agma + +import ( + "bytes" + "compress/gzip" + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/golang/glog" + "github.com/prebid/prebid-server/v2/config" + "github.com/prebid/prebid-server/v2/version" +) + +func compressToGZIP(requestBody []byte) ([]byte, error) { + var b bytes.Buffer + w := gzip.NewWriter(&b) + _, err := w.Write([]byte(requestBody)) + if err != nil { + _ = w.Close() + return nil, err + } + err = w.Close() + if err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func createHttpSender(httpClient *http.Client, endpoint config.AgmaAnalyticsHttpEndpoint) (httpSender, error) { + _, err := url.Parse(endpoint.Url) + if err != nil { + return nil, err + } + + httpTimeout, err := time.ParseDuration(endpoint.Timeout) + if err != nil { + return nil, err + } + + return func(payload []byte) error { + ctx, cancel := context.WithTimeout(context.Background(), httpTimeout) + defer cancel() + + var requestBody []byte + var err error + + if endpoint.Gzip { + requestBody, err = compressToGZIP(payload) + if err != nil { + glog.Errorf("[agmaAnalytics] Compressing request failed %v", err) + return err + } + } else { + requestBody = payload + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.Url, bytes.NewBuffer(requestBody)) + if err != nil { + glog.Errorf("[agmaAnalytics] Creating request failed %v", err) + return err + } + + req.Header.Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) + req.Header.Set("Content-Type", "application/json") + if endpoint.Gzip { + req.Header.Set("Content-Encoding", "gzip") + } + + resp, err := httpClient.Do(req) + if err != nil { + glog.Errorf("[agmaAnalytics] Sending request failed %v", err) + return err + } + + if resp.StatusCode != http.StatusOK { + glog.Errorf("[agmaAnalytics] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + return fmt.Errorf("wrong code received %d instead of %d", resp.StatusCode, http.StatusOK) + } + return nil + }, nil +} diff --git a/analytics/agma/sender_test.go b/analytics/agma/sender_test.go new file mode 100644 index 00000000000..d0390506b2d --- /dev/null +++ b/analytics/agma/sender_test.go @@ -0,0 +1,116 @@ +package agma + +import ( + "compress/gzip" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prebid/prebid-server/v2/config" + "github.com/stretchr/testify/assert" +) + +func TestCreateHttpSender(t *testing.T) { + testCases := []struct { + name string + endpoint config.AgmaAnalyticsHttpEndpoint + wantHeaders http.Header + wantErr bool + }{ + { + name: "Test with invalid/empty URL", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "%%2815197306101420000%29", + Timeout: "1s", + Gzip: false, + }, + wantErr: true, + }, + { + name: "Test with timeout", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "2x", // Very short timeout + Gzip: false, + }, + wantErr: true, + }, + { + name: "Test with Gzip true", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + Gzip: true, + }, + wantHeaders: http.Header{ + "Content-Encoding": []string{"gzip"}, + "Content-Type": []string{"application/json"}, + }, + wantErr: false, + }, + { + name: "Test with Gzip false", + endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + Gzip: false, + }, + wantHeaders: http.Header{ + "Content-Type": []string{"application/json"}, + }, + wantErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testBody := []byte("[{ \"type\": \"test\" }]") + // Create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check the headers + for name, wantValues := range tc.wantHeaders { + assert.Equal(t, wantValues, r.Header[name], "Expected header '%s' to be '%v', got '%v'", name, wantValues, r.Header[name]) + } + defer r.Body.Close() + var reader io.ReadCloser + var err error + if tc.endpoint.Gzip { + reader, err = gzip.NewReader(r.Body) + assert.NoError(t, err) + defer reader.Close() + } else { + reader = r.Body + } + + decompressedData, err := io.ReadAll(reader) + assert.NoError(t, err) + + assert.Equal(t, string(testBody), string(decompressedData)) + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + // Update the URL of the endpoint to the URL of the test server + if !tc.wantErr { + tc.endpoint.Url = ts.URL + } + + // Create a test client + client := &http.Client{} + + // Test the createHttpSender function + sender, err := createHttpSender(client, tc.endpoint) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + // Test the returned HttpSender function + err = sender([]byte(testBody)) + assert.NoError(t, err) + }) + } +} diff --git a/analytics/build/build.go b/analytics/build/build.go index 37846b7d3e4..b9746186e3f 100644 --- a/analytics/build/build.go +++ b/analytics/build/build.go @@ -4,6 +4,7 @@ import ( "github.com/benbjohnson/clock" "github.com/golang/glog" "github.com/prebid/prebid-server/v2/analytics" + "github.com/prebid/prebid-server/v2/analytics/agma" "github.com/prebid/prebid-server/v2/analytics/clients" "github.com/prebid/prebid-server/v2/analytics/filesystem" "github.com/prebid/prebid-server/v2/analytics/pubstack" @@ -40,6 +41,19 @@ func New(analytics *config.Analytics) analytics.Runner { glog.Errorf("Could not initialize PubstackModule: %v", err) } } + + if analytics.Agma.Enabled { + agmaModule, err := agma.NewModule( + clients.GetDefaultHttpInstance(), + analytics.Agma, + clock.New()) + if err == nil { + modules["agma"] = agmaModule + } else { + glog.Errorf("Could not initialize Agma Anayltics: %v", err) + } + } + return modules } diff --git a/analytics/build/build_test.go b/analytics/build/build_test.go index d9b433cec4b..b8723980219 100644 --- a/analytics/build/build_test.go +++ b/analytics/build/build_test.go @@ -115,7 +115,6 @@ func TestNewPBSAnalytics_FileLogger(t *testing.T) { } func TestNewPBSAnalytics_Pubstack(t *testing.T) { - pbsAnalyticsWithoutError := New(&config.Analytics{ Pubstack: config.Pubstack{ Enabled: true, @@ -142,6 +141,40 @@ func TestNewPBSAnalytics_Pubstack(t *testing.T) { assert.Equal(t, len(instanceWithError), 0) } +func TestNewModuleHttp(t *testing.T) { + agmaAnalyticsWithoutError := New(&config.Analytics{ + Agma: config.AgmaAnalytics{ + Enabled: true, + Endpoint: config.AgmaAnalyticsHttpEndpoint{ + Url: "http://localhost:8080", + Timeout: "1s", + }, + Buffers: config.AgmaAnalyticsBuffer{ + BufferSize: "100KB", + EventCount: 50, + Timeout: "30s", + }, + Accounts: []config.AgmaAnalyticsAccount{ + { + PublisherId: "123", + Code: "abc", + }, + }, + }, + }) + instanceWithoutError := agmaAnalyticsWithoutError.(enabledAnalytics) + + assert.Equal(t, len(instanceWithoutError), 1) + + agmaAnalyticsWithError := New(&config.Analytics{ + Agma: config.AgmaAnalytics{ + Enabled: true, + }, + }) + instanceWithError := agmaAnalyticsWithError.(enabledAnalytics) + assert.Equal(t, len(instanceWithError), 0) +} + func TestSampleModuleActivitiesAllowed(t *testing.T) { var count int am := initAnalytics(&count) diff --git a/config/config.go b/config/config.go index f0e9f5c7fa7..eb5702cf2ff 100644 --- a/config/config.go +++ b/config/config.go @@ -442,8 +442,9 @@ type LMT struct { } type Analytics struct { - File FileLogs `mapstructure:"file"` - Pubstack Pubstack `mapstructure:"pubstack"` + File FileLogs `mapstructure:"file"` + Agma AgmaAnalytics `mapstructure:"agma"` + Pubstack Pubstack `mapstructure:"pubstack"` } type CurrencyConverter struct { @@ -459,6 +460,31 @@ func (cfg *CurrencyConverter) validate(errs []error) []error { return errs } +type AgmaAnalytics struct { + Enabled bool `mapstructure:"enabled"` + Endpoint AgmaAnalyticsHttpEndpoint `mapstructure:"endpoint"` + Buffers AgmaAnalyticsBuffer `mapstructure:"buffers"` + Accounts []AgmaAnalyticsAccount `mapstructure:"accounts"` +} + +type AgmaAnalyticsHttpEndpoint struct { + Url string `mapstructure:"url"` + Timeout string `mapstructure:"timeout"` + Gzip bool `mapstructure:"gzip"` +} + +type AgmaAnalyticsBuffer struct { + BufferSize string `mapstructure:"size"` + EventCount int `mapstructure:"count"` + Timeout string `mapstructure:"timeout"` +} + +type AgmaAnalyticsAccount struct { + Code string `mapstructure:"code"` + PublisherId string `mapstructure:"publisher_id"` + SiteAppId string `mapstructure:"site_app_id"` +} + // FileLogs Corresponding config for FileLogger as a PBS Analytics Module type FileLogs struct { Filename string `mapstructure:"filename"` @@ -1046,6 +1072,14 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { v.SetDefault("analytics.pubstack.buffers.size", "2MB") v.SetDefault("analytics.pubstack.buffers.count", 100) v.SetDefault("analytics.pubstack.buffers.timeout", "900s") + v.SetDefault("analytics.agma.enabled", false) + v.SetDefault("analytics.agma.endpoint.url", "https://pbs-go.agma-analytics.de/v1/prebid-server") + v.SetDefault("analytics.agma.endpoint.timeout", "2s") + v.SetDefault("analytics.agma.endpoint.gzip", false) + v.SetDefault("analytics.agma.buffers.size", "2MB") + v.SetDefault("analytics.agma.buffers.count", 100) + v.SetDefault("analytics.agma.buffers.timeout", "15m") + v.SetDefault("analytics.agma.accounts", []AgmaAnalyticsAccount{}) v.SetDefault("amp_timeout_adjustment_ms", 0) v.BindEnv("gdpr.default_value") v.SetDefault("gdpr.enabled", true) diff --git a/config/config_test.go b/config/config_test.go index a551c1be66e..48b682fe429 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -222,6 +222,14 @@ func TestDefaults(t *testing.T) { cmpInts(t, "account_defaults.privacy.ipv4.anon_keep_bits", 24, cfg.AccountDefaults.Privacy.IPv4Config.AnonKeepBits) //Assert purpose VendorExceptionMap hash tables were built correctly + cmpBools(t, "analytics.agma.enabled", false, cfg.Analytics.Agma.Enabled) + cmpStrings(t, "analytics.agma.endpoint.timeout", "2s", cfg.Analytics.Agma.Endpoint.Timeout) + cmpBools(t, "analytics.agma.endpoint.gzip", false, cfg.Analytics.Agma.Endpoint.Gzip) + cmpStrings(t, "analytics.agma.endppoint.url", "https://pbs-go.agma-analytics.de/v1/prebid-server", cfg.Analytics.Agma.Endpoint.Url) + cmpStrings(t, "analytics.agma.buffers.size", "2MB", cfg.Analytics.Agma.Buffers.BufferSize) + cmpInts(t, "analytics.agma.buffers.count", 100, cfg.Analytics.Agma.Buffers.EventCount) + cmpStrings(t, "analytics.agma.buffers.timeout", "15m", cfg.Analytics.Agma.Buffers.Timeout) + cmpInts(t, "analytics.agma.accounts", 0, len(cfg.Analytics.Agma.Accounts)) expectedTCF2 := TCF2{ Enabled: true, Purpose1: TCF2Purpose{ @@ -508,6 +516,21 @@ tmax_adjustments: bidder_response_duration_min_ms: 700 bidder_network_latency_buffer_ms: 100 pbs_response_preparation_duration_ms: 100 +analytics: + agma: + enabled: true + endpoint: + url: "http://test.com" + timeout: "5s" + gzip: false + buffers: + size: 10MB + count: 111 + timeout: 5m + accounts: + - code: agma-code + publisher_id: publisher-id + site_app_id: site-or-app-id `) func cmpStrings(t *testing.T, key, expected, actual string) { @@ -787,6 +810,16 @@ func TestFullConfig(t *testing.T) { cmpInts(t, "experiment.adscert.remote.signing_timeout_ms", 10, cfg.Experiment.AdCerts.Remote.SigningTimeoutMs) cmpBools(t, "hooks.enabled", true, cfg.Hooks.Enabled) cmpBools(t, "account_modules_metrics", true, cfg.Metrics.Disabled.AccountModulesMetrics) + cmpBools(t, "analytics.agma.enabled", true, cfg.Analytics.Agma.Enabled) + cmpStrings(t, "analytics.agma.endpoint.timeout", "5s", cfg.Analytics.Agma.Endpoint.Timeout) + cmpBools(t, "analytics.agma.endpoint.gzip", false, cfg.Analytics.Agma.Endpoint.Gzip) + cmpStrings(t, "analytics.agma.endpoint.url", "http://test.com", cfg.Analytics.Agma.Endpoint.Url) + cmpStrings(t, "analytics.agma.buffers.size", "10MB", cfg.Analytics.Agma.Buffers.BufferSize) + cmpInts(t, "analytics.agma.buffers.count", 111, cfg.Analytics.Agma.Buffers.EventCount) + cmpStrings(t, "analytics.agma.buffers.timeout", "5m", cfg.Analytics.Agma.Buffers.Timeout) + cmpStrings(t, "analytics.agma.accounts.0.publisher_id", "publisher-id", cfg.Analytics.Agma.Accounts[0].PublisherId) + cmpStrings(t, "analytics.agma.accounts.0.code", "agma-code", cfg.Analytics.Agma.Accounts[0].Code) + cmpStrings(t, "analytics.agma.accounts.0.site_app_id", "site-or-app-id", cfg.Analytics.Agma.Accounts[0].SiteAppId) } func TestValidateConfig(t *testing.T) { @@ -900,7 +933,6 @@ func TestUserSyncFromEnv(t *testing.T) { assert.Equal(t, "http://somedifferent.url/sync?redirect={{.RedirectURL}}", cfg.BidderInfos["bidder2"].Syncer.IFrame.URL) assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.Redirect) assert.Nil(t, cfg.BidderInfos["bidder2"].Syncer.SupportCORS) - } func TestBidderInfoFromEnv(t *testing.T) {