From f812104a92b5a497217a954bade5d8289a484450 Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:09:52 +0530 Subject: [PATCH] MM-60331 - Scheduled post telemetry (#29132) * Added scheduled posts telemetry * Tracking scheudled post creation via webapp to filter by platform * Fixed keys * Lint fix --- server/channels/app/scheduled_post_job.go | 16 +++++++++++++ .../platform/services/telemetry/telemetry.go | 2 ++ .../send_post_options/core_menu_options.tsx | 24 ++++++++++++++++++- .../src/components/drafts/draft_row.tsx | 7 +++--- .../src/actions/scheduled_posts.ts | 4 ++-- .../mattermost-redux/src/client/rudder.ts | 8 +++---- .../src/constants/telemetry.ts | 5 ++++ webapp/platform/client/src/client4.ts | 18 ++++++++++---- webapp/platform/types/src/posts.ts | 4 +++- 9 files changed, 73 insertions(+), 15 deletions(-) diff --git a/server/channels/app/scheduled_post_job.go b/server/channels/app/scheduled_post_job.go index 6f03a50824..81a5ba8265 100644 --- a/server/channels/app/scheduled_post_job.go +++ b/server/channels/app/scheduled_post_job.go @@ -8,6 +8,8 @@ import ( "net/http" "time" + "github.com/mattermost/mattermost/server/v8/platform/services/telemetry" + "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" @@ -337,6 +339,12 @@ func (a *App) handleSuccessfulScheduledPosts(rctx request.CTX, successfulSchedul ) return errors.Wrap(err, "App.handleSuccessfulScheduledPosts: failed to delete successfully posted scheduled posts") } + + a.Srv().telemetryService.SendTelemetryForFeature( + telemetry.TrackScheduledPosts, + "scheduled_posts_success", + map[string]any{"count": len(successfulScheduledPostIDs)}, + ) } return nil @@ -354,4 +362,12 @@ func (a *App) handleFailedScheduledPosts(rctx request.CTX, failedScheduledPosts ) } } + + if len(failedScheduledPosts) > 0 { + a.Srv().telemetryService.SendTelemetryForFeature( + telemetry.TrackScheduledPosts, + "scheduled_posts_failed", + map[string]any{"count": len(failedScheduledPosts)}, + ) + } } diff --git a/server/platform/services/telemetry/telemetry.go b/server/platform/services/telemetry/telemetry.go index 00930e5595..66221a415b 100644 --- a/server/platform/services/telemetry/telemetry.go +++ b/server/platform/services/telemetry/telemetry.go @@ -107,6 +107,7 @@ const ( TrackGroupsFeature TrackFeature = "custom_groups" TrackReadOnlyFeature TrackFeature = "read_only_channels" TrackSharedChannelsFeature TrackFeature = "shared_channels" + TrackScheduledPosts TrackFeature = "scheduled_posts" ) const ( @@ -114,6 +115,7 @@ const ( TrackPropertyGroup = "group_id" TrackPropertyChannel = "channel_id" TrackPropertyPostAuthor = "post_owner_id" + TrackPropertyUserAgent = "user_agent" ) type ServerIface interface { diff --git a/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx b/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx index 63569965b5..5a6b5a9840 100644 --- a/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx +++ b/webapp/channels/src/components/advanced_text_editor/send_button/send_post_options/core_menu_options.tsx @@ -2,11 +2,18 @@ // See LICENSE.txt for license information. import moment from 'moment'; -import React, {memo, useCallback} from 'react'; +import React, {memo, useCallback, useEffect} from 'react'; import {FormattedMessage} from 'react-intl'; import {useSelector} from 'react-redux'; +import { + TrackPropertyUser, TrackPropertyUserAgent, + TrackScheduledPostsFeature, +} from 'mattermost-redux/constants/telemetry'; import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; + +import {trackFeatureEvent} from 'actions/telemetry_actions'; import * as Menu from 'components/menu'; import Timestamp from 'components/timestamp'; @@ -17,6 +24,21 @@ type Props = { function CoreMenuOptions({handleOnSelect}: Props) { const userTimezone = useSelector(getCurrentTimezone); + const currentUserId = useSelector(getCurrentUserId); + + useEffect(() => { + // tracking opening of scheduled posts option menu. + // Since MUI menu has no `onOpen` event, we are tracking it here. + // useEffect ensures that it is tracked only once. + trackFeatureEvent( + TrackScheduledPostsFeature, + 'scheduled_posts_menu_opened', + { + [TrackPropertyUser]: currentUserId, + [TrackPropertyUserAgent]: 'webapp', + }, + ); + }, [currentUserId]); const today = moment().tz(userTimezone); const tomorrow9amTime = moment(). diff --git a/webapp/channels/src/components/drafts/draft_row.tsx b/webapp/channels/src/components/drafts/draft_row.tsx index 43b3353b8f..20afd881e1 100644 --- a/webapp/channels/src/components/drafts/draft_row.tsx +++ b/webapp/channels/src/components/drafts/draft_row.tsx @@ -181,7 +181,8 @@ function DraftRow({ // if scheduled posts was being sent, delete the scheduled post after it's been sent if (isScheduledPostBeingSent.current && response.created && !response.error) { - dispatch(deleteScheduledPost((item as ScheduledPost).id, connectionId)); + const scheduledPost = item as ScheduledPost; + dispatch(deleteScheduledPost(scheduledPost.user_id, scheduledPost.id, connectionId)); isScheduledPostBeingSent.current = false; } }, [connectionId, dispatch, handleOnDelete, item]); @@ -262,8 +263,8 @@ function DraftRow({ const handleSchedulePostOnDelete = useCallback(async () => { handleCancelEdit(); - const scheduledPostId = (item as ScheduledPost).id; - const result = await dispatch(deleteScheduledPost(scheduledPostId, connectionId)); + const scheduledPost = item as ScheduledPost; + const result = await dispatch(deleteScheduledPost(scheduledPost.user_id, scheduledPost.id, connectionId)); return { error: result.error?.message, }; diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts index 14a7bdf464..20ec4f0184 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/scheduled_posts.ts @@ -74,10 +74,10 @@ export function updateScheduledPost(scheduledPost: ScheduledPost, connectionId: }; } -export function deleteScheduledPost(scheduledPostId: string, connectionId: string) { +export function deleteScheduledPost(userId: string, scheduledPostId: string, connectionId: string) { return async (dispatch: DispatchFunc) => { try { - const deletedScheduledPost = await Client4.deleteScheduledPost(scheduledPostId, connectionId); + const deletedScheduledPost = await Client4.deleteScheduledPost(userId, scheduledPostId, connectionId); dispatch({ type: ScheduledPostTypes.SCHEDULED_POST_DELETED, diff --git a/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts b/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts index 5e38cba9e2..2295fad0a1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts @@ -7,7 +7,7 @@ import * as rudderAnalytics from 'rudder-sdk-js'; import type {TelemetryHandler} from '@mattermost/client'; -import {TrackMiscCategory, eventCategory, eventSKUs} from 'mattermost-redux/constants/telemetry'; +import {TrackMiscCategory, eventCategory, eventSKUs, TrackPropertyUser} from 'mattermost-redux/constants/telemetry'; import {isSystemAdmin} from 'mattermost-redux/utils/user_utils'; export {rudderAnalytics}; @@ -18,7 +18,7 @@ export class RudderTelemetryHandler implements TelemetryHandler { category, type: event, user_actual_role: getActualRoles(userRoles), - user_actual_id: userId, + [TrackPropertyUser]: userId, }, props); const options = { context: { @@ -41,7 +41,7 @@ export class RudderTelemetryHandler implements TelemetryHandler { const properties = Object.assign({ category: getEventCategory(event), type: event, - user_actual_id: userId, + [TrackPropertyUser]: userId, user_actual_role: getActualRoles(userRoles), }, props); @@ -68,7 +68,7 @@ export class RudderTelemetryHandler implements TelemetryHandler { title: '', url: '', user_actual_role: getActualRoles(userRoles), - user_actual_id: userId, + [TrackPropertyUser]: userId, }, { context: { diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts index 82907607c2..9bf33409ae 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts @@ -8,6 +8,7 @@ export const TrackEnterpriseSKU = 'enterprise'; // Features export const TrackGroupsFeature = 'custom_groups'; export const TrackPassiveKeywordsFeature = 'passive_keywords'; +export const TrackScheduledPostsFeature = 'scheduled_posts'; // Events export const TrackInviteGroupEvent = 'invite_group_to_channel__add_member'; @@ -17,6 +18,10 @@ export const TrackPassiveKeywordsEvent = 'update_passive_keywords'; export const TrackActionCategory = 'action'; export const TrackMiscCategory = 'miscellaneous'; +// Properties +export const TrackPropertyUser = 'user_actual_id'; +export const TrackPropertyUserAgent = 'user_agent'; + export const eventSKUs: {[event: string]: string[]} = { [TrackInviteGroupEvent]: [TrackProfessionalSKU, TrackEnterpriseSKU], [TrackPassiveKeywordsEvent]: [TrackProfessionalSKU, TrackEnterpriseSKU], diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index b42da6f8a4..f11b9c0aec 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -4,6 +4,10 @@ /* eslint-disable max-lines */ import FormData from 'form-data'; +import { + TrackPropertyUser, + TrackPropertyUserAgent, TrackScheduledPostsFeature, +} from 'mattermost-webapp/src/packages/mattermost-redux/src/constants/telemetry'; import type {ClusterInfo, AnalyticsRow, SchemaMigration, LogFilterQuery} from '@mattermost/types/admin'; import type {AppBinding, AppCallRequest, AppCallResponse} from '@mattermost/types/apps'; @@ -2174,7 +2178,7 @@ export default class Client4 { `${this.getPostsRoute()}`, {method: 'post', body: JSON.stringify(post)}, ); - const analyticsData = {channel_id: result.channel_id, post_id: result.id, user_actual_id: result.user_id, root_id: result.root_id} as PostAnalytics; + const analyticsData = {channel_id: result.channel_id, post_id: result.id, [TrackPropertyUser]: result.user_id, root_id: result.root_id} as PostAnalytics; if (post.metadata?.priority) { analyticsData.priority = post.metadata.priority.priority; analyticsData.requested_ack = post.metadata.priority.requested_ack; @@ -3885,7 +3889,7 @@ export default class Client4 { context: { ...call.context, track_as_submit: trackAsSubmit, - user_agent: 'webapp', + [TrackPropertyUserAgent]: 'webapp', }, }; return this.doFetch( @@ -3898,7 +3902,7 @@ export default class Client4 { const params = { channel_id: channelID, team_id: teamID, - user_agent: 'webapp', + [TrackPropertyUserAgent]: 'webapp', }; return this.doFetch( @@ -4423,6 +4427,8 @@ export default class Client4 { // Schedule Post methods createScheduledPost = (schedulePost: ScheduledPost, connectionId: string) => { + this.trackFeatureEvent(TrackScheduledPostsFeature, 'create_scheduled_post', {[TrackPropertyUser]: schedulePost.user_id, [TrackPropertyUserAgent]: 'desktop'}); + return this.doFetchWithResponse( `${this.getPostsRoute()}/schedule`, {method: 'post', body: JSON.stringify(schedulePost), headers: {'Connection-Id': connectionId}}, @@ -4438,13 +4444,17 @@ export default class Client4 { }; updateScheduledPost = (schedulePost: ScheduledPost, connectionId: string) => { + this.trackFeatureEvent(TrackScheduledPostsFeature, 'update_scheduled_post', {[TrackPropertyUser]: schedulePost.user_id, [TrackPropertyUserAgent]: 'desktop'}); + return this.doFetchWithResponse( `${this.getPostsRoute()}/schedule/${schedulePost.id}`, {method: 'put', body: JSON.stringify(schedulePost), headers: {'Connection-Id': connectionId}}, ); }; - deleteScheduledPost = (schedulePostId: string, connectionId: string) => { + deleteScheduledPost = (userId: string, schedulePostId: string, connectionId: string) => { + this.trackFeatureEvent(TrackScheduledPostsFeature, 'delete_scheduled_post', {[TrackPropertyUser]: userId, [TrackPropertyUserAgent]: 'desktop'}); + return this.doFetchWithResponse( `${this.getPostsRoute()}/schedule/${schedulePostId}`, {method: 'delete', headers: {'Connection-Id': connectionId}}, diff --git a/webapp/platform/types/src/posts.ts b/webapp/platform/types/src/posts.ts index 6dc71d5135..fd791d1950 100644 --- a/webapp/platform/types/src/posts.ts +++ b/webapp/platform/types/src/posts.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import type {TrackPropertyUser} from 'mattermost-webapp/src/packages/mattermost-redux/src/constants/telemetry'; + import type {Channel, ChannelType} from './channels'; import type {CustomEmoji} from './emojis'; import type {FileInfo} from './files'; @@ -199,7 +201,7 @@ export declare type TeamsUsageResponse = { export type PostAnalytics = { channel_id: string; post_id: string; - user_actual_id: string; + [TrackPropertyUser]: string; root_id: string; priority?: PostPriority|''; requested_ack?: boolean;