From bc22852f06767b630d6a71be0874d0245928ccbf Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 12 Sep 2024 16:53:50 +0300 Subject: [PATCH] bridgev2: add analytics sending method --- bridgev2/bridgeconfig/config.go | 7 +++ bridgev2/bridgeconfig/upgrade.go | 5 ++ bridgev2/matrix/analytics.go | 62 ++++++++++++++++++++++ bridgev2/matrix/connector.go | 3 ++ bridgev2/matrix/mxmain/example-config.yaml | 9 ++++ bridgev2/matrixinterface.go | 4 ++ bridgev2/user.go | 11 ++++ 7 files changed, 101 insertions(+) create mode 100644 bridgev2/matrix/analytics.go diff --git a/bridgev2/bridgeconfig/config.go b/bridgev2/bridgeconfig/config.go index 051e6a00..1731688d 100644 --- a/bridgev2/bridgeconfig/config.go +++ b/bridgev2/bridgeconfig/config.go @@ -21,6 +21,7 @@ type Config struct { Homeserver HomeserverConfig `yaml:"homeserver"` AppService AppserviceConfig `yaml:"appservice"` Matrix MatrixConfig `yaml:"matrix"` + Analytics AnalyticsConfig `yaml:"analytics"` Provisioning ProvisioningConfig `yaml:"provisioning"` PublicMedia PublicMediaConfig `yaml:"public_media"` DirectMedia DirectMediaConfig `yaml:"direct_media"` @@ -78,6 +79,12 @@ type MatrixConfig struct { UploadFileThreshold int64 `yaml:"upload_file_threshold"` } +type AnalyticsConfig struct { + Token string `yaml:"token"` + URL string `yaml:"url"` + UserID string `yaml:"user_id"` +} + type ProvisioningConfig struct { Prefix string `yaml:"prefix"` SharedSecret string `yaml:"shared_secret"` diff --git a/bridgev2/bridgeconfig/upgrade.go b/bridgev2/bridgeconfig/upgrade.go index d6ccf007..4491dedd 100644 --- a/bridgev2/bridgeconfig/upgrade.go +++ b/bridgev2/bridgeconfig/upgrade.go @@ -87,6 +87,10 @@ func doUpgrade(helper up.Helper) { helper.Copy(up.Bool, "matrix", "federate_rooms") helper.Copy(up.Int, "matrix", "upload_file_threshold") + helper.Copy(up.Str|up.Null, "analytics", "token") + helper.Copy(up.Str|up.Null, "analytics", "url") + helper.Copy(up.Str|up.Null, "analytics", "user_id") + helper.Copy(up.Str, "provisioning", "prefix") if secret, ok := helper.Get(up.Str, "provisioning", "shared_secret"); !ok || secret == "generate" { sharedSecret := random.String(64) @@ -176,6 +180,7 @@ var SpacedBlocks = [][]string{ {"appservice", "as_token"}, {"appservice", "username_template"}, {"matrix"}, + {"analytics"}, {"provisioning"}, {"public_media"}, {"direct_media"}, diff --git a/bridgev2/matrix/analytics.go b/bridgev2/matrix/analytics.go new file mode 100644 index 00000000..92ea2104 --- /dev/null +++ b/bridgev2/matrix/analytics.go @@ -0,0 +1,62 @@ +package matrix + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "maunium.net/go/mautrix/id" +) + +func (br *Connector) trackSync(userID id.UserID, event string, properties map[string]any) error { + var buf bytes.Buffer + var analyticsUserID string + if br.Config.Analytics.UserID != "" { + analyticsUserID = br.Config.Analytics.UserID + } else { + analyticsUserID = userID.String() + } + err := json.NewEncoder(&buf).Encode(map[string]any{ + "userId": analyticsUserID, + "event": event, + "properties": properties, + }) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodPost, br.Config.Analytics.URL, &buf) + if err != nil { + return err + } + req.SetBasicAuth(br.Config.Analytics.Token, "") + resp, err := br.AS.HTTPClient.Do(req) + if err != nil { + return err + } + _ = resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code %d", resp.StatusCode) + } + return nil +} + +func (br *Connector) Track(userID id.UserID, event string, props map[string]any) { + if br.Config.Analytics.Token == "" || br.Config.Analytics.URL == "" { + return + } + + if props == nil { + props = map[string]any{} + } + props["bridge"] = br.Bridge.Network.GetName().BeeperBridgeType + go func() { + err := br.trackSync(userID, event, props) + if err != nil { + br.Log.Err(err).Str("component", "analytics").Str("event", event).Msg("Error tracking event") + } else { + br.Log.Debug().Str("component", "analytics").Str("event", event).Msg("Tracked event") + } + }() +} diff --git a/bridgev2/matrix/connector.go b/bridgev2/matrix/connector.go index 4297cba7..c5df2421 100644 --- a/bridgev2/matrix/connector.go +++ b/bridgev2/matrix/connector.go @@ -103,6 +103,9 @@ var ( _ bridgev2.MatrixConnector = (*Connector)(nil) _ bridgev2.MatrixConnectorWithServer = (*Connector)(nil) _ bridgev2.MatrixConnectorWithPostRoomBridgeHandling = (*Connector)(nil) + _ bridgev2.MatrixConnectorWithPublicMedia = (*Connector)(nil) + _ bridgev2.MatrixConnectorWithNameDisambiguation = (*Connector)(nil) + _ bridgev2.MatrixConnectorWithAnalytics = (*Connector)(nil) ) func NewConnector(cfg *bridgeconfig.Config) *Connector { diff --git a/bridgev2/matrix/mxmain/example-config.yaml b/bridgev2/matrix/mxmain/example-config.yaml index 31490bb3..7e642f96 100644 --- a/bridgev2/matrix/mxmain/example-config.yaml +++ b/bridgev2/matrix/mxmain/example-config.yaml @@ -205,6 +205,15 @@ matrix: # rather than keeping the whole file in memory. upload_file_threshold: 5242880 +# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. +analytics: + # API key to send with tracking requests. Tracking is disabled if this is null. + token: null + # Address to send tracking requests to. + url: https://api.segment.io/v1/track + # Optional user ID for tracking events. If null, defaults to using Matrix user ID. + user_id: null + # Settings for provisioning API provisioning: # Prefix for the provisioning API paths. diff --git a/bridgev2/matrixinterface.go b/bridgev2/matrixinterface.go index 4473b74e..6ff69250 100644 --- a/bridgev2/matrixinterface.go +++ b/bridgev2/matrixinterface.go @@ -73,6 +73,10 @@ type MatrixConnectorWithPostRoomBridgeHandling interface { HandleNewlyBridgedRoom(ctx context.Context, roomID id.RoomID) error } +type MatrixConnectorWithAnalytics interface { + Track(userID id.UserID, event string, properties map[string]any) +} + type MatrixSendExtra struct { Timestamp time.Time MessageMeta *database.Message diff --git a/bridgev2/user.go b/bridgev2/user.go index 5c2344e8..fbb8095e 100644 --- a/bridgev2/user.go +++ b/bridgev2/user.go @@ -253,3 +253,14 @@ func (user *User) GetManagementRoom(ctx context.Context) (id.RoomID, error) { func (user *User) Save(ctx context.Context) error { return user.Bridge.DB.User.Update(ctx, user.User) } + +func (br *Bridge) Track(userID id.UserID, event string, props map[string]any) { + analyticSender, ok := br.Matrix.(MatrixConnectorWithAnalytics) + if ok { + analyticSender.Track(userID, event, props) + } +} + +func (user *User) Track(event string, props map[string]any) { + user.Bridge.Track(user.MXID, event, props) +}