mirror of
https://github.com/foomo/sesamy-go.git
synced 2025-10-16 12:35:43 +00:00
commit
ad3a673307
@ -151,9 +151,16 @@ func (l *Loki) Write(payload mpv2.Payload[any]) {
|
|||||||
l.l.Warn("buffer size reached", zap.Int("size", l.bufferSize))
|
l.l.Warn("buffer size reached", zap.Int("size", l.bufferSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timestamp time.Time
|
||||||
|
if payload.TimestampMicros > 0 {
|
||||||
|
timestamp = time.UnixMicro(payload.TimestampMicros)
|
||||||
|
} else {
|
||||||
|
timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
l.entries <- logproto.Entry{
|
l.entries <- logproto.Entry{
|
||||||
Line: string(lineBytes),
|
Line: string(lineBytes),
|
||||||
Timestamp: time.UnixMicro(payload.TimestampMicros),
|
Timestamp: timestamp,
|
||||||
StructuredMetadata: push.LabelsAdapter{
|
StructuredMetadata: push.LabelsAdapter{
|
||||||
{
|
{
|
||||||
Name: "event_name",
|
Name: "event_name",
|
||||||
|
|||||||
42
integration/loki/middleware.go
Normal file
42
integration/loki/middleware.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/gtagencode"
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
|
gtaghttp "github.com/foomo/sesamy-go/pkg/http/gtag"
|
||||||
|
mpv2http "github.com/foomo/sesamy-go/pkg/http/mpv2"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GTagMiddleware(loki *Loki) gtaghttp.Middleware {
|
||||||
|
return func(next gtaghttp.MiddlewareHandler) gtaghttp.MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
|
err := next(l, w, r, payload)
|
||||||
|
if err != nil {
|
||||||
|
// encode to mpv2
|
||||||
|
var mpv2Payload mpv2.Payload[any]
|
||||||
|
if err := gtagencode.MPv2(*payload, &mpv2Payload); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to encode gtag to mpv2")
|
||||||
|
}
|
||||||
|
loki.Write(mpv2Payload)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MPv2Middleware(loki *Loki) mpv2http.Middleware {
|
||||||
|
return func(next mpv2http.MiddlewareHandler) mpv2http.MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
err := next(l, w, r, payload)
|
||||||
|
if err != nil {
|
||||||
|
loki.Write(*payload)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -99,7 +99,7 @@ func TestMPv2MessageHandler(t *testing.T) {
|
|||||||
var done atomic.Bool
|
var done atomic.Bool
|
||||||
router.AddHandler("gtag", "in", pubSub, "out", pubSub, gtag.MPv2MessageHandler)
|
router.AddHandler("gtag", "in", pubSub, "out", pubSub, gtag.MPv2MessageHandler)
|
||||||
router.AddNoPublisherHandler("mpv2", "out", pubSub, func(msg *message.Message) error {
|
router.AddNoPublisherHandler("mpv2", "out", pubSub, func(msg *message.Message) error {
|
||||||
expected := `{"client_id":"C123456","events":[{"name":"add_to_cart","params":{}}],"debug_mode":true}`
|
expected := `{"client_id":"C123456","consent":{"ad_user_data":"GRANTED","ad_personalization":"GRANTED","analytics_storage":"GRANTED"},"events":[{"name":"add_to_cart","params":{"page_location":"https://foomo.org","page_title":"Home"}}],"debug_mode":true}`
|
||||||
if !assert.JSONEq(t, expected, string(msg.Payload)) {
|
if !assert.JSONEq(t, expected, string(msg.Payload)) {
|
||||||
fmt.Println(string(msg.Payload))
|
fmt.Println(string(msg.Payload))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,15 +3,13 @@ package gtag
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ThreeDotsLabs/watermill"
|
"github.com/ThreeDotsLabs/watermill"
|
||||||
"github.com/ThreeDotsLabs/watermill/message"
|
"github.com/ThreeDotsLabs/watermill/message"
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||||
|
gtaghttp "github.com/foomo/sesamy-go/pkg/http/gtag"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -22,12 +20,10 @@ type (
|
|||||||
uuidFunc func() string
|
uuidFunc func() string
|
||||||
messages chan *message.Message
|
messages chan *message.Message
|
||||||
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
||||||
middlewares []SubscriberMiddleware
|
middlewares []gtaghttp.Middleware
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
SubscriberOption func(*Subscriber)
|
SubscriberOption func(*Subscriber)
|
||||||
SubscriberHandler func(l *zap.Logger, r *http.Request, payload *gtag.Payload) error
|
|
||||||
SubscriberMiddleware func(next SubscriberHandler) SubscriberHandler
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
@ -46,7 +42,7 @@ func SubscriberWithMessageFunc(v func(l *zap.Logger, r *http.Request, msg *messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscriberWithMiddlewares(v ...SubscriberMiddleware) SubscriberOption {
|
func SubscriberWithMiddlewares(v ...gtaghttp.Middleware) SubscriberOption {
|
||||||
return func(o *Subscriber) {
|
return func(o *Subscriber) {
|
||||||
o.middlewares = append(o.middlewares, v...)
|
o.middlewares = append(o.middlewares, v...)
|
||||||
}
|
}
|
||||||
@ -69,52 +65,8 @@ func NewSubscriber(l *zap.Logger, opts ...SubscriberOption) *Subscriber {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
var values url.Values
|
// retrieve payload
|
||||||
|
payload := gtaghttp.Handler(w, r)
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
values = r.URL.Query()
|
|
||||||
case http.MethodPost:
|
|
||||||
values = r.URL.Query()
|
|
||||||
|
|
||||||
// read request body
|
|
||||||
out, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("failed to read body: %s", err.Error()), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// append request body to query
|
|
||||||
if len(out) > 0 {
|
|
||||||
v, err := url.ParseQuery(string(out))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, fmt.Sprintf("failed to parse extended url: %s", err.Error()), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for s2, i := range v {
|
|
||||||
values.Set(s2, i[0])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
values = r.URL.Query()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmarshal event
|
|
||||||
var payload *gtag.Payload
|
|
||||||
if err := gtag.Decode(values, &payload); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate
|
|
||||||
if payload.EventName == nil || payload.EventName.String() == "" {
|
|
||||||
http.Error(w, "missing event name", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// compose middlewares
|
// compose middlewares
|
||||||
next := s.handle
|
next := s.handle
|
||||||
@ -123,13 +75,13 @@ func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run handler
|
// run handler
|
||||||
if err := next(s.l, r, payload); err != nil {
|
if err := next(s.l, w, r, payload); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) handle(l *zap.Logger, r *http.Request, payload *gtag.Payload) error {
|
func (s *Subscriber) handle(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
// marshal message payload
|
// marshal message payload
|
||||||
data, err := json.Marshal(payload)
|
data, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,40 +0,0 @@
|
|||||||
package gtag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SubscriberMiddlewareUserID(cookieName string) SubscriberMiddleware {
|
|
||||||
return func(next SubscriberHandler) SubscriberHandler {
|
|
||||||
return func(l *zap.Logger, r *http.Request, payload *gtag.Payload) error {
|
|
||||||
if cookie, err := r.Cookie(cookieName); err == nil {
|
|
||||||
payload.UserID = gtag.Set(cookie.Value)
|
|
||||||
}
|
|
||||||
return next(l, r, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriberMiddlewareLogger(next SubscriberHandler) SubscriberHandler {
|
|
||||||
return func(l *zap.Logger, r *http.Request, payload *gtag.Payload) error {
|
|
||||||
if spanCtx := trace.SpanContextFromContext(r.Context()); spanCtx.IsValid() && spanCtx.IsSampled() {
|
|
||||||
l = l.With(zap.String("trace_id", spanCtx.TraceID().String()), zap.String("span_id", spanCtx.SpanID().String()))
|
|
||||||
}
|
|
||||||
l = l.With(
|
|
||||||
zap.String("event_name", gtag.GetDefault(payload.EventName, "-").String()),
|
|
||||||
zap.String("event_user_id", gtag.GetDefault(payload.UserID, "-")),
|
|
||||||
zap.String("event_session_id", gtag.GetDefault(payload.SessionID, "-")),
|
|
||||||
)
|
|
||||||
err := next(l, r, payload)
|
|
||||||
if err != nil {
|
|
||||||
l.Error("handled event", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
l.Info("handled event")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/ThreeDotsLabs/watermill"
|
"github.com/ThreeDotsLabs/watermill"
|
||||||
"github.com/ThreeDotsLabs/watermill/message"
|
"github.com/ThreeDotsLabs/watermill/message"
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
|
mpv2http "github.com/foomo/sesamy-go/pkg/http/mpv2"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -19,12 +20,10 @@ type (
|
|||||||
uuidFunc func() string
|
uuidFunc func() string
|
||||||
messages chan *message.Message
|
messages chan *message.Message
|
||||||
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
||||||
middlewares []SubscriberMiddleware
|
middlewares []mpv2http.Middleware
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
SubscriberOption func(*Subscriber)
|
SubscriberOption func(*Subscriber)
|
||||||
SubscriberHandler func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error
|
|
||||||
SubscriberMiddleware func(next SubscriberHandler) SubscriberHandler
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
@ -43,7 +42,7 @@ func SubscriberWithMessageFunc(v func(l *zap.Logger, r *http.Request, msg *messa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscriberWithMiddlewares(v ...SubscriberMiddleware) SubscriberOption {
|
func SubscriberWithMiddlewares(v ...mpv2http.Middleware) SubscriberOption {
|
||||||
return func(o *Subscriber) {
|
return func(o *Subscriber) {
|
||||||
o.middlewares = append(o.middlewares, v...)
|
o.middlewares = append(o.middlewares, v...)
|
||||||
}
|
}
|
||||||
@ -70,30 +69,8 @@ func NewSubscriber(l *zap.Logger, opts ...SubscriberOption) *Subscriber {
|
|||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
// retrieve payload
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
payload := mpv2http.Handler(w, r)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read request body
|
|
||||||
var payload *mpv2.Payload[any]
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&payload)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate required fields
|
|
||||||
if len(payload.Events) == 0 {
|
|
||||||
http.Error(w, "missing events", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, event := range payload.Events {
|
|
||||||
if event.Name == "" {
|
|
||||||
http.Error(w, "missing event name", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// compose middlewares
|
// compose middlewares
|
||||||
next := s.handle
|
next := s.handle
|
||||||
@ -102,13 +79,13 @@ func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// run handler
|
// run handler
|
||||||
if err := next(s.l, r, payload); err != nil {
|
if err := next(s.l, w, r, payload); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Subscriber) handle(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error {
|
func (s *Subscriber) handle(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
// marshal message payload
|
// marshal message payload
|
||||||
jsonPayload, err := json.Marshal(payload)
|
jsonPayload, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,101 +0,0 @@
|
|||||||
package mpv2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
|
||||||
"github.com/foomo/sesamy-go/pkg/session"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
"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 err
|
|
||||||
}
|
|
||||||
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 == "" {
|
|
||||||
value, err := session.ParseGAClientID(r)
|
|
||||||
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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 && session.IsGTMDebug(r) {
|
|
||||||
payload.DebugMode = true
|
|
||||||
}
|
|
||||||
return next(l, r, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriberMiddlewareUserID(cookieName string) SubscriberMiddleware {
|
|
||||||
return func(next SubscriberHandler) SubscriberHandler {
|
|
||||||
return func(l *zap.Logger, 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(l, r, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriberMiddlewareTimestamp(next SubscriberHandler) SubscriberHandler {
|
|
||||||
return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error {
|
|
||||||
if payload.TimestampMicros == 0 {
|
|
||||||
payload.TimestampMicros = time.Now().UnixMicro()
|
|
||||||
}
|
|
||||||
return next(l, r, payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SubscriberMiddlewareLogger(next SubscriberHandler) SubscriberHandler {
|
|
||||||
return func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error {
|
|
||||||
eventNames := make([]string, len(payload.Events))
|
|
||||||
for i, event := range payload.Events {
|
|
||||||
eventNames[i] = event.Name.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if spanCtx := trace.SpanContextFromContext(r.Context()); spanCtx.IsValid() && spanCtx.IsSampled() {
|
|
||||||
l = l.With(zap.String("trace_id", spanCtx.TraceID().String()), zap.String("span_id", spanCtx.SpanID().String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
l = l.With(
|
|
||||||
zap.String("event_names", strings.Join(eventNames, ",")),
|
|
||||||
zap.String("event_user_id", payload.UserID),
|
|
||||||
)
|
|
||||||
|
|
||||||
err := next(l, r, payload)
|
|
||||||
if err != nil {
|
|
||||||
l.Error("handled event", zap.Error(err))
|
|
||||||
} else {
|
|
||||||
l.Info("handled event")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
"github.com/foomo/sesamy-go/pkg/sesamy"
|
"github.com/foomo/sesamy-go/pkg/sesamy"
|
||||||
@ -110,16 +109,14 @@ func (c *MPv2) HTTPClient() *http.Client {
|
|||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
func (c *MPv2) Collect(r *http.Request, events ...sesamy.AnyEvent) error {
|
func (c *MPv2) Collect(r *http.Request, events ...sesamy.AnyEvent) error {
|
||||||
anyEvents := make([]sesamy.Event[any], len(events))
|
payload := mpv2.NewPayload[any]()
|
||||||
for i, event := range events {
|
for _, event := range events {
|
||||||
anyEvents[i] = event.AnyEvent()
|
payload.Events = append(payload.Events, event.AnyEvent())
|
||||||
}
|
|
||||||
|
|
||||||
payload := &mpv2.Payload[any]{
|
|
||||||
Events: anyEvents,
|
|
||||||
TimestampMicros: time.Now().UnixMicro(),
|
|
||||||
}
|
}
|
||||||
|
return c.SendPayload(r, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MPv2) SendPayload(r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
next := c.SendRaw
|
next := c.SendRaw
|
||||||
for _, middleware := range c.middlewares {
|
for _, middleware := range c.middlewares {
|
||||||
next = middleware(next)
|
next = middleware(next)
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
"github.com/foomo/sesamy-go/pkg/session"
|
"github.com/foomo/sesamy-go/pkg/session"
|
||||||
@ -62,12 +61,3 @@ func MPv2MiddlewareUserID(cookieName string) MPv2Middleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
184
pkg/collect/collect.go
Normal file
184
pkg/collect/collect.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package collect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
|
gtaghttp "github.com/foomo/sesamy-go/pkg/http/gtag"
|
||||||
|
mpv2http "github.com/foomo/sesamy-go/pkg/http/mpv2"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Collect struct {
|
||||||
|
l *zap.Logger
|
||||||
|
taggingURL string
|
||||||
|
taggingClient *http.Client
|
||||||
|
gtagHTTPMiddlewares []gtaghttp.Middleware
|
||||||
|
mpv2HTTPMiddlewares []mpv2http.Middleware
|
||||||
|
}
|
||||||
|
Option func(*Collect) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// ~ Options
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func WithTagging(v string) Option {
|
||||||
|
return func(c *Collect) error {
|
||||||
|
c.taggingURL = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTaggingClient(v *http.Client) Option {
|
||||||
|
return func(c *Collect) error {
|
||||||
|
c.taggingClient = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithGTagHTTPMiddlewares(v ...gtaghttp.Middleware) Option {
|
||||||
|
return func(c *Collect) error {
|
||||||
|
c.gtagHTTPMiddlewares = append(c.gtagHTTPMiddlewares, v...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMPv2HTTPMiddlewares(v ...mpv2http.Middleware) Option {
|
||||||
|
return func(c *Collect) error {
|
||||||
|
c.mpv2HTTPMiddlewares = append(c.mpv2HTTPMiddlewares, v...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// ~ Constructor
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func New(l *zap.Logger, opts ...Option) (*Collect, error) {
|
||||||
|
inst := &Collect{
|
||||||
|
l: l,
|
||||||
|
taggingClient: http.DefaultClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
|
if err := opt(inst); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// ~ Public methods
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c *Collect) GTagHTTPHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// retrieve payload
|
||||||
|
payload := gtaghttp.Handler(w, r)
|
||||||
|
|
||||||
|
// compose middlewares
|
||||||
|
next := c.gtagHandler
|
||||||
|
for _, middleware := range c.gtagHTTPMiddlewares {
|
||||||
|
next = middleware(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run handler
|
||||||
|
if err := next(c.l, w, r, payload); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collect) MPv2HTTPHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// retrieve payload
|
||||||
|
payload := mpv2http.Handler(w, r)
|
||||||
|
|
||||||
|
// compose middlewares
|
||||||
|
next := c.mpv2Handler
|
||||||
|
for _, middleware := range c.mpv2HTTPMiddlewares {
|
||||||
|
next = middleware(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run handler
|
||||||
|
if err := next(c.l, w, r, payload); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
// ~ Private methods
|
||||||
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (c *Collect) gtagHandler(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
|
values, body, err := gtag.Encode(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("%s%s?%s", c.taggingURL, "/g/collect", gtag.EncodeValues(values)), body)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy headers
|
||||||
|
req.Header = r.Header.Clone()
|
||||||
|
|
||||||
|
resp, err := c.taggingClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// copy headers
|
||||||
|
r.Header = resp.Header.Clone()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collect) mpv2Handler(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("%s%s", c.taggingURL, "/mp/collect"), bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy headers
|
||||||
|
req.Header = r.Header.Clone()
|
||||||
|
// copy raw query
|
||||||
|
req.URL.RawQuery = r.URL.RawQuery
|
||||||
|
|
||||||
|
resp, err := c.taggingClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// copy headers
|
||||||
|
r.Header = resp.Header.Clone()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
package gtag
|
package gtag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// See https://developers.google.com/tag-platform/security/concepts/consent-mode
|
||||||
type Consent struct {
|
type Consent struct {
|
||||||
// Current Google Consent Status. Format 'G1'+'AdsStorageBoolStatus'`+'AnalyticsStorageBoolStatus'
|
// Current Google Consent Status. Format 'G1'+'AdsStorageBoolStatus'`+'AnalyticsStorageBoolStatus'
|
||||||
// Example: G101
|
// Example: G101
|
||||||
@ -17,6 +19,11 @@ type Consent struct {
|
|||||||
// Will be added with the value "1" if the Google Consent had a default value before getting an update
|
// Will be added with the value "1" if the Google Consent had a default value before getting an update
|
||||||
// Example: G111
|
// Example: G111
|
||||||
GoogleConsentDefault *string `json:"google_consent_default,omitempty" gtag:"gcd,omitempty"`
|
GoogleConsentDefault *string `json:"google_consent_default,omitempty" gtag:"gcd,omitempty"`
|
||||||
|
// Example: 1
|
||||||
|
// DigitalMarketAct *string `json:"digital_market_act,omitempty" gtag:"dma,omitempty"`
|
||||||
|
// Example: sypham
|
||||||
|
// DigitalMarketActParameters *string `json:"digital_market_act_parameters,omitempty" gtag:"dma_cps,omitempty"`
|
||||||
|
// Example: noapi | denied
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
@ -24,6 +31,12 @@ type Consent struct {
|
|||||||
// ------------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
func (c Consent) AdStorage() bool {
|
func (c Consent) AdStorage() bool {
|
||||||
|
if c.GoogleConsentDefault != nil {
|
||||||
|
gcd := strings.Split(*c.GoogleConsentDefault, "")
|
||||||
|
if len(gcd) > 3 {
|
||||||
|
return slices.Contains([]string{"l", "t", "r", "n", "u", "v"}, gcd[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.GoogleConsentUpdate != nil {
|
if c.GoogleConsentUpdate != nil {
|
||||||
gcs := *c.GoogleConsentUpdate
|
gcs := *c.GoogleConsentUpdate
|
||||||
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
||||||
@ -35,6 +48,12 @@ func (c Consent) AdStorage() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Consent) AnalyticsStorage() bool {
|
func (c Consent) AnalyticsStorage() bool {
|
||||||
|
if c.GoogleConsentDefault != nil {
|
||||||
|
gcd := strings.Split(*c.GoogleConsentDefault, "")
|
||||||
|
if len(gcd) > 5 {
|
||||||
|
return slices.Contains([]string{"l", "t", "r", "n", "u", "v"}, gcd[4])
|
||||||
|
}
|
||||||
|
}
|
||||||
if c.GoogleConsentUpdate != nil {
|
if c.GoogleConsentUpdate != nil {
|
||||||
gcs := *c.GoogleConsentUpdate
|
gcs := *c.GoogleConsentUpdate
|
||||||
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
||||||
@ -44,3 +63,23 @@ func (c Consent) AnalyticsStorage() bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Consent) AdUserData() bool {
|
||||||
|
if c.GoogleConsentDefault != nil {
|
||||||
|
gcd := strings.Split(*c.GoogleConsentDefault, "")
|
||||||
|
if len(gcd) > 7 {
|
||||||
|
return slices.Contains([]string{"l", "t", "r", "n", "u", "v"}, gcd[6])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.AdStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Consent) AdPersonalization() bool {
|
||||||
|
if c.GoogleConsentDefault != nil {
|
||||||
|
gcd := strings.Split(*c.GoogleConsentDefault, "")
|
||||||
|
if len(gcd) > 9 {
|
||||||
|
return slices.Contains([]string{"l", "t", "r", "n", "u", "v"}, gcd[8])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.AdStorage()
|
||||||
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ func TestDecode(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "add_to_cart",
|
name: "add_to_cart",
|
||||||
args: GTagAddToCart,
|
args: "v=2&tid=G-F9XM71K45T>m=45he45m0v9184715813z89184708445za200zb9184708445&_p=1716795486104&_dbg=1&gcd=13l3l3l2l1&npa=1&dma_cps=sypham&dma=1&cid=179294588.1715353601&ecid=788548699&ul=en-us&sr=2056x1329&_fplc=0&ur=&uaa=arm&uab=64&uafvl=Chromium%3B124.0.6367.201%7CGoogle%2520Chrome%3B124.0.6367.201%7CNot-A.Brand%3B99.0.0.0&uamb=0&uam=&uap=macOS&uapv=14.4.1&uaw=0&are=1&frm=0&pscdl=noapi&sst.gcd=13l3l3l2l1&sst.tft=1716795486104&sst.ude=0&_s=4&cu=USD&sid=1716793773&sct=14&seg=1&dl=https%3A%2F%2Fsesamy.local.bestbytes.net%2F%3Fgtm_debug%3D1716795486020&dr=https%3A%2F%2Ftagassistant.google.com%2F&dt=Home&en=add_to_cart&pr1=idSKU_12345~nmStan%20and%20Friends%20Tee~afGoogle%20Merchandise%20Store~cpSUMMER_FUN~ds2.22~lp0~brGoogle~caApparel~c2Adult~c3Shirts~c4Crew~c5Short%20sleeve~lirelated_products~lnRelated%20Products~vagreen~loChIJIQBpAG2ahYAR_6128GcTUEo~pr10.01~qt3&ep.enable_page_views=false&epn.value=30.03&_et=1255&tfd=145479&richsstsse",
|
||||||
want: `{"consent":{"google_consent_default":"13l3l3l2l1"},"campaign":{},"ecommerce":{"currency":"USD","items":[{"affiliation":"Google Merchandise Store","coupon":"SUMMER_FUN","discount":"2.22","item_brand":"Google","item_category":"Apparel","item_category2":"Adult","item_category3":"Shirts","item_category4":"Crew","item_category5":"Short sleeve","item_id":"SKU_12345","item_list_id":"related_products","item_list_name":"Related Products","item_name":"Stan and Friends Tee","item_variant":"green","item_list_position":"0","location_id":"ChIJIQBpAG2ahYAR_6128GcTUEo","price":"10.01","quantity":"3"}]},"client_hints":{"screen_resolution":"2056x1329","user_language":"en-us","user_agent_architecture":"arm","user_agent_bitness":"64","user_agent_full_version_list":"Chromium;124.0.6367.201|Google%20Chrome;124.0.6367.201|Not-A.Brand;99.0.0.0","user_agent_mobile":"0","user_agent_model":"","user_agent_platform":"macOS","user_agent_platform_version":"14.4.1","user_agent_wow_64":"0","user_region":""},"protocol_version":"2","tracking_id":"G-F9XM71K45T","gtmhash_info":"45he45m0v9184715813z89184708445za200zb9184708445","client_id":"179294588.1715353601","richsstsse":"","document_location":"https://sesamy.local.bestbytes.net/?gtm_debug=1716795486020","document_title":"Home","document_referrer":"https://tagassistant.google.com/","is_debug":"1","event_name":"add_to_cart","event_parameter":{"enable_page_views":"false"},"event_parameter_number":{"value":"30.03"},"session_id":"1716793773","non_personalized_ads":"1","sst":{"tft":"1716795486104","gcd":"13l3l3l2l1","ude":"0"}}`,
|
want: `{"consent":{"google_consent_default":"13l3l3l2l1"},"campaign":{},"ecommerce":{"currency":"USD","items":[{"affiliation":"Google Merchandise Store","coupon":"SUMMER_FUN","discount":"2.22","item_brand":"Google","item_category":"Apparel","item_category2":"Adult","item_category3":"Shirts","item_category4":"Crew","item_category5":"Short sleeve","item_id":"SKU_12345","item_list_id":"related_products","item_list_name":"Related Products","item_name":"Stan and Friends Tee","item_variant":"green","item_list_position":"0","location_id":"ChIJIQBpAG2ahYAR_6128GcTUEo","price":"10.01","quantity":"3"}]},"client_hints":{"screen_resolution":"2056x1329","user_language":"en-us","user_agent_architecture":"arm","user_agent_bitness":"64","user_agent_full_version_list":"Chromium;124.0.6367.201|Google%20Chrome;124.0.6367.201|Not-A.Brand;99.0.0.0","user_agent_mobile":"0","user_agent_model":"","user_agent_platform":"macOS","user_agent_platform_version":"14.4.1","user_agent_wow_64":"0","user_region":""},"protocol_version":"2","tracking_id":"G-F9XM71K45T","gtmhash_info":"45he45m0v9184715813z89184708445za200zb9184708445","client_id":"179294588.1715353601","richsstsse":"","document_location":"https://sesamy.local.bestbytes.net/?gtm_debug=1716795486020","document_title":"Home","document_referrer":"https://tagassistant.google.com/","is_debug":"1","event_name":"add_to_cart","event_parameter":{"enable_page_views":"false"},"event_parameter_number":{"value":"30.03"},"session_id":"1716793773","non_personalized_ads":"1","sst":{"tft":"1716795486104","gcd":"13l3l3l2l1","ude":"0"}}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -151,11 +151,6 @@ type Payload struct {
|
|||||||
NonPersonalizedAds *string `json:"non_personalized_ads,omitempty" gtag:"npa,omitempty"`
|
NonPersonalizedAds *string `json:"non_personalized_ads,omitempty" gtag:"npa,omitempty"`
|
||||||
// Example: 1
|
// Example: 1
|
||||||
// ARE *string `json:"are,omitempty" gtag:"are,omitempty"`
|
// ARE *string `json:"are,omitempty" gtag:"are,omitempty"`
|
||||||
// Example: 1
|
|
||||||
// DigitalMarketAct *string `json:"digital_market_act,omitempty" gtag:"dma,omitempty"`
|
|
||||||
// Example: sypham
|
|
||||||
// DigitalMarketActParameters *string `json:"digital_market_act_parameters,omitempty" gtag:"dma_cps,omitempty"`
|
|
||||||
// Example: noapi | denied
|
|
||||||
// PrivacySandboxCookieDeprecationLabel *string `json:"privacy_sandbox_cookie_deprecation_label,omitempty" gtag:"pscdl,omitempty"`
|
// PrivacySandboxCookieDeprecationLabel *string `json:"privacy_sandbox_cookie_deprecation_label,omitempty" gtag:"pscdl,omitempty"`
|
||||||
// A timestamp measuring the difference between the moment this parameter gets populated and the moment the navigation started on that particular page.
|
// A timestamp measuring the difference between the moment this parameter gets populated and the moment the navigation started on that particular page.
|
||||||
// TFD *string `json:"tfd,omitempty" gtag:"tfd,omitempty"`
|
// TFD *string `json:"tfd,omitempty" gtag:"tfd,omitempty"`
|
||||||
|
|||||||
@ -13,7 +13,7 @@ type SST struct {
|
|||||||
GCSub *string `json:"gcsub,omitempty" gtag:"gcsub,omitempty"`
|
GCSub *string `json:"gcsub,omitempty" gtag:"gcsub,omitempty"`
|
||||||
// Example: DE
|
// Example: DE
|
||||||
UC *string `json:"uc,omitempty" gtag:"uc,omitempty"`
|
UC *string `json:"uc,omitempty" gtag:"uc,omitempty"`
|
||||||
// Example: 1708250245344
|
// Session start time, time first seen. Example: 1708250245344
|
||||||
TFT *string `json:"tft,omitempty" gtag:"tft,omitempty"`
|
TFT *string `json:"tft,omitempty" gtag:"tft,omitempty"`
|
||||||
// Example: 13l3l3l3l1
|
// Example: 13l3l3l3l1
|
||||||
GCD *string `json:"gcd,omitempty" gtag:"gcd,omitempty"`
|
GCD *string `json:"gcd,omitempty" gtag:"gcd,omitempty"`
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@ -26,12 +27,19 @@ func MPv2(source gtag.Payload, target any) error {
|
|||||||
targetData := map[string]any{
|
targetData := map[string]any{
|
||||||
"client_id": source.ClientID,
|
"client_id": source.ClientID,
|
||||||
"user_id": source.UserID,
|
"user_id": source.UserID,
|
||||||
|
"session_id": source.SessionID,
|
||||||
"non_personalized_ads": source.NonPersonalizedAds,
|
"non_personalized_ads": source.NonPersonalizedAds,
|
||||||
"debug_mode": source.IsDebug,
|
"debug_mode": source.IsDebug,
|
||||||
}
|
}
|
||||||
if source.SST != nil && source.SST.TFT != nil {
|
|
||||||
targetData["timestamp_micros"] = gtag.Get(source.SST.TFT) + "000"
|
// consent
|
||||||
|
targetConsentData := map[string]any{
|
||||||
|
"add_storage": mpv2.ConsentText(source.AdStorage()),
|
||||||
|
"ad_user_data": mpv2.ConsentText(source.AdUserData()),
|
||||||
|
"ad_personalization": mpv2.ConsentText(source.AdPersonalization()),
|
||||||
|
"analytics_storage": mpv2.ConsentText(source.AnalyticsStorage()),
|
||||||
}
|
}
|
||||||
|
targetData["consent"] = targetConsentData
|
||||||
|
|
||||||
// combine user properties
|
// combine user properties
|
||||||
targetUserProperties := map[string]any{}
|
targetUserProperties := map[string]any{}
|
||||||
@ -52,6 +60,15 @@ func MPv2(source gtag.Payload, target any) error {
|
|||||||
"name": source.EventName,
|
"name": source.EventName,
|
||||||
}
|
}
|
||||||
targetEventDataParams := map[string]any{}
|
targetEventDataParams := map[string]any{}
|
||||||
|
if value, ok := sourceData["document_title"]; ok {
|
||||||
|
targetEventDataParams["page_title"] = value
|
||||||
|
}
|
||||||
|
if value, ok := sourceData["document_referrer"]; ok {
|
||||||
|
targetEventDataParams["page_referrer"] = value
|
||||||
|
}
|
||||||
|
if value, ok := sourceData["document_location"]; ok {
|
||||||
|
targetEventDataParams["page_location"] = value
|
||||||
|
}
|
||||||
if node, ok := sourceData["ecommerce"].(map[string]any); ok {
|
if node, ok := sourceData["ecommerce"].(map[string]any); ok {
|
||||||
maps.Copy(targetEventDataParams, node)
|
maps.Copy(targetEventDataParams, node)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,3 +6,10 @@ const (
|
|||||||
ConsentDenied Consent = "DENIED"
|
ConsentDenied Consent = "DENIED"
|
||||||
ConsentGranted Consent = "GRANTED"
|
ConsentGranted Consent = "GRANTED"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func ConsentText(v bool) Consent {
|
||||||
|
if v {
|
||||||
|
return ConsentGranted
|
||||||
|
}
|
||||||
|
return ConsentDenied
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package mpv2
|
package mpv2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/foomo/sesamy-go/pkg/sesamy"
|
"github.com/foomo/sesamy-go/pkg/sesamy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,3 +19,9 @@ type Payload[P any] struct {
|
|||||||
SessionID string `json:"session_id,omitempty"`
|
SessionID string `json:"session_id,omitempty"`
|
||||||
EngagementTimeMSec int64 `json:"engagement_time_msec,omitempty"`
|
EngagementTimeMSec int64 `json:"engagement_time_msec,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewPayload[P any]() *Payload[P] {
|
||||||
|
return &Payload[P]{
|
||||||
|
TimestampMicros: time.Now().UnixMicro(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -48,17 +48,23 @@ func GTag[P any](source mpv2.Payload[P], target any) error {
|
|||||||
targetData["event_name"] = sourceData["name"]
|
targetData["event_name"] = sourceData["name"]
|
||||||
|
|
||||||
if params, ok := sourceData["params"].(map[string]any); ok {
|
if params, ok := sourceData["params"].(map[string]any); ok {
|
||||||
|
targetData["document_title"] = params["page_title"]
|
||||||
|
delete(params, "page_title")
|
||||||
|
targetData["document_referrer"] = params["page_referrer"]
|
||||||
|
delete(params, "page_referrer")
|
||||||
|
targetData["document_location"] = params["page_location"]
|
||||||
|
delete(params, "page_location")
|
||||||
targetData["currency"] = params["currency"]
|
targetData["currency"] = params["currency"]
|
||||||
targetData["promotion_id"] = params["promotion_id"]
|
|
||||||
targetData["promotion_name"] = params["promotion_name"]
|
|
||||||
targetData["location_id"] = params["location_id"]
|
|
||||||
targetData["is_conversion"] = params["is_conversion"]
|
|
||||||
targetData["items"] = params["items"]
|
|
||||||
delete(params, "currency")
|
delete(params, "currency")
|
||||||
|
targetData["promotion_id"] = params["promotion_id"]
|
||||||
delete(params, "promotion_id")
|
delete(params, "promotion_id")
|
||||||
|
targetData["promotion_name"] = params["promotion_name"]
|
||||||
delete(params, "promotion_name")
|
delete(params, "promotion_name")
|
||||||
|
targetData["location_id"] = params["location_id"]
|
||||||
delete(params, "location_id")
|
delete(params, "location_id")
|
||||||
|
targetData["is_conversion"] = params["is_conversion"]
|
||||||
delete(params, "is_conversion")
|
delete(params, "is_conversion")
|
||||||
|
targetData["items"] = params["items"]
|
||||||
delete(params, "items")
|
delete(params, "items")
|
||||||
{ // user_property
|
{ // user_property
|
||||||
targetEventProperty := map[string]any{}
|
targetEventProperty := map[string]any{}
|
||||||
|
|||||||
@ -3,5 +3,6 @@ package params
|
|||||||
// PageView https://developers.google.com/analytics/devguides/collection/ga4/views?client_type=gtag#manually_send_page_view_events
|
// PageView https://developers.google.com/analytics/devguides/collection/ga4/views?client_type=gtag#manually_send_page_view_events
|
||||||
type PageView struct {
|
type PageView struct {
|
||||||
PageTitle string `json:"page_title,omitempty"`
|
PageTitle string `json:"page_title,omitempty"`
|
||||||
|
PageReferrer string `json:"page_referrer,omitempty"`
|
||||||
PageLocation string `json:"page_location,omitempty"`
|
PageLocation string `json:"page_location,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
10
pkg/http/eventhandler.go
Normal file
10
pkg/http/eventhandler.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/sesamy"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandler func(l *zap.Logger, r *http.Request, event *sesamy.Event[any]) error
|
||||||
61
pkg/http/gtag/handler.go
Normal file
61
pkg/http/gtag/handler.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package gtag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(w http.ResponseWriter, r *http.Request) *gtag.Payload {
|
||||||
|
var values url.Values
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
values = r.URL.Query()
|
||||||
|
case http.MethodPost:
|
||||||
|
values = r.URL.Query()
|
||||||
|
|
||||||
|
// read request body
|
||||||
|
out, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to read body: %s", err.Error()), http.StatusInternalServerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// append request body to query
|
||||||
|
if len(out) > 0 {
|
||||||
|
v, err := url.ParseQuery(string(out))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to parse extended url: %s", err.Error()), http.StatusInternalServerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for s2, i := range v {
|
||||||
|
values.Set(s2, i[0])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values = r.URL.Query()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshal event
|
||||||
|
var payload *gtag.Payload
|
||||||
|
if err := gtag.Decode(values, &payload); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate
|
||||||
|
if payload.EventName == nil || payload.EventName.String() == "" {
|
||||||
|
http.Error(w, "missing event name", http.StatusBadRequest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
73
pkg/http/gtag/middleware.go
Normal file
73
pkg/http/gtag/middleware.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package gtag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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/foomo/sesamy-go/pkg/encoding/mpv2encode"
|
||||||
|
sesamyhttp "github.com/foomo/sesamy-go/pkg/http"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Middleware func(next MiddlewareHandler) MiddlewareHandler
|
||||||
|
MiddlewareHandler func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error
|
||||||
|
)
|
||||||
|
|
||||||
|
func MiddlewareEventHandler(h sesamyhttp.EventHandler) Middleware {
|
||||||
|
return func(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
|
var mpv2Payload *mpv2.Payload[any]
|
||||||
|
if err := gtagencode.MPv2(*payload, &mpv2Payload); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to encode gtag to mpv2")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, event := range mpv2Payload.Events {
|
||||||
|
if err := h(l, r, &event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mpv2Payload.Events[i] = event
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mpv2encode.GTag[any](*mpv2Payload, &payload); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to encode mpv2 to gtag")
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareUserID(cookieName string) Middleware {
|
||||||
|
return func(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
|
if cookie, err := r.Cookie(cookieName); err == nil {
|
||||||
|
payload.UserID = gtag.Set(cookie.Value)
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareLogger(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *gtag.Payload) error {
|
||||||
|
if spanCtx := trace.SpanContextFromContext(r.Context()); spanCtx.IsValid() && spanCtx.IsSampled() {
|
||||||
|
l = l.With(zap.String("trace_id", spanCtx.TraceID().String()), zap.String("span_id", spanCtx.SpanID().String()))
|
||||||
|
}
|
||||||
|
l = l.With(
|
||||||
|
zap.String("event_name", gtag.GetDefault(payload.EventName, "-").String()),
|
||||||
|
zap.String("event_user_id", gtag.GetDefault(payload.UserID, "-")),
|
||||||
|
zap.String("event_session_id", gtag.GetDefault(payload.SessionID, "-")),
|
||||||
|
)
|
||||||
|
err := next(l, w, r, payload)
|
||||||
|
if err != nil {
|
||||||
|
l.Error("handled event", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
l.Info("handled event")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
37
pkg/http/mpv2/handler.go
Normal file
37
pkg/http/mpv2/handler.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package mpv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Handler(w http.ResponseWriter, r *http.Request) *mpv2.Payload[any] {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read request body
|
||||||
|
var payload *mpv2.Payload[any]
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate required fields
|
||||||
|
if len(payload.Events) == 0 {
|
||||||
|
http.Error(w, "missing events", http.StatusBadRequest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, event := range payload.Events {
|
||||||
|
if event.Name == "" {
|
||||||
|
http.Error(w, "missing event name", http.StatusBadRequest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
170
pkg/http/mpv2/middleware.go
Normal file
170
pkg/http/mpv2/middleware.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package mpv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||||
|
sesamyhttp "github.com/foomo/sesamy-go/pkg/http"
|
||||||
|
"github.com/foomo/sesamy-go/pkg/session"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MiddlewareHandler func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error
|
||||||
|
Middleware func(next MiddlewareHandler) MiddlewareHandler
|
||||||
|
)
|
||||||
|
|
||||||
|
func MiddlewareEventHandler(h sesamyhttp.EventHandler) Middleware {
|
||||||
|
return func(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
for i, event := range payload.Events {
|
||||||
|
if err := h(l, r, &event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload.Events[i] = event
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareSessionID(measurementID string) Middleware {
|
||||||
|
measurementID = strings.Split(measurementID, "-")[1]
|
||||||
|
return func(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, 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 err
|
||||||
|
}
|
||||||
|
payload.SessionID = value
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareClientID(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
if payload.ClientID == "" {
|
||||||
|
value, err := session.ParseGAClientID(r)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrNoCookie) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload.ClientID = value
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareDebugMode(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
if !payload.DebugMode && session.IsGTMDebug(r) {
|
||||||
|
payload.DebugMode = true
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareUserID(cookieName string) Middleware {
|
||||||
|
return func(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, 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(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareTimestamp(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
if payload.TimestampMicros == 0 {
|
||||||
|
payload.TimestampMicros = time.Now().UnixMicro()
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareUserAgent(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
|
||||||
|
for i, event := range payload.Events {
|
||||||
|
if value, ok := event.Params.(map[string]any); ok {
|
||||||
|
value["user_agent"] = userAgent
|
||||||
|
payload.Events[i] = event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareIPOverride(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
var ipOverride string
|
||||||
|
for _, key := range []string{"CF-Connecting-IP", "X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-Ip"} {
|
||||||
|
if value := r.Header.Get(key); value != "" {
|
||||||
|
ipOverride = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ipOverride != "" {
|
||||||
|
for i, event := range payload.Events {
|
||||||
|
if value, ok := event.Params.(map[string]any); ok {
|
||||||
|
value["ip_override"] = ipOverride
|
||||||
|
payload.Events[i] = event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewarePageLocation(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
if referrer := r.Header.Get("Referer"); referrer != "" {
|
||||||
|
for i, event := range payload.Events {
|
||||||
|
if value, ok := event.Params.(map[string]any); ok {
|
||||||
|
value["page_location"] = referrer
|
||||||
|
payload.Events[i] = event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(l, w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiddlewareLogger(next MiddlewareHandler) MiddlewareHandler {
|
||||||
|
return func(l *zap.Logger, w http.ResponseWriter, r *http.Request, payload *mpv2.Payload[any]) error {
|
||||||
|
eventNames := make([]string, len(payload.Events))
|
||||||
|
for i, event := range payload.Events {
|
||||||
|
eventNames[i] = event.Name.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if spanCtx := trace.SpanContextFromContext(r.Context()); spanCtx.IsValid() && spanCtx.IsSampled() {
|
||||||
|
l = l.With(zap.String("trace_id", spanCtx.TraceID().String()), zap.String("span_id", spanCtx.SpanID().String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.With(
|
||||||
|
zap.String("event_names", strings.Join(eventNames, ",")),
|
||||||
|
zap.String("event_user_id", payload.UserID),
|
||||||
|
)
|
||||||
|
|
||||||
|
err := next(l, w, r, payload)
|
||||||
|
if err != nil {
|
||||||
|
l.Error("handled event", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
l.Info("handled event")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,3 +36,7 @@ func (e Event[P]) Decode(output any) error {
|
|||||||
func (e Event[P]) DecodeParams(output any) error {
|
func (e Event[P]) DecodeParams(output any) error {
|
||||||
return Decode(e.Params, output)
|
return Decode(e.Params, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e Event[P]) EncodeParams(input any) error {
|
||||||
|
return Decode(input, &e.Params)
|
||||||
|
}
|
||||||
|
|||||||
35
pkg/sesamy/event_test.go
Normal file
35
pkg/sesamy/event_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package sesamy_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/foomo/sesamy-go/pkg/sesamy"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeParams(t *testing.T) {
|
||||||
|
type params struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
event := sesamy.Event[any]{
|
||||||
|
Name: "test",
|
||||||
|
Params: map[string]any{
|
||||||
|
"title": "foo",
|
||||||
|
"description": "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p params
|
||||||
|
require.NoError(t, event.DecodeParams(&p))
|
||||||
|
assert.Equal(t, "foo", p.Title)
|
||||||
|
|
||||||
|
p.Title = "bar"
|
||||||
|
|
||||||
|
require.NoError(t, event.EncodeParams(p))
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"title": "bar",
|
||||||
|
"description": "foo",
|
||||||
|
}, event.Params)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user