diff --git a/.golangci.yml b/.golangci.yml index f34e716..7514120 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -113,7 +113,6 @@ linters: - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - - exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] #- forbidigo # Forbids identifiers [fast: false, auto-fix: false] - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid. [fast: true, auto-fix: false] diff --git a/go.mod b/go.mod index 6f45eeb..ccddbe4 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.23.0 require ( github.com/ThreeDotsLabs/watermill v1.3.5 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/foomo/go v0.0.3 github.com/foomo/gostandards v0.1.0 github.com/gogo/protobuf v1.3.2 @@ -16,9 +17,11 @@ require ( github.com/json-iterator/go v1.1.12 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 + github.com/pperaltaisern/watermillzap v1.0.0 github.com/prometheus/common v0.55.0 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -40,7 +43,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -140,7 +142,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.29.2 // indirect k8s.io/client-go v0.29.2 // indirect diff --git a/go.sum b/go.sum index f1465cf..a6d5258 100644 --- a/go.sum +++ b/go.sum @@ -495,6 +495,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pperaltaisern/watermillzap v1.0.0 h1:TtlI/WW6VHgkwgyXOtcMd/Utvw/2ZBm22o4bJ9IaoD4= +github.com/pperaltaisern/watermillzap v1.0.0/go.mod h1:tc6T7N3R5pKS7RAG2RUoscJI33yVk8aUPrfzHTGNR3c= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= diff --git a/integration/loki/line.go b/integration/loki/line.go index 4f5301f..d043593 100644 --- a/integration/loki/line.go +++ b/integration/loki/line.go @@ -8,15 +8,16 @@ import ( ) type Line struct { - Name sesamy.EventName `json:"name"` - Params any `json:"params"` - ClientID string `json:"client_id"` - UserID string `json:"user_id,omitempty"` - UserProperties map[string]any `json:"user_properties,omitempty"` - Consent *mpv2.Consent `json:"consent,omitempty"` - NonPersonalizedAds bool `json:"non_personalized_ads,omitempty"` - UserData *mpv2.UserData `json:"user_data,omitempty"` - DebugMode bool `json:"debug_mode,omitempty"` + Name sesamy.EventName `json:"name"` + Params any `json:"params"` + ClientID string `json:"client_id"` + UserID string `json:"user_id,omitempty"` + UserProperties map[string]any `json:"user_properties,omitempty"` + Consent *mpv2.ConsentData `json:"consent,omitempty"` + UserData *mpv2.UserData `json:"user_data,omitempty"` + DebugMode bool `json:"debug_mode,omitempty"` + SessionID string `json:"session_id,omitempty"` + EngagementTimeMSec int64 `json:"engagement_time_msec,omitempty"` } func (l *Line) Marshal() ([]byte, error) { diff --git a/integration/loki/loki.go b/integration/loki/loki.go index 48902c8..9ffadba 100644 --- a/integration/loki/loki.go +++ b/integration/loki/loki.go @@ -142,8 +142,9 @@ func (l *Loki) Write(payload mpv2.Payload[any]) { UserData: payload.UserData, ClientID: payload.ClientID, UserProperties: payload.UserProperties, - NonPersonalizedAds: payload.NonPersonalizedAds, DebugMode: payload.DebugMode, + SessionID: payload.SessionID, + EngagementTimeMSec: payload.EngagementTimeMSec, } lineBytes, err := line.Marshal() diff --git a/integration/loki/messagehandler.go b/integration/loki/messagehandler.go new file mode 100644 index 0000000..9c468ad --- /dev/null +++ b/integration/loki/messagehandler.go @@ -0,0 +1,23 @@ +package loki + +import ( + "encoding/json" + + "github.com/ThreeDotsLabs/watermill/message" + "github.com/foomo/sesamy-go/pkg/encoding/mpv2" + "github.com/pkg/errors" +) + +func MPv2MessageHandler(loki *Loki) message.NoPublishHandlerFunc { + return func(msg *message.Message) error { + var payload mpv2.Payload[any] + + // unmarshal payload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + return errors.Wrap(err, "failed to unmarshal payload") + } + + loki.Write(payload) + return nil + } +} diff --git a/integration/loki/service.go b/integration/loki/service.go new file mode 100644 index 0000000..908ff4d --- /dev/null +++ b/integration/loki/service.go @@ -0,0 +1,29 @@ +package loki + +import ( + "context" +) + +type Service struct { + loki *Loki +} + +func NewService(loki *Loki) *Service { + return &Service{loki: loki} +} + +func (s *Service) Name() string { + return "loki" +} + +// Start pulls lines out of the channel and sends them to Loki +func (s *Service) Start(ctx context.Context) error { + s.loki.Start(ctx) + return nil +} + +// Close will cancel any ongoing requests and stop the goroutine listening for requests +func (s *Service) Close(ctx context.Context) error { + s.loki.Stop() + return nil +} diff --git a/integration/watermill/gtag/messagehandler.go b/integration/watermill/gtag/messagehandler.go index eeb8be7..4af3c46 100644 --- a/integration/watermill/gtag/messagehandler.go +++ b/integration/watermill/gtag/messagehandler.go @@ -5,6 +5,8 @@ import ( "github.com/ThreeDotsLabs/watermill/message" "github.com/foomo/sesamy-go/pkg/encoding/gtag" + "github.com/foomo/sesamy-go/pkg/encoding/gtagencode" + "github.com/foomo/sesamy-go/pkg/encoding/mpv2" "github.com/pkg/errors" ) @@ -32,3 +34,27 @@ func MessageHandler(handler func(payload *gtag.Payload, msg *message.Message) er return []*message.Message{msg}, nil } } + +func MPv2MessageHandler(msg *message.Message) ([]*message.Message, error) { + var payload gtag.Payload + + // unmarshal payload + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + + // encode to mpv2 + var mpv2Payload *mpv2.Payload[any] + if err := gtagencode.MPv2(payload, &mpv2Payload); err != nil { + return nil, errors.Wrap(err, "failed to encode gtag to mpv2") + } + + // marshal payload + b, err := json.Marshal(mpv2Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal payload") + } + msg.Payload = b + + return []*message.Message{msg}, nil +} diff --git a/integration/watermill/gtag/messagehandler_test.go b/integration/watermill/gtag/messagehandler_test.go new file mode 100644 index 0000000..5aa4085 --- /dev/null +++ b/integration/watermill/gtag/messagehandler_test.go @@ -0,0 +1,148 @@ +package gtag_test + +import ( + "context" + "encoding/json" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/ThreeDotsLabs/watermill/pubsub/gochannel" + "github.com/foomo/sesamy-go/integration/watermill/gtag" + encoding "github.com/foomo/sesamy-go/pkg/encoding/gtag" + "github.com/foomo/sesamy-go/pkg/sesamy" + "github.com/pperaltaisern/watermillzap" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestMessageHandler(t *testing.T) { + l := zaptest.NewLogger(t) + + router, err := message.NewRouter(message.RouterConfig{}, watermillzap.NewLogger(l)) + require.NoError(t, err) + defer router.Close() + + // Create pubSub + pubSub := gochannel.NewGoChannel( + gochannel.Config{}, + watermillzap.NewLogger(l), + ) + + var done atomic.Bool + router.AddHandler("gtag", "in", pubSub, "out", pubSub, gtag.MessageHandler(func(payload *encoding.Payload, msg *message.Message) error { + expected := `{"consent":{},"campaign":{},"ecommerce":{},"client_hints":{},"protocol_version":"2","client_id":"C123456","richsstsse":"1","document_location":"https://foomo.org","document_title":"Home","is_debug":"1","event_name":"add_to_cart"}` + if !assert.JSONEq(t, expected, string(msg.Payload)) { + fmt.Println(string(msg.Payload)) + } + done.Store(true) + return nil + })) + + go func() { + assert.NoError(t, router.Run(context.TODO())) + }() + assert.Eventually(t, router.IsRunning, time.Second, 50*time.Millisecond) + + payload := encoding.Payload{ + Consent: encoding.Consent{}, + Campaign: encoding.Campaign{}, + ECommerce: encoding.ECommerce{}, + ClientHints: encoding.ClientHints{}, + ProtocolVersion: encoding.Set("2"), + TrackingID: nil, + GTMHashInfo: nil, + ClientID: encoding.Set("C123456"), + Richsstsse: encoding.Set("1"), + DocumentLocation: encoding.Set("https://foomo.org"), + DocumentTitle: encoding.Set("Home"), + DocumentReferrer: nil, + IsDebug: encoding.Set("1"), + EventName: encoding.Set(sesamy.EventNameAddToCart), + EventParameter: nil, + EventParameterNumber: nil, + UserID: nil, + SessionID: nil, + UserProperty: nil, + UserPropertyNumber: nil, + NonPersonalizedAds: nil, + SST: nil, + Remain: nil, + } + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, pubSub.Publish("in", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} + +func TestMPv2MessageHandler(t *testing.T) { + l := zaptest.NewLogger(t) + + router, err := message.NewRouter(message.RouterConfig{}, watermillzap.NewLogger(l)) + require.NoError(t, err) + defer router.Close() + + // Create pubSub + pubSub := gochannel.NewGoChannel( + gochannel.Config{}, + watermillzap.NewLogger(l), + ) + + var done atomic.Bool + router.AddHandler("gtag", "in", pubSub, "out", pubSub, gtag.MPv2MessageHandler) + router.AddNoPublisherHandler("mpv2", "out", pubSub, func(msg *message.Message) error { + expected := `{"client_id":"C123456","events":[{"name":"add_to_cart","params":{}}],"debug_mode":true}` + if !assert.JSONEq(t, expected, string(msg.Payload)) { + fmt.Println(string(msg.Payload)) + } + done.Store(true) + return nil + }) + + go func() { + assert.NoError(t, router.Run(context.TODO())) + }() + assert.Eventually(t, router.IsRunning, time.Second, 50*time.Millisecond) + + payload := encoding.Payload{ + Consent: encoding.Consent{}, + Campaign: encoding.Campaign{}, + ECommerce: encoding.ECommerce{}, + ClientHints: encoding.ClientHints{}, + ProtocolVersion: encoding.Set("2"), + TrackingID: nil, + GTMHashInfo: nil, + ClientID: encoding.Set("C123456"), + Richsstsse: encoding.Set("1"), + DocumentLocation: encoding.Set("https://foomo.org"), + DocumentTitle: encoding.Set("Home"), + DocumentReferrer: nil, + IsDebug: encoding.Set("1"), + EventName: encoding.Set(sesamy.EventNameAddToCart), + EventParameter: nil, + EventParameterNumber: nil, + UserID: nil, + SessionID: nil, + UserProperty: nil, + UserPropertyNumber: nil, + NonPersonalizedAds: nil, + SST: nil, + Remain: nil, + } + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, pubSub.Publish("in", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} diff --git a/integration/watermill/gtag/publisher.go b/integration/watermill/gtag/publisher.go index c67d84f..a0a81f9 100644 --- a/integration/watermill/gtag/publisher.go +++ b/integration/watermill/gtag/publisher.go @@ -19,13 +19,17 @@ var ( type ( Publisher struct { - l *zap.Logger - host string - path string - client *http.Client - closed bool + l *zap.Logger + host string + path string + client *http.Client + closed bool + middlewares []PublisherMiddleware + maxResponseCode int } - PublisherOption func(*Publisher) + PublisherOption func(*Publisher) + PublisherHandler func(l *zap.Logger, msg *message.Message) error + PublisherMiddleware func(next PublisherHandler) PublisherHandler // PublisherMarshalMessageFunc transforms the message into a HTTP request to be sent to the specified url. PublisherMarshalMessageFunc func(url string, msg *message.Message) (*http.Request, error) ) @@ -36,10 +40,11 @@ type ( func NewPublisher(l *zap.Logger, host string, opts ...PublisherOption) *Publisher { inst := &Publisher{ - l: l, - host: host, - path: "/g/collect", - client: http.DefaultClient, + l: l, + host: host, + path: "/g/collect", + client: http.DefaultClient, + maxResponseCode: http.StatusBadRequest, } for _, opt := range opts { opt(inst) @@ -63,6 +68,18 @@ func PublisherWithClient(v *http.Client) PublisherOption { } } +func PublisherWithMiddlewares(v ...PublisherMiddleware) PublisherOption { + return func(o *Publisher) { + o.middlewares = append(o.middlewares, v...) + } +} + +func PublisherWithMaxResponseCode(v int) PublisherOption { + return func(o *Publisher) { + o.maxResponseCode = v + } +} + // ------------------------------------------------------------------------------------------------ // ~ Getter // ------------------------------------------------------------------------------------------------ @@ -81,62 +98,76 @@ func (p *Publisher) Publish(topic string, messages ...*message.Message) error { } for _, msg := range messages { - var event *gtag.Payload - if err := json.Unmarshal(msg.Payload, &event); err != nil { - return err + // compose middlewares + next := p.handle + for _, middleware := range p.middlewares { + next = middleware(next) } - values, body, err := gtag.Encode(event) - if err != nil { + // run handler + if err := next(p.l.With( + zap.String("message_id", msg.UUID), + ), msg); err != nil { return err } + } - req, err := http.NewRequestWithContext(msg.Context(), http.MethodPost, fmt.Sprintf("%s%s?%s", p.host, p.path, gtag.EncodeValues(values)), body) - if err != nil { - return errors.Wrap(err, "failed to create request") - } + return nil +} - for s, s2 := range msg.Metadata { - req.Header.Set(s, s2) - } +func (p *Publisher) Close() error { + if p.closed { + return nil + } - l := p.l.With( - zap.String("message_id", msg.UUID), - ) + p.closed = true + return nil +} - if err := func() error { - resp, err := p.client.Do(req) - if err != nil { - return errors.Wrapf(err, "failed to publish message: %s", msg.UUID) - } - defer resp.Body.Close() +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ - l = l.With(zap.Int("http_status_code", resp.StatusCode)) +func (p *Publisher) handle(l *zap.Logger, msg *message.Message) error { + var event *gtag.Payload + if err := json.Unmarshal(msg.Payload, &event); err != nil { + return err + } - if resp.StatusCode >= http.StatusBadRequest { - if body, err := io.ReadAll(resp.Body); err == nil { - l = l.With(zap.String("http_response", string(body))) - } - l.Warn("server responded with error") - return errors.Wrap(ErrErrorResponse, resp.Status) - } + values, body, err := gtag.Encode(event) + if err != nil { + return err + } - l.Debug("message published") + req, err := http.NewRequestWithContext(msg.Context(), http.MethodPost, fmt.Sprintf("%s%s?%s", p.host, p.path, gtag.EncodeValues(values)), body) + if err != nil { + return errors.Wrap(err, "failed to create request") + } - return nil - }(); err != nil { - return err - } + for s, s2 := range msg.Metadata { + req.Header.Set(s, s2) } - return nil -} + if err := func() error { + resp, err := p.client.Do(req) + if err != nil { + return errors.Wrapf(err, "failed to publish message: %s", msg.UUID) + } + defer resp.Body.Close() + + l = l.With(zap.Int("http_status_code", resp.StatusCode)) + + if p.maxResponseCode > 0 && resp.StatusCode >= p.maxResponseCode { + if body, err := io.ReadAll(resp.Body); err == nil { + l = l.With(zap.String("http_response", string(body))) + } + return errors.Wrap(ErrErrorResponse, resp.Status) + } -func (p *Publisher) Close() error { - if p.closed { return nil + }(); err != nil { + return err } - p.closed = true return nil } diff --git a/integration/watermill/gtag/publisher_test.go b/integration/watermill/gtag/publisher_test.go new file mode 100644 index 0000000..e38f4a0 --- /dev/null +++ b/integration/watermill/gtag/publisher_test.go @@ -0,0 +1,68 @@ +package gtag_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/foomo/sesamy-go/integration/watermill/gtag" + encoding "github.com/foomo/sesamy-go/pkg/encoding/gtag" + "github.com/foomo/sesamy-go/pkg/sesamy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestPublisher(t *testing.T) { + l := zaptest.NewLogger(t) + + var done atomic.Bool + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expected := `_dbg=1&cid=C123456&dl=https%3A%2F%2Ffoomo.org&dt=Home&en=add_to_cart&v=2&richsstsse` + assert.Equal(t, expected, r.URL.RawQuery) + done.Store(true) + })) + + p := gtag.NewPublisher(l, s.URL) + + payload := encoding.Payload{ + Consent: encoding.Consent{}, + Campaign: encoding.Campaign{}, + ECommerce: encoding.ECommerce{}, + ClientHints: encoding.ClientHints{}, + ProtocolVersion: encoding.Set("2"), + TrackingID: nil, + GTMHashInfo: nil, + ClientID: encoding.Set("C123456"), + Richsstsse: encoding.Set("1"), + DocumentLocation: encoding.Set("https://foomo.org"), + DocumentTitle: encoding.Set("Home"), + DocumentReferrer: nil, + IsDebug: encoding.Set("1"), + EventName: encoding.Set(sesamy.EventNameAddToCart), + EventParameter: nil, + EventParameterNumber: nil, + UserID: nil, + SessionID: nil, + UserProperty: nil, + UserPropertyNumber: nil, + NonPersonalizedAds: nil, + SST: nil, + Remain: nil, + } + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + fmt.Println(string(jsonPayload)) + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, p.Publish("foo", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} diff --git a/integration/watermill/gtag/publishermiddleware.go b/integration/watermill/gtag/publishermiddleware.go new file mode 100644 index 0000000..5d2afba --- /dev/null +++ b/integration/watermill/gtag/publishermiddleware.go @@ -0,0 +1,14 @@ +package gtag + +import ( + "github.com/ThreeDotsLabs/watermill/message" + "go.uber.org/zap" +) + +func PublisherMiddlewareIgnoreError(next PublisherHandler) PublisherHandler { + return func(l *zap.Logger, msg *message.Message) error { + err := next(l, msg) + l.With(zap.Error(err)).Warn("ignoring error") + return nil + } +} diff --git a/integration/watermill/mpv2/metadata.go b/integration/watermill/mpv2/metadata.go new file mode 100644 index 0000000..c174b14 --- /dev/null +++ b/integration/watermill/mpv2/metadata.go @@ -0,0 +1,5 @@ +package mpv2 + +const ( + MetadataRequestQuery = "RequestQuery" +) diff --git a/integration/watermill/mpv2/publisher.go b/integration/watermill/mpv2/publisher.go index 738fdf0..7140f85 100644 --- a/integration/watermill/mpv2/publisher.go +++ b/integration/watermill/mpv2/publisher.go @@ -119,17 +119,20 @@ func (p *Publisher) handle(l *zap.Logger, msg *message.Message) error { return errors.Wrap(err, "failed to create request") } - for s, s2 := range msg.Metadata { - if s == "Cookie" { - for _, s3 := range strings.Split(s2, "; ") { + for key, value := range msg.Metadata { + switch key { + case "Cookie": + for _, s3 := range strings.Split(value, "; ") { val := strings.Split(s3, "=") req.AddCookie(&http.Cookie{ Name: val[0], Value: strings.Join(val[1:], "="), }) } - } else { - req.Header.Set(s, s2) + case MetadataRequestQuery: + req.URL.RawQuery = value + default: + req.Header.Set(key, value) } } @@ -146,12 +149,9 @@ func (p *Publisher) handle(l *zap.Logger, msg *message.Message) error { if body, err := io.ReadAll(resp.Body); err == nil { l = l.With(zap.String("http_response", string(body))) } - l.Warn("server responded with error") return errors.Wrap(ErrErrorResponse, resp.Status) } - l.Debug("message published") - return nil }(); err != nil { return err diff --git a/integration/watermill/mpv2/publisher_test.go b/integration/watermill/mpv2/publisher_test.go new file mode 100644 index 0000000..7aa289d --- /dev/null +++ b/integration/watermill/mpv2/publisher_test.go @@ -0,0 +1,67 @@ +package mpv2_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/foomo/sesamy-go/integration/watermill/mpv2" + encoding "github.com/foomo/sesamy-go/pkg/encoding/mpv2" + "github.com/foomo/sesamy-go/pkg/event" + "github.com/foomo/sesamy-go/pkg/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestPublisher(t *testing.T) { + l := zaptest.NewLogger(t) + + var done atomic.Bool + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + out, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + expected := `{"client_id":"C123456","user_id":"U123456","timestamp_micros":1727701064057701,"events":[{"name":"page_view","params":{"page_title":"Home","page_location":"https://foomo.org"}}],"debug_mode":true,"session_id":"S123456","engagement_time_msec":100}` + if !assert.JSONEq(t, expected, string(out)) { + fmt.Println(string(out)) + } + done.Store(true) + })) + + p := mpv2.NewPublisher(l, s.URL) + + payload := encoding.Payload[params.PageView]{ + ClientID: "C123456", + UserID: "U123456", + TimestampMicros: 1727701064057701, + UserProperties: nil, + Consent: nil, + Events: []sesamy.Event[params.PageView]{ + event.NewPageView(params.PageView{ + PageTitle: "Home", + PageLocation: "https://foomo.org", + }), + }, + UserData: nil, + DebugMode: true, + SessionID: "S123456", + EngagementTimeMSec: 100, + } + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, p.Publish("foo", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} diff --git a/integration/watermill/mpv2/publishermiddleware.go b/integration/watermill/mpv2/publishermiddleware.go index 412d573..79eae95 100644 --- a/integration/watermill/mpv2/publishermiddleware.go +++ b/integration/watermill/mpv2/publishermiddleware.go @@ -8,19 +8,41 @@ import ( "go.uber.org/zap" ) -func PublisherMiddlewareDebugMode(next PublisherHandler) PublisherHandler { +// PublisherMiddlewareIgnoreError ignores error responses from the gtm endpoint to prevent retries. +func PublisherMiddlewareIgnoreError(next PublisherHandler) PublisherHandler { + return func(l *zap.Logger, msg *message.Message) error { + err := next(l, msg) + l.With(zap.Error(err)).Warn("ignoring error") + return nil + } +} + +// PublisherMiddlewareEventParams moves the `debug_mode`, `session_id` & `engagement_time_msec` into the events params +// since this is required by the measurement protocol but make coding much more complex. That's why it's part of the payload +// in this library. +func PublisherMiddlewareEventParams(next PublisherHandler) PublisherHandler { return func(l *zap.Logger, msg *message.Message) error { var payload *mpv2.Payload[any] if err := json.Unmarshal(msg.Payload, &payload); err != nil { return err } - if payload.DebugMode { - for i, event := range payload.Events { - if params, ok := event.Params.(map[string]any); ok { + for i, event := range payload.Events { + if params, ok := event.Params.(map[string]any); ok { + if payload.DebugMode { params["debug_mode"] = "1" + payload.DebugMode = false + } + if len(payload.SessionID) > 0 { + params["session_id"] = payload.SessionID + payload.SessionID = "" + } + if payload.EngagementTimeMSec > 0 { + params["engagement_time_msec"] = payload.EngagementTimeMSec + payload.EngagementTimeMSec = 0 } - payload.Events[i] = event + event.Params = params } + payload.Events[i] = event out, err := json.Marshal(payload) if err != nil { diff --git a/integration/watermill/mpv2/publishermiddleware_test.go b/integration/watermill/mpv2/publishermiddleware_test.go new file mode 100644 index 0000000..89e9704 --- /dev/null +++ b/integration/watermill/mpv2/publishermiddleware_test.go @@ -0,0 +1,106 @@ +package mpv2_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + "time" + + "github.com/ThreeDotsLabs/watermill" + "github.com/ThreeDotsLabs/watermill/message" + "github.com/foomo/sesamy-go/integration/watermill/mpv2" + encoding "github.com/foomo/sesamy-go/pkg/encoding/mpv2" + "github.com/foomo/sesamy-go/pkg/event" + "github.com/foomo/sesamy-go/pkg/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestPublisherMiddlewareIgnoreError(t *testing.T) { + l := zaptest.NewLogger(t) + + var done atomic.Bool + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + done.Store(true) + })) + + p := mpv2.NewPublisher(l, s.URL, mpv2.PublisherWithMiddlewares(mpv2.PublisherMiddlewareIgnoreError)) + + payload := encoding.Payload[params.PageView]{ + ClientID: "C123456", + UserID: "U123456", + TimestampMicros: 1727701064057701, + UserProperties: nil, + Consent: nil, + Events: []sesamy.Event[params.PageView]{ + event.NewPageView(params.PageView{ + PageTitle: "Home", + PageLocation: "https://foomo.org", + }), + }, + UserData: nil, + DebugMode: true, + SessionID: "S123456", + EngagementTimeMSec: 100, + } + + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, p.Publish("foo", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} + +func TestPublisherMiddlewareEventParams(t *testing.T) { + l := zaptest.NewLogger(t) + + var done atomic.Bool + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + out, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + expected := `{"client_id":"C123456","user_id":"U123456","timestamp_micros":1727701064057701,"events":[{"name":"page_view","params":{"debug_mode":"1","engagement_time_msec":100,"page_location":"https://foomo.org","page_title":"Home","session_id":"S123456"}}]}` + if !assert.JSONEq(t, expected, string(out)) { + fmt.Println(string(out)) + } + done.Store(true) + })) + + p := mpv2.NewPublisher(l, s.URL, mpv2.PublisherWithMiddlewares(mpv2.PublisherMiddlewareEventParams)) + + payload := encoding.Payload[params.PageView]{ + ClientID: "C123456", + UserID: "U123456", + TimestampMicros: 1727701064057701, + UserProperties: nil, + Consent: nil, + Events: []sesamy.Event[params.PageView]{ + event.NewPageView(params.PageView{ + PageTitle: "Home", + PageLocation: "https://foomo.org", + }), + }, + UserData: nil, + DebugMode: true, + SessionID: "S123456", + EngagementTimeMSec: 100, + } + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + msg := message.NewMessage(watermill.NewUUID(), jsonPayload) + + require.NoError(t, p.Publish("foo", msg)) + + assert.Eventually(t, done.Load, time.Second, 50*time.Millisecond) +} diff --git a/integration/watermill/mpv2/subscriber.go b/integration/watermill/mpv2/subscriber.go index 543e66e..e9b0a8a 100644 --- a/integration/watermill/mpv2/subscriber.go +++ b/integration/watermill/mpv2/subscriber.go @@ -119,6 +119,10 @@ func (s *Subscriber) handle(l *zap.Logger, r *http.Request, payload *mpv2.Payloa l = l.With(zap.String("message_id", msg.UUID)) msg.SetContext(context.WithoutCancel(r.Context())) + // store query + msg.Metadata.Set(MetadataRequestQuery, r.URL.RawQuery) + + // store header for name, headers := range r.Header { msg.Metadata.Set(name, strings.Join(headers, ",")) } diff --git a/integration/watermill/mpv2/subscribermiddleware.go b/integration/watermill/mpv2/subscribermiddleware.go index de91fc4..6267415 100644 --- a/integration/watermill/mpv2/subscribermiddleware.go +++ b/integration/watermill/mpv2/subscribermiddleware.go @@ -7,17 +7,43 @@ import ( "github.com/foomo/sesamy-go/pkg/encoding/mpv2" "github.com/foomo/sesamy-go/pkg/session" + "github.com/pkg/errors" "go.uber.org/zap" ) +func SubscriberMiddlewareSessionID(measurementID string) SubscriberMiddleware { + measurementID = strings.Split(measurementID, "-")[1] + return func(next SubscriberHandler) SubscriberHandler { + return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { + if payload.SessionID == "" { + value, err := session.ParseGASessionID(r, measurementID) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + return errors.Wrap(err, "failed to parse client cookie") + } + payload.SessionID = value + } + return next(l, r, payload) + } + } +} + func SubscriberMiddlewareClientID(next SubscriberHandler) SubscriberHandler { return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { if payload.ClientID == "" { - clientID, err := session.ParseGAClientID(r) + value, err := session.ParseGAClientID(r) if err != nil { return err } - payload.ClientID = clientID + payload.ClientID = value + } + return next(l, r, payload) + } +} + +func SubscriberMiddlewareDebugMode(next SubscriberHandler) SubscriberHandler { + return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { + if !payload.DebugMode { + payload.DebugMode = session.IsGTMDebug(r) } return next(l, r, payload) } @@ -26,26 +52,23 @@ func SubscriberMiddlewareClientID(next SubscriberHandler) SubscriberHandler { func SubscriberMiddlewareUserID(cookieName string) SubscriberMiddleware { return func(next SubscriberHandler) SubscriberHandler { return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { - if cookie, err := r.Cookie(cookieName); err == nil { - payload.UserID = cookie.Value + if payload.UserID == "" { + value, err := r.Cookie(cookieName) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + return err + } + payload.UserID = value.Value } return next(l, r, payload) } } } -func SubscriberMiddlewareDebugMode(next SubscriberHandler) SubscriberHandler { - return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { - if session.IsGTMDebug(r) { - payload.DebugMode = true - } - return next(l, r, payload) - } -} - func SubscriberMiddlewareTimestamp(next SubscriberHandler) SubscriberHandler { return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error { - payload.TimestampMicros = time.Now().UnixMicro() + if payload.TimestampMicros == 0 { + payload.TimestampMicros = time.Now().UnixMicro() + } return next(l, r, payload) } } diff --git a/pkg/client/gtagmiddleware.go b/pkg/client/gtagmiddleware.go index a056069..d0a667d 100644 --- a/pkg/client/gtagmiddleware.go +++ b/pkg/client/gtagmiddleware.go @@ -58,11 +58,11 @@ func GTagMiddlewarClientID(next GTagHandler) GTagHandler { } } -func GTagMiddlewarSessionID(trackingID string) GTagMiddleware { - trackingID = strings.Split(trackingID, "-")[1] +func GTagMiddlewarSessionID(measurementID string) GTagMiddleware { + measurementID = strings.Split(measurementID, "-")[1] return func(next GTagHandler) GTagHandler { return func(r *http.Request, payload *gtag.Payload) error { - value, err := session.ParseGASessionID(r, trackingID) + value, err := session.ParseGASessionID(r, measurementID) if err != nil && !errors.Is(err, http.ErrNoCookie) { return errors.Wrap(err, "failed to parse session cookie") } diff --git a/pkg/client/mpv2.go b/pkg/client/mpv2.go index 1d02b06..8bc797a 100644 --- a/pkg/client/mpv2.go +++ b/pkg/client/mpv2.go @@ -16,10 +16,16 @@ import ( type ( MPv2 struct { - l *zap.Logger - path string - host string - cookies []string + l *zap.Logger + path string + host string + cookies []string + // To create a new secret, navigate in the Google Analytics UI to: + // Admin > Data Streams > choose your stream > Measurement Protocol > Create + apiSecret string + // Measurement ID. The identifier for a Data Stream. Found in the Google Analytics UI under: + // Admin > Data Streams > choose your stream > Measurement ID + measurementID string protocolVersion string httpClient *http.Client middlewares []MPv2Middleware @@ -51,6 +57,18 @@ func MPv2WithCookies(v ...string) MPv2Option { } } +func MPv2WithAPISecret(v string) MPv2Option { + return func(o *MPv2) { + o.apiSecret = v + } +} + +func MPv2WithMeasurementID(v string) MPv2Option { + return func(o *MPv2) { + o.measurementID = v + } +} + func MPv2WithMiddlewares(v ...MPv2Middleware) MPv2Option { return func(o *MPv2) { o.middlewares = append(o.middlewares, v...) @@ -125,6 +143,16 @@ func (c *MPv2) SendRaw(r *http.Request, payload *mpv2.Payload[any]) error { return errors.Wrap(err, "failed to create request") } + // query + qry := req.URL.Query() + if len(c.apiSecret) > 0 { + qry.Add("api_secret", c.apiSecret) + } + if len(c.measurementID) > 0 { + qry.Add("measurement_id", c.measurementID) + } + req.URL.RawQuery = qry.Encode() + // TODO valiate: copy headers req.Header = r.Header.Clone() diff --git a/pkg/client/mpv2middleware.go b/pkg/client/mpv2middleware.go index 6326941..b52db65 100644 --- a/pkg/client/mpv2middleware.go +++ b/pkg/client/mpv2middleware.go @@ -2,19 +2,37 @@ package client import ( "net/http" + "strings" + "time" "github.com/foomo/sesamy-go/pkg/encoding/mpv2" "github.com/foomo/sesamy-go/pkg/session" "github.com/pkg/errors" ) +func MPv2MiddlewarSessionID(measurementID string) MPv2Middleware { + measurementID = strings.Split(measurementID, "-")[1] + return func(next MPv2Handler) MPv2Handler { + return func(r *http.Request, payload *mpv2.Payload[any]) error { + if payload.SessionID == "" { + value, err := session.ParseGASessionID(r, measurementID) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + return errors.Wrap(err, "failed to parse client cookie") + } + payload.SessionID = value + } + return next(r, payload) + } + } +} + func MPv2MiddlewarClientID(next MPv2Handler) MPv2Handler { return func(r *http.Request, payload *mpv2.Payload[any]) error { - value, err := session.ParseGAClientID(r) - if err != nil && !errors.Is(err, http.ErrNoCookie) { - return errors.Wrap(err, "failed to parse client cookie") - } - if value != "" { + if payload.ClientID == "" { + value, err := session.ParseGAClientID(r) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + return errors.Wrap(err, "failed to parse client cookie") + } payload.ClientID = value } return next(r, payload) @@ -23,8 +41,32 @@ func MPv2MiddlewarClientID(next MPv2Handler) MPv2Handler { func MPv2MiddlewarDebugMode(next MPv2Handler) MPv2Handler { return func(r *http.Request, payload *mpv2.Payload[any]) error { - if session.IsGTMDebug(r) { - payload.DebugMode = true + if !payload.DebugMode { + payload.DebugMode = session.IsGTMDebug(r) + } + return next(r, payload) + } +} + +func MPv2MiddlewareUserID(cookieName string) MPv2Middleware { + return func(next MPv2Handler) MPv2Handler { + return func(r *http.Request, payload *mpv2.Payload[any]) error { + if payload.UserID == "" { + value, err := r.Cookie(cookieName) + if err != nil && !errors.Is(err, http.ErrNoCookie) { + return err + } + payload.UserID = value.Value + } + return next(r, payload) + } + } +} + +func MPv2MiddlewareTimestamp(next MPv2Handler) MPv2Handler { + return func(r *http.Request, payload *mpv2.Payload[any]) error { + if payload.TimestampMicros == 0 { + payload.TimestampMicros = time.Now().UnixMicro() } return next(r, payload) } diff --git a/pkg/encoding/gtag/consent.go b/pkg/encoding/gtag/consent.go index 81a0cb4..c1ab4d8 100644 --- a/pkg/encoding/gtag/consent.go +++ b/pkg/encoding/gtag/consent.go @@ -1,5 +1,9 @@ package gtag +import ( + "strings" +) + type Consent struct { // Current Google Consent Status. Format 'G1'+'AdsStorageBoolStatus'`+'AnalyticsStorageBoolStatus' // Example: G101 @@ -14,3 +18,29 @@ type Consent struct { // Example: G111 GoogleConsentDefault *string `json:"google_consent_default,omitempty" gtag:"gcd,omitempty"` } + +// ------------------------------------------------------------------------------------------------ +// ~ Public methods +// ------------------------------------------------------------------------------------------------ + +func (c Consent) AdStorage() bool { + if c.GoogleConsentUpdate != nil { + gcs := *c.GoogleConsentUpdate + if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 { + return gcs[2:3] == "1" + } + return false + } + return true +} + +func (c Consent) AnalyticsStorage() bool { + if c.GoogleConsentUpdate != nil { + gcs := *c.GoogleConsentUpdate + if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 { + return gcs[3:4] == "1" + } + return false + } + return true +} diff --git a/pkg/encoding/mpv2/consent.go b/pkg/encoding/mpv2/consent.go index 8587858..2a78551 100644 --- a/pkg/encoding/mpv2/consent.go +++ b/pkg/encoding/mpv2/consent.go @@ -1,6 +1,8 @@ package mpv2 -type Consent struct { - AdUserData *string `json:"ad_user_data,omitempty"` - AdPersonalization *string `json:"ad_personalization,omitempty"` -} +type Consent string + +const ( + ConsentDenied Consent = "DENIED" + ConsentGranted Consent = "GRANTED" +) diff --git a/pkg/encoding/mpv2/consentdata.go b/pkg/encoding/mpv2/consentdata.go new file mode 100644 index 0000000..fe32a3a --- /dev/null +++ b/pkg/encoding/mpv2/consentdata.go @@ -0,0 +1,11 @@ +package mpv2 + +type ConsentData struct { + AdStorage *Consent `json:"ad_storage,omitempty"` + AdUserData *Consent `json:"ad_user_data,omitempty"` + AdPersonalization *Consent `json:"ad_personalization,omitempty"` + AnalyticsStorage *Consent `json:"analytics_storage,omitempty"` + FunctionalityStorage *Consent `json:"functionality_storage,omitempty"` + PersonalizationStorage *Consent `json:"personalization_storage,omitempty"` + SecurityStorage *Consent `json:"security_storage,omitempty"` +} diff --git a/pkg/encoding/mpv2/payload.go b/pkg/encoding/mpv2/payload.go index a61531a..4abcf25 100644 --- a/pkg/encoding/mpv2/payload.go +++ b/pkg/encoding/mpv2/payload.go @@ -4,15 +4,16 @@ import ( "github.com/foomo/sesamy-go/pkg/sesamy" ) +// https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_post_body type Payload[P any] struct { - ClientID string `json:"client_id,omitempty"` - UserID string `json:"user_id,omitempty"` - TimestampMicros int64 `json:"timestamp_micros,omitempty"` - // Reserved user property names: https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_user_property_names + ClientID string `json:"client_id,omitempty"` + UserID string `json:"user_id,omitempty"` + TimestampMicros int64 `json:"timestamp_micros,omitempty"` UserProperties map[string]any `json:"user_properties,omitempty"` - Consent *Consent `json:"consent,omitempty"` - NonPersonalizedAds bool `json:"non_personalized_ads,omitempty"` + Consent *ConsentData `json:"consent,omitempty"` Events []sesamy.Event[P] `json:"events,omitempty"` UserData *UserData `json:"user_data,omitempty"` DebugMode bool `json:"debug_mode,omitempty"` + SessionID string `json:"session_id,omitempty"` + EngagementTimeMSec int64 `json:"engagement_time_msec,omitempty"` } diff --git a/pkg/encoding/mpv2/payload_test.go b/pkg/encoding/mpv2/payload_test.go new file mode 100644 index 0000000..6abf9e9 --- /dev/null +++ b/pkg/encoding/mpv2/payload_test.go @@ -0,0 +1,43 @@ +package mpv2_test + +import ( + "encoding/json" + "testing" + + "github.com/foomo/sesamy-go/pkg/encoding/mpv2" + "github.com/foomo/sesamy-go/pkg/event" + "github.com/foomo/sesamy-go/pkg/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPayload(t *testing.T) { + v := mpv2.Payload[params.PageView]{ + ClientID: "C123456", + UserID: "U123456", + TimestampMicros: 1727701064057701, + UserProperties: nil, + Consent: nil, + Events: []sesamy.Event[params.PageView]{ + event.NewPageView(params.PageView{ + PageTitle: "Home", + PageLocation: "https://foomo.org", + }), + }, + UserData: nil, + DebugMode: true, + SessionID: "S123456", + EngagementTimeMSec: 100, + } + + out, err := json.Marshal(v) + require.NoError(t, err) + expected := `{"debug_mode":true,"session_id":"S123456","engagement_time_msec":100,"client_id":"C123456","user_id":"U123456","timestamp_micros":1727701064057701,"events":[{"name":"page_view","params":{"page_title":"Home","page_location":"https://foomo.org"}}]}` + assert.JSONEq(t, expected, string(out)) + + var in mpv2.Payload[params.PageView] + err = json.Unmarshal(out, &in) + require.NoError(t, err) + assert.Equal(t, v, in) +} diff --git a/pkg/encoding/mpv2encode/gtag.go b/pkg/encoding/mpv2encode/gtag.go index d58fe9a..3ca1fab 100644 --- a/pkg/encoding/mpv2encode/gtag.go +++ b/pkg/encoding/mpv2encode/gtag.go @@ -12,9 +12,8 @@ import ( func GTag[P any](source mpv2.Payload[P], target any) error { targetData := map[string]any{ - "client_id": source.ClientID, - "user_id": source.UserID, - "non_personalized_ads": source.NonPersonalizedAds, + "client_id": source.ClientID, + "user_id": source.UserID, } { // user_property diff --git a/pkg/provider/cookiebot/client/mpv2middleware.go b/pkg/provider/cookiebot/client/mpv2middleware.go new file mode 100644 index 0000000..06e2830 --- /dev/null +++ b/pkg/provider/cookiebot/client/mpv2middleware.go @@ -0,0 +1,66 @@ +package client + +import ( + "errors" + "net/http" + "net/url" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/foomo/sesamy-go/pkg/client" + "github.com/foomo/sesamy-go/pkg/encoding/mpv2" + "github.com/foomo/sesamy-go/pkg/provider/cookiebot" + "go.uber.org/zap" + "gopkg.in/yaml.v2" +) + +func MPv2MiddlewarConsent(l *zap.Logger) client.MPv2Middleware { + return func(next client.MPv2Handler) client.MPv2Handler { + return func(r *http.Request, payload *mpv2.Payload[any]) error { + cookie, err := r.Cookie(cookiebot.CookieName) + if errors.Is(err, http.ErrNoCookie) { + return next(r, payload) + } else if err != nil { + l.With(zap.Error(err)).Warn("failed to retrieve cookie bot cookie") + return next(r, payload) + } else if cookie.Value == "" { + l.With(zap.Error(err)).Warn("empty cookie bot cookie") + return next(r, payload) + } + + data, err := url.QueryUnescape(cookie.Value) + if err != nil { + l.With(zap.Error(err), zap.String("value", cookie.Value)).Warn("failed to unescape cookie bot cookie") + return next(r, payload) + } + + var value cookiebot.Cookie + if err := yaml.Unmarshal([]byte(strings.ReplaceAll(data, ":", ": ")), &value); err != nil { + l.With(zap.Error(err), zap.String("value", data)).Warn("failed to unmarshal cookie bot cookie") + return next(r, payload) + } + spew.Dump(value) + + consent := func(b bool) *mpv2.Consent { + ret := mpv2.ConsentDenied + if b { + ret = mpv2.ConsentGranted + } + return &ret + } + + payload.Consent = &mpv2.ConsentData{ + AdStorage: consent(value.Marketing), + AdUserData: consent(value.Marketing), + AdPersonalization: consent(value.Marketing), + PersonalizationStorage: consent(value.Marketing), + AnalyticsStorage: consent(value.Statistics), + FunctionalityStorage: consent(value.Necessary), + SecurityStorage: consent(value.Necessary), + } + spew.Dump(payload) + + return next(r, payload) + } + } +} diff --git a/pkg/provider/cookiebot/cookie.go b/pkg/provider/cookiebot/cookie.go new file mode 100644 index 0000000..1503fb1 --- /dev/null +++ b/pkg/provider/cookiebot/cookie.go @@ -0,0 +1,16 @@ +package cookiebot + +const CookieName = "CookieConsent" + +// {stamp:'VLZnHUKBPLqZCJyClLLmnGglmUPeZsGxrmiAEZ48i7UH39ptKHY4MA==',necessary:true,preferences:true,statistics:true,marketing:true,method:'explicit',ver:1,utc:1724770548958,region:'de'} +type Cookie struct { + Stamp string `json:"stamp" yaml:"stamp"` + Necessary bool `json:"necessary" yaml:"necessary"` + Preferences bool `json:"preferences" yaml:"preferences"` + Statistics bool `json:"statistics" yaml:"statistics"` + Marketing bool `json:"marketing" yaml:"marketing"` + Method string `json:"method" yaml:"method"` + Version string `json:"ver" yaml:"ver"` + UTC int `json:"utc" yaml:"utc"` + Region string `json:"region" yaml:"region"` +} diff --git a/pkg/provider/tracify/event/params/tracifyaddtocart.go b/pkg/provider/tracify/event/params/tracifyaddtocart.go new file mode 100644 index 0000000..bfea7cd --- /dev/null +++ b/pkg/provider/tracify/event/params/tracifyaddtocart.go @@ -0,0 +1,11 @@ +package params + +import ( + "github.com/foomo/gostandards/iso4217" +) + +type TracifyAddToCart[I any] struct { + Currency iso4217.Currency `json:"currency,omitempty"` + Value float64 `json:"value,omitempty"` + Items []I `json:"items,omitempty"` +} diff --git a/pkg/provider/tracify/event/params/tracifyconversion.go b/pkg/provider/tracify/event/params/tracifyconversion.go new file mode 100644 index 0000000..9d499d9 --- /dev/null +++ b/pkg/provider/tracify/event/params/tracifyconversion.go @@ -0,0 +1,11 @@ +package params + +import ( + "github.com/foomo/gostandards/iso4217" +) + +type TracifyConversion struct { + Currency iso4217.Currency `json:"currency,omitempty"` + Value float64 `json:"value,omitempty"` + ConversionID string `json:"conversion_id,omitempty"` +} diff --git a/pkg/provider/tracify/event/params/tracifypagesview.go b/pkg/provider/tracify/event/params/tracifypagesview.go new file mode 100644 index 0000000..d741479 --- /dev/null +++ b/pkg/provider/tracify/event/params/tracifypagesview.go @@ -0,0 +1,5 @@ +package params + +type TracifyPageView struct { + PageLocation string `json:"page_location,omitempty"` +} diff --git a/pkg/provider/tracify/event/params/tracifyproductview.go b/pkg/provider/tracify/event/params/tracifyproductview.go new file mode 100644 index 0000000..605957f --- /dev/null +++ b/pkg/provider/tracify/event/params/tracifyproductview.go @@ -0,0 +1,5 @@ +package params + +type TracifyProductView struct { + PageLocation string `json:"page_location,omitempty"` +} diff --git a/pkg/provider/tracify/event/params/tracifypurchase.go b/pkg/provider/tracify/event/params/tracifypurchase.go new file mode 100644 index 0000000..cccbe45 --- /dev/null +++ b/pkg/provider/tracify/event/params/tracifypurchase.go @@ -0,0 +1,12 @@ +package params + +import ( + "github.com/foomo/gostandards/iso4217" +) + +type TracifyPurchase[I any] struct { + Currency iso4217.Currency `json:"currency,omitempty"` + Value float64 `json:"value,omitempty"` + TransactionID string `json:"transaction_id,omitempty"` + Items []I `json:"items,omitempty"` +} diff --git a/pkg/provider/tracify/event/tracifyaddtocart.go b/pkg/provider/tracify/event/tracifyaddtocart.go new file mode 100644 index 0000000..9c8a8c7 --- /dev/null +++ b/pkg/provider/tracify/event/tracifyaddtocart.go @@ -0,0 +1,15 @@ +package event + +import ( + sesamyparams "github.com/foomo/sesamy-go/pkg/event/params" + "github.com/foomo/sesamy-go/pkg/provider/tracify/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" +) + +const EventNameTracifyAddToCart sesamy.EventName = "tracify_add_to_cart" + +type TracifyAddToCart sesamy.Event[params.TracifyAddToCart[sesamyparams.Item]] + +func NewTracifyAddToCart(p params.TracifyAddToCart[sesamyparams.Item]) sesamy.Event[params.TracifyAddToCart[sesamyparams.Item]] { + return sesamy.NewEvent(EventNameTracifyAddToCart, p) +} diff --git a/pkg/provider/tracify/event/tracifyconversion.go b/pkg/provider/tracify/event/tracifyconversion.go new file mode 100644 index 0000000..ddff343 --- /dev/null +++ b/pkg/provider/tracify/event/tracifyconversion.go @@ -0,0 +1,14 @@ +package event + +import ( + "github.com/foomo/sesamy-go/pkg/provider/tracify/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" +) + +const EventNameTracifyConversion sesamy.EventName = "tracify_conversion" + +type TracifyConversion sesamy.Event[params.TracifyConversion] + +func NewTracifyConversion(p params.TracifyConversion) sesamy.Event[params.TracifyConversion] { + return sesamy.NewEvent(EventNameTracifyConversion, p) +} diff --git a/pkg/provider/tracify/event/tracifypageview.go b/pkg/provider/tracify/event/tracifypageview.go new file mode 100644 index 0000000..27c5911 --- /dev/null +++ b/pkg/provider/tracify/event/tracifypageview.go @@ -0,0 +1,14 @@ +package event + +import ( + "github.com/foomo/sesamy-go/pkg/provider/tracify/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" +) + +const EventNameTracifyPageView sesamy.EventName = "tracify_page_view" + +type TracifyPageView sesamy.Event[params.TracifyPageView] + +func NewTracifyPageView(p params.TracifyPageView) sesamy.Event[params.TracifyPageView] { + return sesamy.NewEvent(EventNameTracifyPageView, p) +} diff --git a/pkg/provider/tracify/event/tracifyproductview.go b/pkg/provider/tracify/event/tracifyproductview.go new file mode 100644 index 0000000..3552a5b --- /dev/null +++ b/pkg/provider/tracify/event/tracifyproductview.go @@ -0,0 +1,14 @@ +package event + +import ( + "github.com/foomo/sesamy-go/pkg/provider/tracify/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" +) + +const EventNameTracifyProductView sesamy.EventName = "tracify_product_view" + +type TracifyProductView sesamy.Event[params.TracifyProductView] + +func NewTracifyProductView(p params.TracifyProductView) sesamy.Event[params.TracifyProductView] { + return sesamy.NewEvent(EventNameTracifyProductView, p) +} diff --git a/pkg/provider/tracify/event/tracifypurchase.go b/pkg/provider/tracify/event/tracifypurchase.go new file mode 100644 index 0000000..a98cf44 --- /dev/null +++ b/pkg/provider/tracify/event/tracifypurchase.go @@ -0,0 +1,15 @@ +package event + +import ( + sesamyparams "github.com/foomo/sesamy-go/pkg/event/params" + "github.com/foomo/sesamy-go/pkg/provider/tracify/event/params" + "github.com/foomo/sesamy-go/pkg/sesamy" +) + +const EventNameTracifyPurchase sesamy.EventName = "tracify_purchase" + +type TracifyPurchase sesamy.Event[params.TracifyPurchase[sesamyparams.Item]] + +func NewTracifyPurchase(p params.TracifyPurchase[sesamyparams.Item]) sesamy.Event[params.TracifyPurchase[sesamyparams.Item]] { + return sesamy.NewEvent(EventNameTracifyPurchase, p) +}