diff --git a/Makefile b/Makefile index ab78ab12..32f46570 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ DLV_DEBUG_PORT := 2346 DEFAULT_GOOS := $(shell go env GOOS) DEFAULT_GOARCH := $(shell go env GOARCH) INCLUDE_FFMPEG ?= +BUILD_HASH = $(shell git rev-parse HEAD) +LDFLAGS += -X "main.buildHash=$(BUILD_HASH)" +LDFLAGS += -X "main.isDebug=$(MM_DEBUG)" +LDFLAGS += -X "main.rudderWriteKey=$(MM_RUDDER_COPILOT_PROD)" +LDFLAGS += -X "main.rudderDataplaneURL=$(MM_RUDDER_DATAPLANE_URL)" +GO_BUILD_FLAGS += -ldflags '$(LDFLAGS)' export GO111MODULE=on diff --git a/go.mod b/go.mod index cbcf6e3b..ae3ddf4d 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 github.com/r3labs/sse/v2 v2.10.0 + github.com/rudderlabs/analytics-go v3.3.3+incompatible github.com/sashabaranov/go-openai v1.25.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.8.4 @@ -72,7 +73,11 @@ require ( github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/segmentio/backo-go v1.0.1 // indirect github.com/stretchr/objx v0.5.1 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect github.com/tinylib/msgp v1.1.9 // indirect github.com/trivago/tgo v1.0.7 // indirect github.com/ugorji/go/codec v1.1.7 // indirect @@ -80,6 +85,7 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index 396a2fd5..53200437 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= @@ -222,9 +224,13 @@ github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rudderlabs/analytics-go v3.3.3+incompatible h1:OG0XlKoXfr539e2t1dXtTB+Gr89uFW+OUNQBVhHIIBY= +github.com/rudderlabs/analytics-go v3.3.3+incompatible/go.mod h1:LF8/ty9kUX4PTY3l5c97K3nZZaX5Hwsvt+NBaRL/f30= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sashabaranov/go-openai v1.25.0 h1:3h3DtJ55zQJqc+BR4y/iTcPhLk4pewJpyO+MXW2RdW0= github.com/sashabaranov/go-openai v1.25.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= +github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -270,6 +276,13 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= @@ -287,6 +300,8 @@ github.com/wiggin77/merror v1.0.5 h1:P+lzicsn4vPMycAf2mFf7Zk6G9eco5N+jB1qJ2XW3ME github.com/wiggin77/merror v1.0.5/go.mod h1:H2ETSu7/bPE0Ymf4bEwdUoo73OOEkdClnoRisfw0Nm0= github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= diff --git a/server/api.go b/server/api.go index 7b36d9af..f33ecad7 100644 --- a/server/api.go +++ b/server/api.go @@ -16,6 +16,8 @@ const ( ContextPostKey = "post" ContextChannelKey = "channel" ContextBotKey = "bot" + + requestBodyMaxSizeBytes = 1024 * 1024 // 1MB ) func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { @@ -26,6 +28,7 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req router.GET("/ai_threads", p.handleGetAIThreads) router.GET("/ai_bots", p.handleGetAIBots) + router.POST("/telemetry/track", p.handleTrackEvent) botRequriedRouter := router.Group("") botRequriedRouter.Use(p.aiBotRequired) diff --git a/server/api_channel.go b/server/api_channel.go index a826bae8..a7d9b30b 100644 --- a/server/api_channel.go +++ b/server/api_channel.go @@ -103,6 +103,13 @@ func (p *Plugin) handleSince(c *gin.Context) { return } + p.track(evUnreadMessages, map[string]any{ + "channel_id": channel.Id, + "user_actual_id": user.Id, + "since": data.Since, + "type": promptPreset, + }) + prompt, err := p.prompts.ChatCompletion(promptPreset, context, p.getDefaultToolsStore(context.IsDMWithBot())) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) diff --git a/server/api_post.go b/server/api_post.go index 1fc436e2..60a60098 100644 --- a/server/api_post.go +++ b/server/api_post.go @@ -115,6 +115,12 @@ func (p *Plugin) handleSummarize(c *gin.Context) { return } + p.track(evSummarizeThread, map[string]any{ + "channel_id": channel.Id, + "post_id": post.Id, + "user_actual_id": user.Id, + }) + createdPost, err := p.startNewSummaryThread(bot, post.Id, p.MakeConversationContext(bot, user, channel, nil)) if err != nil { c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to produce summary: %w", err)) @@ -198,6 +204,12 @@ func (p *Plugin) handleSummarizeTranscription(c *gin.Context) { return } + p.track(evSummarizeTranscription, map[string]any{ + "channel_id": channel.Id, + "post_id": post.Id, + "user_actual_id": user.Id, + }) + createdPost, err := p.newCallTranscriptionSummaryThread(bot, user, post, channel) if err != nil { c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("unable to summarize transcription: %w", err)) diff --git a/server/configuration.go b/server/configuration.go index ec70aa25..68fd930a 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -85,6 +85,15 @@ func (p *Plugin) setConfiguration(configuration *configuration) { // OnConfigurationChange is invoked when configuration changes may have been made. func (p *Plugin) OnConfigurationChange() error { + serverConfig := p.API.GetConfig() + if serverConfig != nil { + if err := p.initTelemetry(serverConfig.LogSettings.EnableDiagnostics); err != nil { + p.API.LogError(err.Error()) + } + } else { + p.API.LogError("OnConfigurationChange: failed to get server config") + } + var configuration = new(configuration) // Load the public configuration fields from the Mattermost server configuration. diff --git a/server/main.go b/server/main.go index cf8238e6..46039ddd 100644 --- a/server/main.go +++ b/server/main.go @@ -4,6 +4,10 @@ import ( "github.com/mattermost/mattermost/server/public/plugin" ) +var buildHash string +var rudderWriteKey string +var rudderDataplaneURL string + func main() { plugin.ClientMain(&Plugin{}) } diff --git a/server/plugin.go b/server/plugin.go index 96cf177b..2f09e2d5 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -18,6 +18,7 @@ import ( "github.com/mattermost/mattermost-plugin-ai/server/ai/openai" "github.com/mattermost/mattermost-plugin-ai/server/enterprise" "github.com/mattermost/mattermost-plugin-ai/server/metrics" + "github.com/mattermost/mattermost-plugin-ai/server/telemetry" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/pluginapi" @@ -50,6 +51,9 @@ type Plugin struct { pluginAPI *pluginapi.Client + telemetry *telemetry.Client + telemetryMut sync.RWMutex + ffmpegPath string db *sqlx.DB @@ -127,6 +131,13 @@ func (p *Plugin) OnActivate() error { return nil } +func (p *Plugin) OnDeactivate() error { + if err := p.uninitTelemetry(); err != nil { + p.API.LogError(err.Error()) + } + return nil +} + func (p *Plugin) getLLM(llmBotConfig ai.BotConfig) ai.LanguageModel { metrics := p.metricsService.GetMetricsForAIService(llmBotConfig.Name) @@ -248,6 +259,12 @@ func (p *Plugin) handleMentions(bot *Bot, post *model.Post, postingUser *model.U return err } + p.track(evAIBotMention, map[string]any{ + "actual_user_id": postingUser.Id, + "bot_id": bot.mmBot.UserId, + "bot_service_type": bot.cfg.Service.Type, + }) + if err := p.processUserRequestToBot(bot, p.MakeConversationContext(bot, postingUser, channel, post)); err != nil { return fmt.Errorf("unable to process bot mention: %w", err) } @@ -260,6 +277,20 @@ func (p *Plugin) handleDMs(bot *Bot, channel *model.Channel, postingUser *model. return err } + if post.RootId == "" { + p.track(evUserStartedConversation, map[string]any{ + "user_actual_id": postingUser.Id, + "bot_id": bot.mmBot.UserId, + "bot_service_type": bot.cfg.Service.Type, + }) + } else { + p.track(evContinueConversation, map[string]any{ + "user_actual_id": postingUser.Id, + "bot_id": bot.mmBot.UserId, + "bot_service_type": bot.cfg.Service.Type, + }) + } + if err := p.processUserRequestToBot(bot, p.MakeConversationContext(bot, postingUser, channel, post)); err != nil { return fmt.Errorf("unable to process bot DM: %w", err) } diff --git a/server/telemetry.go b/server/telemetry.go new file mode 100644 index 00000000..04bc9bdf --- /dev/null +++ b/server/telemetry.go @@ -0,0 +1,165 @@ +// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import ( + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/mattermost/mattermost-plugin-ai/server/telemetry" + "github.com/mattermost/mattermost/server/public/model" + "github.com/rudderlabs/analytics-go" +) + +const ( + // server-side events + evUserStartedConversation = "user_started_conversation" + evContinueConversation = "continue_conversation" + evAIBotMention = "ai_bot_mention" + evUnreadMessages = "unread_messages" + evSummarizeThread = "summarize_thread" + evSummarizeTranscription = "summarize_transcription" +) + +var ( + telemetryClientTypes = []string{"web", "mobile", "desktop"} + telemetryClientEvents = []string{ + "copilot_apps_bar_clicked", + } + telemetryClientTypesMap map[string]struct{} + telemetryClientEventsMap map[string]struct{} + + enterpriseSKUs = []string{model.LicenseShortSkuEnterprise} + // currently unused + // professionalSKUs = []string{model.LicenseShortSkuProfessional, model.LicenseShortSkuEnterprise} + + // We only need to map events that require a SKU (i.e., licensed features). Anything available on unlicensed + // servers will map to null as expected. + eventToSkusMap = map[string][]string{ + evUnreadMessages: enterpriseSKUs, + evSummarizeThread: enterpriseSKUs, + evSummarizeTranscription: enterpriseSKUs, + } +) + +func init() { + telemetryClientEventsMap = make(map[string]struct{}, len(telemetryClientEvents)) + for _, eventType := range telemetryClientEvents { + telemetryClientEventsMap[eventType] = struct{}{} + } + telemetryClientTypesMap = make(map[string]struct{}, len(telemetryClientTypes)) + for _, clientType := range telemetryClientTypes { + telemetryClientTypesMap[clientType] = struct{}{} + } +} + +type trackEventRequest struct { + Event string `json:"event"` + ClientType string `json:"clientType"` + Source string `json:"source"` + Props map[string]any `json:"props"` +} + +type eventFeature struct { + Name string `json:"name"` + Skus []string `json:"skus"` +} + +func (p *Plugin) track(ev string, props map[string]any) { + p.telemetryMut.RLock() + defer p.telemetryMut.RUnlock() + if p.telemetry == nil { + return + } + + ctx := &analytics.Context{ + Extra: map[string]any{ + "feature": eventFeature{ + Name: "Copilot", + Skus: eventToSkusMap[ev], + }, + }, + } + + if err := p.telemetry.Track(ev, props, ctx); err != nil { + p.API.LogError(err.Error()) + } +} + +func (p *Plugin) uninitTelemetry() error { + p.telemetryMut.Lock() + defer p.telemetryMut.Unlock() + if p.telemetry == nil { + return nil + } + return p.telemetry.Close() +} + +func (p *Plugin) initTelemetry(enableDiagnostics *bool) error { + p.telemetryMut.Lock() + defer p.telemetryMut.Unlock() + if p.telemetry == nil && enableDiagnostics != nil && *enableDiagnostics { + p.API.LogDebug("Initializing telemetry") + // setup telemetry + client, err := telemetry.NewClient(telemetry.ClientConfig{ + WriteKey: rudderWriteKey, + DataplaneURL: rudderDataplaneURL, + DiagnosticID: p.API.GetDiagnosticId(), + DefaultProps: map[string]any{ + "ServerVersion": p.API.GetServerVersion(), + "PluginVersion": manifest.Version, + "PluginBuild": buildHash, + }, + }) + if err != nil { + return err + } + p.telemetry = client + } else if p.telemetry != nil && (enableDiagnostics == nil || !*enableDiagnostics) { + p.API.LogDebug("Deinitializing telemetry") + // destroy telemetry + if err := p.telemetry.Close(); err != nil { + return err + } + p.telemetry = nil + } + return nil +} + +func (p *Plugin) handleTrackEvent(c *gin.Context) { + p.telemetryMut.RLock() + telemetryEnabled := p.telemetry != nil + p.telemetryMut.RUnlock() + + if !telemetryEnabled { + return + } + + var data trackEventRequest + if err := json.NewDecoder(http.MaxBytesReader(c.Writer, c.Request.Body, requestBodyMaxSizeBytes)).Decode(&data); err != nil { + return + } + + if _, ok := telemetryClientEventsMap[data.Event]; !ok { + return + } + + if _, ok := telemetryClientTypesMap[data.ClientType]; !ok { + return + } + + if data.Props == nil { + data.Props = map[string]any{} + } + + if data.Source != "" { + data.Props["Source"] = data.Source + } + + data.Props["ActualUserID"] = c.GetHeader("Mattermost-User-Id") + data.Props["ClientType"] = data.ClientType + + p.track(data.Event, data.Props) +} diff --git a/server/telemetry/telemetry.go b/server/telemetry/telemetry.go new file mode 100644 index 00000000..23c730c1 --- /dev/null +++ b/server/telemetry/telemetry.go @@ -0,0 +1,77 @@ +// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package telemetry + +import ( + "fmt" + + "github.com/rudderlabs/analytics-go" +) + +type ClientConfig struct { + WriteKey string + DataplaneURL string + DiagnosticID string + DefaultProps map[string]any +} + +func (c *ClientConfig) isValid() error { + if c.WriteKey == "" { + return fmt.Errorf("WriteKey should not be empty") + } + + if c.DataplaneURL == "" { + return fmt.Errorf("DataplaneURL should not be empty") + } + + if c.DiagnosticID == "" { + return fmt.Errorf("DiagnosticID should not be empty") + } + + return nil +} + +type Client struct { + config ClientConfig + client analytics.Client +} + +func NewClient(config ClientConfig) (*Client, error) { + if err := config.isValid(); err != nil { + return nil, fmt.Errorf("telemetry: config validation failed: %w", err) + } + + return &Client{ + config: config, + client: analytics.New(config.WriteKey, config.DataplaneURL), + }, nil +} + +func (c *Client) Track(event string, props map[string]any, ctx *analytics.Context) error { + if props == nil { + props = map[string]any{} + } + + for k, v := range c.config.DefaultProps { + props[k] = v + } + + if err := c.client.Enqueue(analytics.Track{ + Event: event, + UserId: c.config.DiagnosticID, + Properties: props, + Context: ctx, + }); err != nil { + return fmt.Errorf("telemetry: failed to track event: %w", err) + } + + return nil +} + +func (c *Client) Close() error { + if err := c.client.Close(); err != nil { + return fmt.Errorf("telemetry: failed to close client: %w", err) + } + return nil +} diff --git a/webapp/src/client.tsx b/webapp/src/client.tsx index ef0cbd04..ae7b7152 100644 --- a/webapp/src/client.tsx +++ b/webapp/src/client.tsx @@ -205,6 +205,31 @@ export async function getAIBots() { }); } +export async function trackEvent(event: string, source: string, props?: Record) { + const url = `${baseRoute()}/telemetry/track`; + const userAgent = window.navigator.userAgent; + const clientType = (userAgent.indexOf('Mattermost') === -1 || userAgent.indexOf('Electron') === -1) ? 'web' : 'desktop'; + const response = await fetch(url, Client4.getOptions({ + method: 'POST', + body: JSON.stringify({ + event, + source, + clientType, + props: props || {}, + }), + })); + + if (response.ok) { + return response.json(); + } + + throw new ClientError(Client4.url, { + message: '', + status_code: response.status, + url, + }); +} + export async function createPost(post: any) { const created = await Client4.createPost(post); return created; diff --git a/webapp/src/constants.ts b/webapp/src/constants.ts index db3d954e..35649b73 100644 --- a/webapp/src/constants.ts +++ b/webapp/src/constants.ts @@ -3,3 +3,11 @@ export const OVERLAY_DELAY = 400; export const BotUsername = 'ai'; + +export const TelemetryEvents = { + CopilotAppsBarClicked: 'copilot_apps_bar_clicked', +}; + +export const TelemetrySources = { + Widget: 'widget', +}; diff --git a/webapp/src/index.tsx b/webapp/src/index.tsx index 10f3576c..cea41804 100644 --- a/webapp/src/index.tsx +++ b/webapp/src/index.tsx @@ -16,9 +16,9 @@ import IconThreadSummarization from './components/assets/icon_thread_summarizati import IconReactForMe from './components/assets/icon_react_for_me'; import RHS from './components/rhs/rhs'; import Config from './components/system_console/config'; -import {doReaction, doSummarize, getAIDirectChannel} from './client'; +import {doReaction, doSummarize, getAIDirectChannel, trackEvent} from './client'; import {setOpenRHSAction} from './redux_actions'; -import {BotUsername} from './constants'; +import {BotUsername, TelemetryEvents, TelemetrySources} from './constants'; import PostEventListener from './websocket'; import {setupRedux} from './redux'; import UnreadsSumarize from './components/unreads_summarize'; @@ -156,6 +156,9 @@ export default class Plugin { registry.registerAdminConsoleCustomSetting('Config', Config); if (rhs) { registry.registerChannelHeaderButtonAction(, () => { + trackEvent(TelemetryEvents.CopilotAppsBarClicked, TelemetrySources.Widget, { + user_id: store.getState().entities.users.currentUserId, + }); store.dispatch(rhs.toggleRHSPlugin); }, 'Copilot',