sesamy-go/pkg/client/mpv2.go
2024-09-30 10:12:33 +02:00

184 lines
4.5 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/foomo/sesamy-go/pkg/encoding/mpv2"
"github.com/foomo/sesamy-go/pkg/sesamy"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type (
MPv2 struct {
l *zap.Logger
path string
host string
cookies []string
// To create a new secret, navigate in the Google Analytics UI to:
// Admin > Data Streams > choose your stream > Measurement Protocol > Create
apiSecret string
// Measurement ID. The identifier for a Data Stream. Found in the Google Analytics UI under:
// Admin > Data Streams > choose your stream > Measurement ID
measurementID string
protocolVersion string
httpClient *http.Client
middlewares []MPv2Middleware
}
MPv2Option func(*MPv2)
MPv2Handler func(r *http.Request, payload *mpv2.Payload[any]) error
MPv2Middleware func(next MPv2Handler) MPv2Handler
)
// ------------------------------------------------------------------------------------------------
// ~ Options
// ------------------------------------------------------------------------------------------------
func MPv2WithHTTPClient(v *http.Client) MPv2Option {
return func(o *MPv2) {
o.httpClient = v
}
}
func MPv2WithPath(v string) MPv2Option {
return func(o *MPv2) {
o.path = v
}
}
func MPv2WithCookies(v ...string) MPv2Option {
return func(o *MPv2) {
o.cookies = append(o.cookies, v...)
}
}
func MPv2WithAPISecret(v string) MPv2Option {
return func(o *MPv2) {
o.apiSecret = v
}
}
func MPv2WithMeasurementID(v string) MPv2Option {
return func(o *MPv2) {
o.measurementID = v
}
}
func MPv2WithMiddlewares(v ...MPv2Middleware) MPv2Option {
return func(o *MPv2) {
o.middlewares = append(o.middlewares, v...)
}
}
// ------------------------------------------------------------------------------------------------
// ~ Constructor
// ------------------------------------------------------------------------------------------------
func NewMPv2(l *zap.Logger, host string, opts ...MPv2Option) *MPv2 {
inst := &MPv2{
l: l,
host: host,
path: "/mp/collect",
cookies: []string{"gtm_auth", "gtm_debug", "gtm_preview"},
protocolVersion: "2",
httpClient: http.DefaultClient,
}
for _, opt := range opts {
opt(inst)
}
inst.middlewares = append(inst.middlewares,
MPv2MiddlewarClientID,
)
return inst
}
// ------------------------------------------------------------------------------------------------
// ~ Getter
// ------------------------------------------------------------------------------------------------
func (c *MPv2) HTTPClient() *http.Client {
return c.httpClient
}
// ------------------------------------------------------------------------------------------------
// ~ Public methods
// ------------------------------------------------------------------------------------------------
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(),
}
next := c.SendRaw
for _, middleware := range c.middlewares {
next = middleware(next)
}
return next(r, payload)
}
func (c *MPv2) SendRaw(r *http.Request, payload *mpv2.Payload[any]) error {
jsonPayload, err := json.Marshal(payload)
if err != nil {
return errors.Wrap(err, "failed to encode payload")
}
req, err := http.NewRequestWithContext(
r.Context(),
http.MethodPost,
fmt.Sprintf("%s%s", c.host, c.path),
bytes.NewReader(jsonPayload),
)
if err != nil {
return errors.Wrap(err, "failed to create request")
}
// query
qry := req.URL.Query()
if len(c.apiSecret) > 0 {
qry.Add("api_secret", c.apiSecret)
}
if len(c.measurementID) > 0 {
qry.Add("measurement_id", c.measurementID)
}
req.URL.RawQuery = qry.Encode()
// TODO valiate: copy headers
req.Header = r.Header.Clone()
// forward cookies
for _, cookie := range c.cookies {
if value, _ := r.Cookie(cookie); value != nil {
req.AddCookie(value)
}
}
resp, err := c.httpClient.Do(req)
if err != nil {
return errors.Wrap(err, "failed to send request")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var body string
if out, err := io.ReadAll(resp.Body); err != nil {
c.l.With(zap.Error(err)).Warn(err.Error())
} else {
body = string(out)
}
return errors.Errorf("unexpected response status: %d (%s)", resp.StatusCode, body)
}
return nil
}