From 4cd73fc2d5f0de48073bcce2860bcf4f7380b72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Vay=C3=A1?= Date: Mon, 21 Oct 2024 15:04:27 +0200 Subject: [PATCH] [MM-61193] Create trackFeatureEvent telemetry handler for paid features in web (#28834) * frontend paid feature usage tracking * fixes on the tooling * fix lint errors * fix types * fix ci * move from paid to SKU * fix linter * move categories and skus to depend on event instead of feature * move to constants --- .../src/actions/telemetry_actions.jsx | 4 ++ .../group_option/group_option.tsx | 4 ++ .../mattermost-redux/src/client/rudder.ts | 47 +++++++++++++++++++ .../mattermost-redux/src/constants/index.ts | 3 +- .../src/constants/telemetry.ts | 18 +++++++ webapp/platform/client/src/client4.test.ts | 1 + webapp/platform/client/src/client4.ts | 5 ++ webapp/platform/client/src/telemetry.ts | 1 + 8 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts diff --git a/webapp/channels/src/actions/telemetry_actions.jsx b/webapp/channels/src/actions/telemetry_actions.jsx index eca38753ab..ffe9a1ec65 100644 --- a/webapp/channels/src/actions/telemetry_actions.jsx +++ b/webapp/channels/src/actions/telemetry_actions.jsx @@ -38,6 +38,10 @@ export function trackEvent(category, event, props) { } } +export function trackFeatureEvent(featureName, event, props) { + Client4.trackFeatureEvent(featureName, event, props); +} + export function pageVisited(category, name) { Client4.pageVisited(category, name); } diff --git a/webapp/channels/src/components/channel_invite_modal/group_option/group_option.tsx b/webapp/channels/src/components/channel_invite_modal/group_option/group_option.tsx index ec382a69d5..c0f26be8aa 100644 --- a/webapp/channels/src/components/channel_invite_modal/group_option/group_option.tsx +++ b/webapp/channels/src/components/channel_invite_modal/group_option/group_option.tsx @@ -10,8 +10,11 @@ import type {Group} from '@mattermost/types/groups'; import type {GlobalState} from '@mattermost/types/store'; import type {UserProfile} from '@mattermost/types/users'; +import {TrackGroupsFeature, TrackInviteGroupEvent} from 'mattermost-redux/constants/telemetry'; import {getUser, makeDisplayNameGetter, makeGetProfilesByIdsAndUsernames} from 'mattermost-redux/selectors/entities/users'; +import {trackFeatureEvent} from 'actions/telemetry_actions'; + import type {Value} from 'components/multiselect/multiselect'; import WithTooltip from 'components/with_tooltip'; @@ -55,6 +58,7 @@ const GroupOption = (props: Props) => { for (const profile of profiles) { addUserProfile(profile); } + trackFeatureEvent(TrackGroupsFeature, TrackInviteGroupEvent, {}); }, [addUserProfile, profiles]); const onKeyDown = useCallback((e: KeyboardEvent) => { 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 5af1f1bafe..2517bed3d2 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/client/rudder.ts @@ -7,10 +7,19 @@ import * as rudderAnalytics from 'rudder-sdk-js'; import type {TelemetryHandler} from '@mattermost/client'; +import {TrackMiscCategory, TrackActionCategory, TrackEnterpriseSKU, TrackProfessionalSKU, TrackInviteGroupEvent} from 'mattermost-redux/constants/telemetry'; import {isSystemAdmin} from 'mattermost-redux/utils/user_utils'; export {rudderAnalytics}; +const eventSKUs: {[event: string]: string[]} = { + [TrackInviteGroupEvent]: [TrackProfessionalSKU, TrackEnterpriseSKU], +}; + +const eventCategory: {[event: string]: string} = { + [TrackInviteGroupEvent]: TrackActionCategory, +}; + export class RudderTelemetryHandler implements TelemetryHandler { trackEvent(userId: string, userRoles: string, category: string, event: string, props?: any) { const properties = Object.assign({ @@ -36,6 +45,25 @@ export class RudderTelemetryHandler implements TelemetryHandler { rudderAnalytics.track('event', properties, options); } + trackFeatureEvent(userId: string, userRoles: string, featureName: string, event: string, props?: any) { + const properties = Object.assign({ + category: getEventCategory(event), + type: event, + user_actual_id: userId, + user_actual_role: getActualRoles(userRoles), + }, props); + const options = { + context: { + feature: { + name: featureName, + skus: getSKUs(event), + }, + }, + }; + + rudderAnalytics.track(event, properties, options); + } + pageVisited(userId: string, userRoles: string, category: string, name: string) { rudderAnalytics.page( category, @@ -62,3 +90,22 @@ export class RudderTelemetryHandler implements TelemetryHandler { function getActualRoles(userRoles: string) { return userRoles && isSystemAdmin(userRoles) ? 'system_admin, system_user' : 'system_user'; } + +function getSKUs(eventName: string) { + const skus: string[] | undefined = eventSKUs[eventName]; + if (skus === undefined) { + // Next line is to be aware if you've forgotten to add a SKU, add an empty array for Team edition + // eslint-disable-next-line + console.warn(`Event ${eventName} has no SKUs attached`); + } + return skus ?? []; +} + +function getEventCategory(eventName: string) { + const category: string | undefined = eventCategory[eventName]; + if (category === undefined) { + // eslint-disable-next-line + console.warn(`Event ${eventName} doesn't have a category`); + } + return category ?? TrackMiscCategory; +} diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts index 403abefb11..7b34210f5c 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/index.ts @@ -12,8 +12,9 @@ import RequestStatus from './request_status'; import Roles from './roles'; import Stats from './stats'; import Teams from './teams'; +import Telemetry from './telemetry'; import Threads from './threads'; import Users from './users'; import WebsocketEvents from './websocket'; -export {General, Preferences, Posts, Files, RequestStatus, WebsocketEvents, Teams, Stats, Permissions, Emoji, Plugins, Users, Roles, Threads}; +export {General, Preferences, Posts, Files, RequestStatus, WebsocketEvents, Teams, Stats, Permissions, Emoji, Plugins, Users, Roles, Threads, Telemetry}; diff --git a/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts b/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts new file mode 100644 index 0000000000..5e6958fa1d --- /dev/null +++ b/webapp/channels/src/packages/mattermost-redux/src/constants/telemetry.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// SKUs +export const TrackProfessionalSKU = 'professional'; +export const TrackEnterpriseSKU = 'enterprise'; + +// Features +export const TrackGroupsFeature: string = 'custom_groups'; + +// Events +export const TrackInviteGroupEvent: string = 'invite_group_to_channel'; + +// Categories +export const TrackActionCategory: string = 'action'; +export const TrackMiscCategory: string = 'miscellaneous'; + +export default {TrackActionCategory, TrackMiscCategory, TrackInviteGroupEvent, TrackGroupsFeature, TrackEnterpriseSKU, TrackProfessionalSKU}; diff --git a/webapp/platform/client/src/client4.test.ts b/webapp/platform/client/src/client4.test.ts index 60d53ec940..46923d78d9 100644 --- a/webapp/platform/client/src/client4.test.ts +++ b/webapp/platform/client/src/client4.test.ts @@ -82,6 +82,7 @@ describe('ClientError', () => { describe('trackEvent', () => { class TestTelemetryHandler implements TelemetryHandler { trackEvent = jest.fn(); + trackFeatureEvent = jest.fn(); pageVisited = jest.fn(); } diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 773c658918..6eb8dae22a 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -4327,6 +4327,11 @@ export default class Client4 { this.telemetryHandler.trackEvent(this.userId, this.userRoles, category, event, props); } } + trackFeatureEvent(featureName: string, event: string, props?: any) { + if (this.telemetryHandler) { + this.telemetryHandler.trackFeatureEvent(this.userId, this.userRoles, featureName, event, props); + } + } pageVisited(category: string, name: string) { if (this.telemetryHandler) { diff --git a/webapp/platform/client/src/telemetry.ts b/webapp/platform/client/src/telemetry.ts index 5bbfcec52b..f7fb3e364b 100644 --- a/webapp/platform/client/src/telemetry.ts +++ b/webapp/platform/client/src/telemetry.ts @@ -3,5 +3,6 @@ export interface TelemetryHandler { trackEvent: (userId: string, userRoles: string, category: string, event: string, props?: any) => void; + trackFeatureEvent: (userId: string, userRoles: string, featureName: string, event: string, props?: any) => void; pageVisited: (userId: string, userRoles: string, category: string, name: string) => void; }