Allow sending analytics to custom server

This commit is contained in:
Tulir Asokan 2023-09-29 14:19:48 +03:00
parent e1edb644c0
commit d2110f6ee7
8 changed files with 54 additions and 41 deletions

View File

@ -26,27 +26,26 @@ import (
"maunium.net/go/mautrix/id"
)
const SegmentURL = "https://api.segment.io/v1/track"
type SegmentClient struct {
type AnalyticsClient struct {
url string
key string
userID string
log log.Logger
client http.Client
}
var Segment SegmentClient
var Analytics AnalyticsClient
func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
func (sc *AnalyticsClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
var buf bytes.Buffer
var segmentUserID string
if Segment.userID != "" {
segmentUserID = Segment.userID
var analyticsUserID string
if Analytics.userID != "" {
analyticsUserID = Analytics.userID
} else {
segmentUserID = userID.String()
analyticsUserID = userID.String()
}
err := json.NewEncoder(&buf).Encode(map[string]interface{}{
"userId": segmentUserID,
"userId": analyticsUserID,
"event": event,
"properties": properties,
})
@ -54,7 +53,7 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return err
}
req, err := http.NewRequest("POST", SegmentURL, &buf)
req, err := http.NewRequest(http.MethodPost, sc.url, &buf)
if err != nil {
return err
}
@ -70,11 +69,11 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return nil
}
func (sc *SegmentClient) IsEnabled() bool {
func (sc *AnalyticsClient) IsEnabled() bool {
return len(sc.key) > 0
}
func (sc *SegmentClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
func (sc *AnalyticsClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
if !sc.IsEnabled() {
return
} else if len(properties) > 1 {

View File

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

View File

@ -27,8 +27,9 @@ import (
func DoUpgrade(helper *up.Helper) {
bridgeconfig.Upgrader.DoUpgrade(helper)
helper.Copy(up.Str|up.Null, "segment_key")
helper.Copy(up.Str|up.Null, "segment_user_id")
helper.Copy(up.Str|up.Null, "analytics", "host")
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.Str, "metrics", "listen")
@ -181,7 +182,7 @@ var SpacedBlocks = [][]string{
{"appservice", "database"},
{"appservice", "id"},
{"appservice", "as_token"},
{"segment_key"},
{"analytics"},
{"metrics"},
{"whatsapp"},
{"bridge"},

View File

@ -76,10 +76,14 @@ appservice:
as_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_key: null
# Optional user_id to use when sending Segment events. If null, defaults to using mxID.
segment_user_id: null
# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors.
analytics:
# Hostname of the tracking server. The path is hardcoded to /v1/track
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.
metrics:

20
main.go
View File

@ -19,6 +19,7 @@ package main
import (
_ "embed"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@ -90,13 +91,18 @@ func (br *WABridge) Init() {
br.EventProcessor.On(TypeMSC3381PollResponse, br.MatrixHandler.HandleMessage)
br.EventProcessor.On(TypeMSC3381V2PollResponse, br.MatrixHandler.HandleMessage)
Segment.log = br.Log.Sub("Segment")
Segment.key = br.Config.SegmentKey
Segment.userID = br.Config.SegmentUserID
if Segment.IsEnabled() {
Segment.log.Infoln("Segment metrics are enabled")
if Segment.userID != "" {
Segment.log.Infoln("Overriding Segment user_id with %v", Segment.userID)
Analytics.log = br.Log.Sub("Analytics")
Analytics.url = (&url.URL{
Scheme: "https",
Host: br.Config.Analytics.Host,
Path: "/v1/track",
}).String()
Analytics.key = br.Config.Analytics.Token
Analytics.userID = br.Config.Analytics.UserID
if Analytics.IsEnabled() {
Analytics.log.Infoln("Analytics metrics are enabled")
if Analytics.userID != "" {
Analytics.log.Infoln("Overriding analytics user_id with %v", Analytics.userID)
}
}

View File

@ -773,7 +773,7 @@ func (portal *Portal) handleUndecryptableMessage(source *User, evt *events.Undec
if evt.IsUnavailable {
metricType = "unavailable"
}
Segment.Track(source.MXID, "WhatsApp undecryptable message", map[string]interface{}{
Analytics.Track(source.MXID, "WhatsApp undecryptable message", map[string]interface{}{
"messageID": evt.Info.ID,
"undecryptableType": metricType,
})
@ -849,7 +849,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message, historica
if evt.UnavailableRequestID != "" {
resolveType = "phone"
}
Segment.Track(source.MXID, "WhatsApp undecryptable message resolved", map[string]interface{}{
Analytics.Track(source.MXID, "WhatsApp undecryptable message resolved", map[string]interface{}{
"messageID": evt.Info.ID,
"resolveType": resolveType,
})

View File

@ -692,7 +692,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
}
}
user.log.Debugln("Started login via provisioning API")
Segment.Track(user.MXID, "$login_start")
Analytics.Track(user.MXID, "$login_start")
for {
select {
@ -701,7 +701,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelSuccess.Event:
jid := user.Client.Store.ID
user.log.Debugln("Successful login as", jid, "via provisioning API")
Segment.Track(user.MXID, "$login_success")
Analytics.Track(user.MXID, "$login_success")
_ = c.WriteJSON(map[string]interface{}{
"success": true,
"jid": jid,
@ -711,7 +711,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelTimeout.Event:
user.log.Debugln("Login via provisioning API timed out")
errCode := "login timed out"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "QR code scan timed out. Please try again.",
ErrCode: errCode,
@ -719,7 +719,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelErrUnexpectedEvent.Event:
user.log.Debugln("Login via provisioning API failed due to unexpected event")
errCode := "unexpected event"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Got unexpected event while waiting for QRs, perhaps you're already logged in?",
ErrCode: errCode,
@ -727,14 +727,14 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelClientOutdated.Event:
user.log.Debugln("Login via provisioning API failed due to outdated client")
errCode := "bridge outdated"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Got client outdated error while waiting for QRs. The bridge must be updated to continue.",
ErrCode: errCode,
})
case whatsmeow.QRChannelScannedWithoutMultidevice.Event:
errCode := "multidevice not enabled"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Please enable the WhatsApp multidevice beta and scan the QR code again.",
ErrCode: errCode,
@ -742,13 +742,13 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
continue
case "error":
errCode := "fatal error"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Fatal error while logging in",
ErrCode: errCode,
})
case "code":
Segment.Track(user.MXID, "$qrcode_retrieved")
Analytics.Track(user.MXID, "$qrcode_retrieved")
_ = c.WriteJSON(map[string]interface{}{
"code": evt.Code,
"timeout": int(evt.Timeout.Seconds()),

View File

@ -498,7 +498,7 @@ func (user *User) createClient(sess *store.Device) {
user.Client.SetForceActiveDeliveryReceipts(user.bridge.Config.Bridge.ForceActiveDeliveryReceipts)
user.Client.AutomaticMessageRerequestFromPhone = true
user.Client.GetMessageForRetry = func(requester, to types.JID, id types.MessageID) *waProto.Message {
Segment.Track(user.MXID, "WhatsApp incoming retry (message not found)", map[string]interface{}{
Analytics.Track(user.MXID, "WhatsApp incoming retry (message not found)", map[string]interface{}{
"requester": user.obfuscateJID(requester),
"messageID": id,
})
@ -506,7 +506,7 @@ func (user *User) createClient(sess *store.Device) {
return nil
}
user.Client.PreRetryCallback = func(receipt *events.Receipt, messageID types.MessageID, retryCount int, msg *waProto.Message) bool {
Segment.Track(user.MXID, "WhatsApp incoming retry (accepted)", map[string]interface{}{
Analytics.Track(user.MXID, "WhatsApp incoming retry (accepted)", map[string]interface{}{
"requester": user.obfuscateJID(receipt.Sender),
"messageID": messageID,
"retryCount": retryCount,