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))
|
||||
}
|
||||
|
||||
var timestamp time.Time
|
||||
if payload.TimestampMicros > 0 {
|
||||
timestamp = time.UnixMicro(payload.TimestampMicros)
|
||||
} else {
|
||||
timestamp = time.Now()
|
||||
}
|
||||
|
||||
l.entries <- logproto.Entry{
|
||||
Line: string(lineBytes),
|
||||
Timestamp: time.UnixMicro(payload.TimestampMicros),
|
||||
Timestamp: timestamp,
|
||||
StructuredMetadata: push.LabelsAdapter{
|
||||
{
|
||||
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
|
||||
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}`
|
||||
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)) {
|
||||
fmt.Println(string(msg.Payload))
|
||||
}
|
||||
|
||||
@ -3,15 +3,13 @@ package gtag
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||
gtaghttp "github.com/foomo/sesamy-go/pkg/http/gtag"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -22,12 +20,10 @@ type (
|
||||
uuidFunc func() string
|
||||
messages chan *message.Message
|
||||
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
||||
middlewares []SubscriberMiddleware
|
||||
middlewares []gtaghttp.Middleware
|
||||
closed bool
|
||||
}
|
||||
SubscriberOption func(*Subscriber)
|
||||
SubscriberHandler func(l *zap.Logger, r *http.Request, payload *gtag.Payload) error
|
||||
SubscriberMiddleware func(next SubscriberHandler) SubscriberHandler
|
||||
SubscriberOption func(*Subscriber)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
@ -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) {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
// retrieve payload
|
||||
payload := gtaghttp.Handler(w, r)
|
||||
|
||||
// compose middlewares
|
||||
next := s.handle
|
||||
@ -123,13 +75,13 @@ func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
data, err := json.Marshal(payload)
|
||||
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/message"
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||
mpv2http "github.com/foomo/sesamy-go/pkg/http/mpv2"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -19,12 +20,10 @@ type (
|
||||
uuidFunc func() string
|
||||
messages chan *message.Message
|
||||
messageFunc func(l *zap.Logger, r *http.Request, msg *message.Message) error
|
||||
middlewares []SubscriberMiddleware
|
||||
middlewares []mpv2http.Middleware
|
||||
closed bool
|
||||
}
|
||||
SubscriberOption func(*Subscriber)
|
||||
SubscriberHandler func(l *zap.Logger, r *http.Request, payload *mpv2.Payload[any]) error
|
||||
SubscriberMiddleware func(next SubscriberHandler) SubscriberHandler
|
||||
SubscriberOption func(*Subscriber)
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
@ -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) {
|
||||
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) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
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
|
||||
}
|
||||
}
|
||||
// retrieve payload
|
||||
payload := mpv2http.Handler(w, r)
|
||||
|
||||
// compose middlewares
|
||||
next := s.handle
|
||||
@ -102,13 +79,13 @@ func (s *Subscriber) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
jsonPayload, err := json.Marshal(payload)
|
||||
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"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||
"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 {
|
||||
anyEvents := make([]sesamy.Event[any], len(events))
|
||||
for i, event := range events {
|
||||
anyEvents[i] = event.AnyEvent()
|
||||
}
|
||||
|
||||
payload := &mpv2.Payload[any]{
|
||||
Events: anyEvents,
|
||||
TimestampMicros: time.Now().UnixMicro(),
|
||||
payload := mpv2.NewPayload[any]()
|
||||
for _, event := range events {
|
||||
payload.Events = append(payload.Events, event.AnyEvent())
|
||||
}
|
||||
return c.SendPayload(r, payload)
|
||||
}
|
||||
|
||||
func (c *MPv2) SendPayload(r *http.Request, payload *mpv2.Payload[any]) error {
|
||||
next := c.SendRaw
|
||||
for _, middleware := range c.middlewares {
|
||||
next = middleware(next)
|
||||
|
||||
@ -3,7 +3,6 @@ package client
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||
"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
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// See https://developers.google.com/tag-platform/security/concepts/consent-mode
|
||||
type Consent struct {
|
||||
// Current Google Consent Status. Format 'G1'+'AdsStorageBoolStatus'`+'AnalyticsStorageBoolStatus'
|
||||
// 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
|
||||
// Example: G111
|
||||
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 {
|
||||
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 {
|
||||
gcs := *c.GoogleConsentUpdate
|
||||
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
||||
@ -35,6 +48,12 @@ func (c Consent) AdStorage() 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 {
|
||||
gcs := *c.GoogleConsentUpdate
|
||||
if strings.HasPrefix(gcs, "G1") && len(gcs) == 4 {
|
||||
@ -44,3 +63,23 @@ func (c Consent) AnalyticsStorage() bool {
|
||||
}
|
||||
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",
|
||||
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"}}`,
|
||||
},
|
||||
{
|
||||
|
||||
@ -151,11 +151,6 @@ type Payload struct {
|
||||
NonPersonalizedAds *string `json:"non_personalized_ads,omitempty" gtag:"npa,omitempty"`
|
||||
// Example: 1
|
||||
// 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"`
|
||||
// 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"`
|
||||
|
||||
@ -13,7 +13,7 @@ type SST struct {
|
||||
GCSub *string `json:"gcsub,omitempty" gtag:"gcsub,omitempty"`
|
||||
// Example: DE
|
||||
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"`
|
||||
// Example: 13l3l3l3l1
|
||||
GCD *string `json:"gcd,omitempty" gtag:"gcd,omitempty"`
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/gtag"
|
||||
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -26,12 +27,19 @@ func MPv2(source gtag.Payload, target any) error {
|
||||
targetData := map[string]any{
|
||||
"client_id": source.ClientID,
|
||||
"user_id": source.UserID,
|
||||
"session_id": source.SessionID,
|
||||
"non_personalized_ads": source.NonPersonalizedAds,
|
||||
"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
|
||||
targetUserProperties := map[string]any{}
|
||||
@ -52,6 +60,15 @@ func MPv2(source gtag.Payload, target any) error {
|
||||
"name": source.EventName,
|
||||
}
|
||||
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 {
|
||||
maps.Copy(targetEventDataParams, node)
|
||||
}
|
||||
|
||||
@ -6,3 +6,10 @@ const (
|
||||
ConsentDenied Consent = "DENIED"
|
||||
ConsentGranted Consent = "GRANTED"
|
||||
)
|
||||
|
||||
func ConsentText(v bool) Consent {
|
||||
if v {
|
||||
return ConsentGranted
|
||||
}
|
||||
return ConsentDenied
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package mpv2
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/foomo/sesamy-go/pkg/sesamy"
|
||||
)
|
||||
|
||||
@ -17,3 +19,9 @@ type Payload[P any] struct {
|
||||
SessionID string `json:"session_id,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"]
|
||||
|
||||
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["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")
|
||||
targetData["promotion_id"] = params["promotion_id"]
|
||||
delete(params, "promotion_id")
|
||||
targetData["promotion_name"] = params["promotion_name"]
|
||||
delete(params, "promotion_name")
|
||||
targetData["location_id"] = params["location_id"]
|
||||
delete(params, "location_id")
|
||||
targetData["is_conversion"] = params["is_conversion"]
|
||||
delete(params, "is_conversion")
|
||||
targetData["items"] = params["items"]
|
||||
delete(params, "items")
|
||||
{ // user_property
|
||||
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
|
||||
type PageView struct {
|
||||
PageTitle string `json:"page_title,omitempty"`
|
||||
PageReferrer string `json:"page_referrer,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 {
|
||||
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