From 662e59865c68de0cdafd084b0d7df81b445784b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Vay=C3=A1?= Date: Mon, 7 Oct 2024 13:30:19 +0200 Subject: [PATCH] MM-59360 add telemetry for paid features related to guests (#28295) * add telemetry for paid features related to guests --------- Co-authored-by: Mattermost Build --- server/channels/app/channel.go | 8 +++ server/channels/app/notification.go | 22 +++++++ .../platform/services/telemetry/telemetry.go | 54 +++++++++++++++- .../services/telemetry/telemetry_test.go | 64 +++++++++++++------ 4 files changed, 126 insertions(+), 22 deletions(-) diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go index 8664c33c2e2be..d93735037e863 100644 --- a/server/channels/app/channel.go +++ b/server/channels/app/channel.go @@ -13,6 +13,7 @@ import ( "strings" "github.com/mattermost/mattermost/server/v8/channels/utils" + "github.com/mattermost/mattermost/server/v8/platform/services/telemetry" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" @@ -1568,6 +1569,13 @@ func (a *App) addUserToChannel(c request.CTX, user *model.User, channel *model.C return nil, model.NewAppError("AddUserToChannel", "app.channel_member_history.log_join_event.internal_error", nil, "", http.StatusInternalServerError).Wrap(nErr) } + if user.IsGuest() { + a.Srv().telemetryService.SendTelemetryForFeature( + telemetry.TrackGuestFeature, + "add_guest_to_channel", + map[string]any{"user_actual_id": user.Id}) + } + a.Srv().Platform().InvalidateChannelCacheForUser(user.Id) a.invalidateCacheForChannelMembers(channel.Id) diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index 23cc0b05e5110..3d45b1d74fb1a 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -19,6 +19,7 @@ import ( "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/store" + "github.com/mattermost/mattermost/server/v8/platform/services/telemetry" ) func (a *App) canSendPushNotifications() bool { @@ -877,6 +878,27 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea mlog.String("post_id", post.Id), ) + for id, reason := range mentions.Mentions { + user, ok := profileMap[id] + if !ok { + continue + } + if user.IsGuest() { + if reason == KeywordMention { + a.Srv().telemetryService.SendTelemetryForFeature( + telemetry.TrackGuestFeature, + "post_mentioned_guest", + map[string]any{"user_actual_id": user.Id, "post_owner_id": sender.Id}, + ) + } else if reason == DMMention { + a.Srv().telemetryService.SendTelemetryForFeature( + telemetry.TrackGuestFeature, + "direct_message_to_guest", + map[string]any{"user_actual_id": user.Id, "post_owner_id": sender.Id}, + ) + } + } + } return mentionedUsersList, nil } diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index 9103234d7446b..70beddd33362e 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -93,6 +93,17 @@ const ( TrackPlugins = "plugins" ) +type TrackSKU string + +const ( + TrackProfessionalSKU TrackSKU = "professional" + TrackEnterpriseSKU TrackSKU = "enterprise" +) + +type TrackFeature string + +const TrackGuestFeature TrackFeature = "guest_accounts" + type ServerIface interface { Config() *model.Config IsLeader() bool @@ -119,6 +130,15 @@ type RudderConfig struct { DataplaneURL string } +type EventFeature struct { + Name TrackFeature `json:"name"` + SKUS []TrackSKU `json:"skus"` +} + +var featureSKUS = map[TrackFeature][]TrackSKU{ + TrackGuestFeature: {TrackProfessionalSKU, TrackEnterpriseSKU}, +} + func New(srv ServerIface, dbStore store.Store, searchEngine *searchengine.Broker, log *mlog.Logger, verbose bool) (*TelemetryService, error) { service := &TelemetryService{ srv: srv, @@ -202,7 +222,7 @@ func (ts *TelemetryService) sendDailyTelemetry(override bool) { func (ts *TelemetryService) SendTelemetry(event string, properties map[string]any) { if ts.rudderClient != nil { var context *rudder.Context - // if we are part of a cloud installation, add it's ID to the tracked event's context + // if we are part of a cloud installation, add its ID to the tracked event's context if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" { context = &rudder.Context{Traits: map[string]any{"installationId": installationId}} } @@ -218,6 +238,38 @@ func (ts *TelemetryService) SendTelemetry(event string, properties map[string]an } } +func (ts *TelemetryService) SendTelemetryForFeature(featureName TrackFeature, event string, properties map[string]any) { + if ts.rudderClient != nil { + skus, ok := featureSKUS[featureName] + if !ok { + skus = []TrackSKU{} + mlog.Warn("Telemetry SKUS are not defined for the feature", mlog.String("feature", featureName)) + } + feature := EventFeature{ + Name: featureName, + SKUS: skus, + } + + var context *rudder.Context = &rudder.Context{ + Extra: map[string]any{"feature": feature}, + } + // if we are part of a cloud installation, add its ID to the tracked event's context + if installationId := os.Getenv("MM_CLOUD_INSTALLATION_ID"); installationId != "" { + context.Traits = map[string]any{"installationId": installationId} + } + + err := ts.rudderClient.Enqueue(rudder.Track{ + Event: event, + UserId: ts.TelemetryID, + Properties: properties, + Context: context, + }) + if err != nil { + ts.log.Warn("Error sending telemetry for feature", mlog.Err(err)) + } + } +} + func isDefaultArray(setting, defaultValue []string) bool { if len(setting) != len(defaultValue) { return false diff --git a/server/platform/services/telemetry/telemetry_test.go b/server/platform/services/telemetry/telemetry_test.go index 8012a089a1304..edce41b536191 100644 --- a/server/platform/services/telemetry/telemetry_test.go +++ b/server/platform/services/telemetry/telemetry_test.go @@ -35,17 +35,20 @@ type FakeConfigService struct { cfg *model.Config } +type testBatch struct { + MessageId string + UserId string + Event string + Timestamp time.Time + Properties map[string]any + Context map[string]any +} + type testTelemetryPayload struct { MessageId string SentAt time.Time - Batch []struct { - MessageId string - UserId string - Event string - Timestamp time.Time - Properties map[string]any - } - Context struct { + Batch []testBatch + Context struct { Library struct { Name string Version string @@ -53,15 +56,7 @@ type testTelemetryPayload struct { } } -type testBatch struct { - MessageId string - UserId string - Event string - Timestamp time.Time - Properties map[string]any -} - -func assertPayload(t *testing.T, actual testTelemetryPayload, event string, properties map[string]any) { +func assertPayload(t *testing.T, actual testTelemetryPayload, event string, properties map[string]any, featureContext map[string]any) { t.Helper() assert.NotEmpty(t, actual.MessageId) assert.False(t, actual.SentAt.IsZero()) @@ -75,6 +70,11 @@ func assertPayload(t *testing.T, actual testTelemetryPayload, event string, prop if properties != nil { assert.Equal(t, properties, actual.Batch[0].Properties) } + if featureContext != nil { + actualFeature := actual.Batch[0].Context["feature"].(map[string]any) + assert.Equal(t, featureContext["name"], actualFeature["name"], "feature name must match") + assert.Equal(t, featureContext["skus"], actualFeature["skus"], "SKUs must match") + } } assert.Equal(t, "analytics-go", actual.Context.Library.Name) assert.Equal(t, "3.3.0", actual.Context.Library.Version) @@ -85,7 +85,7 @@ func collectBatches(t *testing.T, info *[]testBatch, pchan chan testTelemetryPay for { select { case result := <-pchan: - assertPayload(t, result, "", nil) + assertPayload(t, result, "", nil, nil) *info = append(*info, result.Batch[0]) case <-time.After(2 * time.Second): return @@ -126,7 +126,7 @@ func makeTelemetryServiceAndReceiver(t *testing.T, cloudLicense bool) (*Telemetr // initializing rudder send a client identify message select { case identifyMessage := <-pchan: - assertPayload(t, identifyMessage, "", nil) + assertPayload(t, identifyMessage, "", nil, nil) case <-time.After(2 * time.Second): require.Fail(t, "Did not receive ID message") } @@ -435,7 +435,7 @@ func TestRudderTelemetry(t *testing.T) { for { select { case result := <-pchan: - assertPayload(t, result, "", nil) + assertPayload(t, result, "", nil, nil) *info = append(*info, result.Batch[0].Event) case <-time.After(2 * time.Second): return @@ -452,7 +452,29 @@ func TestRudderTelemetry(t *testing.T) { case result := <-pchan: assertPayload(t, result, "Testing Telemetry", map[string]any{ "hey": testValue, - }) + }, nil) + case <-time.After(2 * time.Second): + require.Fail(t, "Did not receive telemetry") + } + }) + + t.Run("Send Feature", func(t *testing.T) { + const testEvent = "test-send-value-4567" + const testProperty = "test-property-9876" + + service.SendTelemetryForFeature(TrackGuestFeature, testEvent, map[string]any{ + "prop": testProperty, + }) + + select { + case result := <-pchan: + assertPayload( + t, + result, + testEvent, + map[string]any{"prop": testProperty}, + map[string]any{"skus": []any{"professional", "enterprise"}, "name": "guest_accounts"}, + ) case <-time.After(2 * time.Second): require.Fail(t, "Did not receive telemetry") }