浏览代码

segment: add tracking to provisioning API

Co-authored-by: Tulir Asokan <tulir@maunium.net>
Sumner Evans 3 年之前
父节点
当前提交
a68bce35fc
共有 5 个文件被更改,包括 122 次插入7 次删除
  1. 1 0
      config/config.go
  2. 1 0
      config/upgrade.go
  3. 5 0
      example-config.yaml
  4. 25 7
      provisioning.go
  5. 90 0
      segment.go

+ 1 - 0
config/config.go

@@ -46,6 +46,7 @@ type Config struct {
 		Provisioning struct {
 			Prefix       string `yaml:"prefix"`
 			SharedSecret string `yaml:"shared_secret"`
+			SegmentKey   string `yaml:"segment_key"`
 		} `yaml:"provisioning"`
 
 		ID  string `yaml:"id"`

+ 1 - 0
config/upgrade.go

@@ -54,6 +54,7 @@ func (helper *UpgradeHelper) doUpgrade() {
 	} else {
 		helper.Copy(Str, "appservice", "provisioning", "shared_secret")
 	}
+	helper.Copy(Str|Null, "appservice", "provisioning", "segment_key")
 	helper.Copy(Str, "appservice", "id")
 	helper.Copy(Str, "appservice", "bot", "username")
 	helper.Copy(Str, "appservice", "bot", "displayname")

+ 5 - 0
example-config.yaml

@@ -48,6 +48,11 @@ appservice:
         # Shared secret for authentication. If set to "generate", a random secret will be generated,
         # or if set to "disable", the provisioning API will be disabled.
         shared_secret: generate
+        # Segment API key to enable analytics tracking for web server
+        # endpoints. Set to null to disable.
+        # Currently the only events are login start, QR code retrieve, and login
+        # success/failure.
+        segment_key: null
 
     # The unique ID of this appservice.
     id: whatsapp

+ 25 - 7
provisioning.go

@@ -41,12 +41,17 @@ import (
 )
 
 type ProvisioningAPI struct {
-	bridge *Bridge
-	log    log.Logger
+	bridge  *Bridge
+	log     log.Logger
+	segment *Segment
 }
 
 func (prov *ProvisioningAPI) Init() {
 	prov.log = prov.bridge.Log.Sub("Provisioning")
+
+	// Set up segment
+	prov.segment = NewSegment(prov.bridge.Config.AppService.Provisioning.SegmentKey, prov.log)
+
 	prov.log.Debugln("Enabling provisioning API at", prov.bridge.Config.AppService.Provisioning.Prefix)
 	r := prov.bridge.AS.Router.PathPrefix(prov.bridge.Config.AppService.Provisioning.Prefix).Subrouter()
 	r.Use(prov.AuthMiddleware)
@@ -490,6 +495,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 	user.log.Debugln("Started login via provisioning API")
+	prov.segment.Track(user.MXID, "$login_start")
 
 	for {
 		select {
@@ -498,6 +504,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")
+				prov.segment.Track(user.MXID, "$login_success")
 				_ = c.WriteJSON(map[string]interface{}{
 					"success": true,
 					"jid":     jid,
@@ -505,34 +512,45 @@ 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"
+				prov.segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
 				_ = c.WriteJSON(Error{
 					Error:   "QR code scan timed out. Please try again.",
-					ErrCode: "login timed out",
+					ErrCode: errCode,
 				})
 			case whatsmeow.QRChannelErrUnexpectedEvent.Event:
 				user.log.Debugln("Login via provisioning API failed due to unexpected event")
+				errCode := "unexpected event"
+				prov.segment.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: "unexpected event",
+					ErrCode: errCode,
 				})
 			case whatsmeow.QRChannelClientOutdated.Event:
 				user.log.Debugln("Login via provisioning API failed due to outdated client")
+				errCode := "bridge outdated"
+				prov.segment.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: "bridge outdated",
+					ErrCode: errCode,
 				})
 			case whatsmeow.QRChannelScannedWithoutMultidevice.Event:
+				errCode := "multidevice not enabled"
+				prov.segment.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: "multidevice not enabled",
+					ErrCode: errCode,
 				})
 				continue
 			case "error":
+				errCode := "fatal error"
+				prov.segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
 				_ = c.WriteJSON(Error{
 					Error:   "Fatal error while logging in",
-					ErrCode: "fatal error",
+					ErrCode: errCode,
 				})
 			case "code":
+				prov.segment.Track(user.MXID, "$qrcode_retrieved")
 				_ = c.WriteJSON(map[string]interface{}{
 					"code":    evt.Code,
 					"timeout": int(evt.Timeout.Seconds()),

+ 90 - 0
segment.go

@@ -0,0 +1,90 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2022 Tulir Asokan, Sumner Evans
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+
+	log "maunium.net/go/maulogger/v2"
+	"maunium.net/go/mautrix/id"
+)
+
+const SegmentURL = "https://api.segment.io/v1/track"
+
+type Segment struct {
+	segmentKey string
+	log        log.Logger
+	client     *http.Client
+}
+
+func NewSegment(segmentKey string, parentLogger log.Logger) *Segment {
+	return &Segment{
+		segmentKey: segmentKey,
+		log:        parentLogger.Sub("Segment"),
+		client:     &http.Client{},
+	}
+}
+
+func (segment *Segment) track(userID id.UserID, event string, properties map[string]interface{}) error {
+	data := map[string]interface{}{
+		"userId":     userID,
+		"event":      event,
+		"properties": properties,
+	}
+	json_data, err := json.Marshal(data)
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequest("POST", SegmentURL, bytes.NewBuffer(json_data))
+	if err != nil {
+		return err
+	}
+	req.SetBasicAuth(segment.segmentKey, "")
+	resp, err := segment.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	return nil
+}
+
+func (segment *Segment) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
+	if segment.segmentKey == "" {
+		return
+	}
+	if len(properties) > 1 {
+		segment.log.Fatalf("Track should be called with at most one property map")
+	}
+
+	go (func() error {
+		props := map[string]interface{}{}
+		if len(properties) > 0 {
+			props = properties[0]
+		}
+		props["bridge"] = "whatsapp"
+		err := segment.track(userID, event, props)
+		if err != nil {
+			segment.log.Errorf("Error tracking %s: %v+", event, err)
+			return err
+		}
+		segment.log.Debug("Tracked ", event)
+		return nil
+	})()
+}