add generic analytics section for segment compatible api

This commit is contained in:
John Newman 2023-10-09 13:03:58 +01:00
parent ed00d333d1
commit c7244e67da
7 changed files with 53 additions and 40 deletions

View file

@ -26,27 +26,26 @@ import (
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )
const SegmentURL = "https://api.segment.io/v1/track" type AnalyticsClient struct {
url string
type SegmentClient struct {
key string key string
userID string userID string
log zerolog.Logger log zerolog.Logger
client http.Client client http.Client
} }
var Segment SegmentClient var Analytics AnalyticsClient
func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error { func (ac *AnalyticsClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
var buf bytes.Buffer var buf bytes.Buffer
var segmentUserID string var analyticsUserId string
if Segment.userID != "" { if Analytics.userID != "" {
segmentUserID = Segment.userID analyticsUserId = Analytics.userID
} else { } else {
segmentUserID = userID.String() analyticsUserId = userID.String()
} }
err := json.NewEncoder(&buf).Encode(map[string]interface{}{ err := json.NewEncoder(&buf).Encode(map[string]interface{}{
"userId": segmentUserID, "userId": analyticsUserId,
"event": event, "event": event,
"properties": properties, "properties": properties,
}) })
@ -54,12 +53,12 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return err return err
} }
req, err := http.NewRequest("POST", SegmentURL, &buf) req, err := http.NewRequest("POST", ac.url, &buf)
if err != nil { if err != nil {
return err return err
} }
req.SetBasicAuth(sc.key, "") req.SetBasicAuth(ac.key, "")
resp, err := sc.client.Do(req) resp, err := ac.client.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -70,12 +69,12 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return nil return nil
} }
func (sc *SegmentClient) IsEnabled() bool { func (ac *AnalyticsClient) IsEnabled() bool {
return len(sc.key) > 0 return len(ac.key) > 0
} }
func (sc *SegmentClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) { func (ac *AnalyticsClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
if !sc.IsEnabled() { if !ac.IsEnabled() {
return return
} else if len(properties) > 1 { } else if len(properties) > 1 {
panic("Track should be called with at most one property map") panic("Track should be called with at most one property map")
@ -87,11 +86,11 @@ func (sc *SegmentClient) Track(userID id.UserID, event string, properties ...map
props = properties[0] props = properties[0]
} }
props["bridge"] = "gmessages" props["bridge"] = "gmessages"
err := sc.trackSync(userID, event, props) err := ac.trackSync(userID, event, props)
if err != nil { if err != nil {
sc.log.Err(err).Str("event", event).Msg("Error tracking event") ac.log.Err(err).Str("event", event).Msg("Error tracking event")
} else { } else {
sc.log.Debug().Str("event", event).Msg("Tracked event") ac.log.Debug().Str("event", event).Msg("Tracked event")
} }
}() }()
} }

View file

@ -24,8 +24,11 @@ import (
type Config struct { type Config struct {
*bridgeconfig.BaseConfig `yaml:",inline"` *bridgeconfig.BaseConfig `yaml:",inline"`
SegmentKey string `yaml:"segment_key"` Analytics struct {
SegmentUserID string `yaml:"segment_user_id"` Host string `yaml:"host"`
Token string `yaml:"token"`
UserID string `yaml:"user_id"`
}
Metrics struct { Metrics struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`

View file

@ -25,8 +25,9 @@ import (
func DoUpgrade(helper *up.Helper) { func DoUpgrade(helper *up.Helper) {
bridgeconfig.Upgrader.DoUpgrade(helper) bridgeconfig.Upgrader.DoUpgrade(helper)
helper.Copy(up.Str|up.Null, "segment_key") helper.Copy(up.Str|up.Null, "analytics", "host")
helper.Copy(up.Str|up.Null, "segment_user_id") helper.Copy(up.Str|up.Null, "analytics", "token")
helper.Copy(up.Str|up.Null, "analytics", "user_id")
helper.Copy(up.Bool, "metrics", "enabled") helper.Copy(up.Bool, "metrics", "enabled")
helper.Copy(up.Str, "metrics", "listen") helper.Copy(up.Str, "metrics", "listen")
@ -104,7 +105,7 @@ var SpacedBlocks = [][]string{
{"appservice", "database"}, {"appservice", "database"},
{"appservice", "id"}, {"appservice", "id"},
{"appservice", "as_token"}, {"appservice", "as_token"},
{"segment_key"}, {"analytics"},
{"metrics"}, {"metrics"},
{"google_messages"}, {"google_messages"},
{"bridge"}, {"bridge"},

View file

@ -76,10 +76,14 @@ appservice:
as_token: "This value is generated when generating the registration" as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration" hs_token: "This value is generated when generating the registration"
# Segment API key to track some events, like provisioning API login and encryption errors. # Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors.
segment_key: null analytics:
# Optional user_id to use when sending Segment events. If null, defaults to using mxID. # Hostname of the tracking server. The path is hardcoded to /v1/track
segment_user_id: null host: api.segment.io
# API key to send with tracking requests. Tracking is disabled if this is null.
token: null
# Optional user ID for tracking events. If null, defaults to using Matrix user ID.
user_id: null
# Prometheus config. # Prometheus config.
metrics: metrics:

20
main.go
View file

@ -18,6 +18,7 @@ package main
import ( import (
_ "embed" _ "embed"
"net/url"
"sync" "sync"
"go.mau.fi/util/configupgrade" "go.mau.fi/util/configupgrade"
@ -81,13 +82,18 @@ func (br *GMBridge) Init() {
util.BrowserDetailsMessage.DeviceType = gmproto.DeviceType(deviceVal) util.BrowserDetailsMessage.DeviceType = gmproto.DeviceType(deviceVal)
} }
Segment.log = br.ZLog.With().Str("component", "segment").Logger() Analytics.log = br.ZLog.With().Str("component", "segment").Logger()
Segment.key = br.Config.SegmentKey Analytics.url = (&url.URL{
Segment.userID = br.Config.SegmentUserID Scheme: "https",
if Segment.IsEnabled() { Host: br.Config.Analytics.Host,
Segment.log.Info().Msg("Segment metrics are enabled") Path: "/v1/track",
if Segment.userID != "" { }).String()
Segment.log.Info().Str("user_id", Segment.userID).Msg("Overriding Segment user ID") Analytics.key = br.Config.Analytics.Token
Analytics.userID = br.Config.Analytics.UserID
if Analytics.IsEnabled() {
Analytics.log.Info().Msg("Analytics are enabled")
if Analytics.userID != "" {
Analytics.log.Info().Str("user_id", Analytics.userID).Msg("Overriding Analytics user ID")
} }
} }

View file

@ -359,11 +359,11 @@ Loop:
switch { switch {
case item.qr != "": case item.qr != "":
log.Debug().Msg("Got code in QR channel") log.Debug().Msg("Got code in QR channel")
Segment.Track(user.MXID, "$qrcode_retrieved") Analytics.Track(user.MXID, "$qrcode_retrieved")
jsonResponse(w, http.StatusOK, LoginResponse{Status: "qr", Code: item.qr}) jsonResponse(w, http.StatusOK, LoginResponse{Status: "qr", Code: item.qr})
case item.err != nil: case item.err != nil:
log.Err(item.err).Msg("Got error in QR channel") log.Err(item.err).Msg("Got error in QR channel")
Segment.Track(user.MXID, "$login_failure") Analytics.Track(user.MXID, "$login_failure")
var resp LoginResponse var resp LoginResponse
switch { switch {
case errors.Is(item.err, ErrLoginTimeout): case errors.Is(item.err, ErrLoginTimeout):
@ -375,7 +375,7 @@ Loop:
jsonResponse(w, http.StatusOK, resp) jsonResponse(w, http.StatusOK, resp)
case item.success: case item.success:
log.Debug().Msg("Got pair success in QR channel") log.Debug().Msg("Got pair success in QR channel")
Segment.Track(user.MXID, "$login_success") Analytics.Track(user.MXID, "$login_success")
jsonResponse(w, http.StatusOK, LoginResponse{Status: "success"}) jsonResponse(w, http.StatusOK, LoginResponse{Status: "success"})
default: default:
log.Error().Any("item_data", item).Msg("Unknown item in QR channel") log.Error().Any("item_data", item).Msg("Unknown item in QR channel")

View file

@ -403,7 +403,7 @@ func (user *User) Login(maxAttempts int) (<-chan qrChannelItem, error) {
user.loginInProgress.Store(false) user.loginInProgress.Store(false)
return nil, fmt.Errorf("failed to connect to Google Messages: %w", err) return nil, fmt.Errorf("failed to connect to Google Messages: %w", err)
} }
Segment.Track(user.MXID, "$login_start") Analytics.Track(user.MXID, "$login_start")
ch := make(chan qrChannelItem, maxAttempts+2) ch := make(chan qrChannelItem, maxAttempts+2)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
user.cancelLogin = cancel user.cancelLogin = cancel