1
0
mirror of https://github.com/mattermost/focalboard.git synced 2024-11-24 08:22:29 +02:00

Plugin telemetry (#1069)

* implement webapp telemetry

* cleanup

* remove imports, update events

* change event title

* update for lint

* add test, update filename

* linter fix

* fix field name

* revert changes

* fix test

* update builds

* fix workflows

* fix workflows

* fix workflow

* temp checkin

* remove log lines

* updates from peer review
This commit is contained in:
Scott Bishel 2021-09-01 15:53:27 -06:00 committed by GitHub
parent 59b8ae4517
commit 94e6e8a9f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 280 additions and 21 deletions

View File

@ -12,12 +12,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional

View File

@ -11,12 +11,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional

View File

@ -11,12 +11,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.0.2

View File

@ -16,12 +16,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional
@ -69,12 +75,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional
@ -105,12 +117,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.0.2
@ -154,12 +172,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_DEV_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional

View File

@ -11,12 +11,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional

View File

@ -11,12 +11,18 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Replace token 1
- name: Replace token 1 server
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2
- name: Replace token 1 webapp
run: sed -i -e "s,placeholder_rudder_dataplane_url,${{ secrets.RUDDER_DATAPLANE_URL }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: Replace token 2 server
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/server/services/telemetry/telemetry.go
- name: Replace token 2 webapp
run: sed -i -e "s,placeholder_rudder_key,${{ secrets.RUDDER_PROD_KEY }},g" ${{ github.workspace }}/mattermost-plugin/webapp/src/index.tsx
- name: npm install
run: cd webapp; npm install --no-optional

View File

@ -88,6 +88,13 @@ func (p *Plugin) OnActivate() error {
baseURL = *mmconfig.ServiceSettings.SiteURL
}
serverID := client.System.GetDiagnosticID()
enableTelemetry := false
if mmconfig.LogSettings.EnableDiagnostics != nil {
enableTelemetry = *mmconfig.LogSettings.EnableDiagnostics
}
cfg := &config.Configuration{
ServerRoot: baseURL + "/plugins/focalboard",
Port: -1,
@ -100,7 +107,8 @@ func (p *Plugin) OnActivate() error {
FilesDriver: *mmconfig.FileSettings.DriverName,
FilesPath: *mmconfig.FileSettings.Directory,
FilesS3Config: filesS3Config,
Telemetry: true,
Telemetry: enableTelemetry,
TelemetryID: serverID,
WebhookUpdate: []string{},
SessionExpireTime: 2592000,
SessionRefreshTime: 18000,
@ -122,7 +130,6 @@ func (p *Plugin) OnActivate() error {
db = layeredStore
}
serverID := client.System.GetDiagnosticID()
p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db))
server, err := server.New(cfg, "", db, logger, serverID, p.wsPluginAdapter)

View File

@ -5,6 +5,8 @@ import {Store, Action} from 'redux'
import {Provider as ReduxProvider} from 'react-redux'
import {useHistory} from 'mm-react-router-dom'
import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/rudder'
import {GlobalState} from 'mattermost-redux/types/store'
import {getTheme} from 'mattermost-redux/selectors/entities/preferences'
@ -20,9 +22,12 @@ import FocalboardIcon from '../../../webapp/src/widgets/icons/logo'
import {setMattermostTheme} from '../../../webapp/src/theme'
import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK} from './../../../webapp/src/wsclient'
import TelemetryClient from '../../../webapp/src/telemetry/telemetryClient'
import '../../../webapp/src/styles/focalboard-variables.scss'
import '../../../webapp/src/styles/main.scss'
import '../../../webapp/src/styles/labels.scss'
import octoClient from '../../../webapp/src/octoClient'
import manifest from './manifest'
import ErrorBoundary from './error_boundary'
@ -32,6 +37,22 @@ import {PluginRegistry} from './types/mattermost-webapp'
import './plugin.scss'
const TELEMETRY_RUDDER_KEY = 'placeholder_rudder_key'
const TELEMETRY_RUDDER_DATAPLANE_URL = 'placeholder_rudder_dataplane_url'
const TELEMETRY_OPTIONS = {
context: {
ip: '0.0.0.0',
},
page: {
path: '',
referrer: '',
search: '',
title: '',
url: '',
},
anonymousId: '00000000000000000000000000',
}
type Props = {
webSocketClient: MMWebSocketClient
}
@ -150,6 +171,32 @@ export default class Plugin {
this.registry.registerCustomRoute('/', MainApp)
}
const config = await octoClient.getClientConfig()
if (config?.telemetry) {
let rudderKey = TELEMETRY_RUDDER_KEY
let rudderUrl = TELEMETRY_RUDDER_DATAPLANE_URL
if (rudderKey.startsWith('placeholder') && rudderUrl.startsWith('placeholder')) {
rudderKey = process.env.RUDDER_KEY as string //eslint-disable-line no-process-env
rudderUrl = process.env.RUDDER_DATAPLANE_URL as string //eslint-disable-line no-process-env
}
if (rudderKey !== '') {
rudderAnalytics.load(rudderKey, rudderUrl)
rudderAnalytics.identify(config?.telemetryid, {}, TELEMETRY_OPTIONS)
rudderAnalytics.page('BoardsLoaded', '',
TELEMETRY_OPTIONS.page,
{
context: TELEMETRY_OPTIONS.context,
anonymousId: TELEMETRY_OPTIONS.anonymousId,
})
TelemetryClient.setTelemetryHandler(new RudderTelemetryHandler())
}
}
// register websocket handlers
this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data))
}

View File

@ -143,3 +143,11 @@ module.exports = {
mode,
plugins,
};
const env = {};
env.RUDDER_KEY = JSON.stringify(process.env.RUDDER_KEY || ''); //eslint-disable-line no-process-env
env.RUDDER_DATAPLANE_URL = JSON.stringify(process.env.RUDDER_DATAPLANE_URL || ''); //eslint-disable-line no-process-env
module.exports.plugins.push(new webpack.DefinePlugin({
'process.env': env,
}));

View File

@ -90,6 +90,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
apiv1.HandleFunc("/login", a.handleLogin).Methods("POST")
apiv1.HandleFunc("/register", a.handleRegister).Methods("POST")
apiv1.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET")
apiv1.HandleFunc("/workspaces/{workspaceID}/{rootID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
@ -115,6 +116,17 @@ func (a *API) requireCSRFToken(next http.Handler) http.Handler {
})
}
func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) {
clientConfig := a.app.GetClientConfig()
configData, err := json.Marshal(clientConfig)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
jsonBytesResponse(w, http.StatusOK, configData)
}
func (a *API) checkCSRFToken(r *http.Request) bool {
token := r.Header.Get(HeaderRequestedWith)
return token == HeaderRequestedWithXML

View File

@ -0,0 +1,12 @@
package app
import (
"github.com/mattermost/focalboard/server/model"
)
func (a *App) GetClientConfig() *model.ClientConfig {
return &model.ClientConfig{
Telemetry: a.config.Telemetry,
TelemetryID: a.config.TelemetryID,
}
}

View File

@ -0,0 +1,6 @@
package model
type ClientConfig struct {
Telemetry bool `json:"telemetry"`
TelemetryID string `json:"telemetryid"`
}

View File

@ -38,6 +38,7 @@ type Configuration struct {
FilesS3Config AmazonS3Config `json:"filess3config" mapstructure:"filess3config"`
FilesPath string `json:"filespath" mapstructure:"filespath"`
Telemetry bool `json:"telemetry" mapstructure:"telemetry"`
TelemetryID string `json:"telemetryid" mapstructure:"telemetryid"`
PrometheusAddress string `json:"prometheus_address" mapstructure:"prometheus_address"`
WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"`
Secret string `json:"secret" mapstructure:"secret"`
@ -73,6 +74,7 @@ func ReadConfigFile() (*Configuration, error) {
viper.SetDefault("FilesPath", "./files")
viper.SetDefault("FilesDriver", "local")
viper.SetDefault("Telemetry", true)
viper.SetDefault("TelemetryID", "")
viper.SetDefault("WebhookUpdate", nil)
viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime
viper.SetDefault("SessionRefreshTime", 60*60*5) // 5 minutes session refresh

View File

@ -12,6 +12,8 @@ import {DndProvider} from 'react-dnd'
import {HTML5Backend} from 'react-dnd-html5-backend'
import {TouchBackend} from 'react-dnd-touch-backend'
import TelemetryClient from './telemetry/telemetryClient'
import {getMessages} from './i18n'
import {FlashMessages} from './components/flashMessages'
import BoardPage from './pages/boardPage'
@ -22,15 +24,18 @@ import LoginPage from './pages/loginPage'
import RegisterPage from './pages/registerPage'
import {Utils} from './utils'
import wsClient from './wsclient'
import {fetchMe, getLoggedIn} from './store/users'
import {fetchMe, getLoggedIn, getMe} from './store/users'
import {getLanguage, fetchLanguage} from './store/language'
import {setGlobalError, getGlobalError} from './store/globalError'
import {useAppSelector, useAppDispatch} from './store/hooks'
import {IUser} from './user'
const App = React.memo((): JSX.Element => {
const language = useAppSelector<string>(getLanguage)
const loggedIn = useAppSelector<boolean|null>(getLoggedIn)
const globalError = useAppSelector<string>(getGlobalError)
const me = useAppSelector<IUser|null>(getMe)
const dispatch = useAppDispatch()
useEffect(() => {
@ -45,6 +50,12 @@ const App = React.memo((): JSX.Element => {
}
}, [])
useEffect(() => {
if (me) {
TelemetryClient.setUser(me)
}
}, [me])
let globalErrorRedirect = null
if (globalError) {
globalErrorRedirect = <Route path='/*'><Redirect to={`/error?id=${globalError}`}/></Route>

View File

@ -19,6 +19,8 @@ import {updateView} from '../store/views'
import './centerPanel.scss'
import TelemetryClient from '../../../webapp/src/telemetry/telemetryClient'
import CardDialog from './cardDialog'
import RootPortal from './rootPortal'
import TopBar from './topBar'
@ -80,6 +82,10 @@ class CenterPanel extends React.Component<Props, State> {
}
}
componentDidMount(): void {
TelemetryClient.trackEvent('boards', 'view', {viewType: this.props.activeView.fields.viewType})
}
constructor(props: Props) {
super(props)
this.state = {
@ -92,6 +98,10 @@ class CenterPanel extends React.Component<Props, State> {
return true
}
componentDidUpdate(): void {
TelemetryClient.trackEvent('boards', 'view', {viewType: this.props.activeView.fields.viewType})
}
render(): JSX.Element {
const {groupByProperty, activeView, board, views, cards} = this.props
const {visible: visibleGroups, hidden: hiddenGroups} = this.getVisibleAndHiddenGroups(cards, activeView.fields.visibleOptionIds, activeView.fields.hiddenOptionIds, groupByProperty)

View File

@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type ClientConfig = {
telemetry: boolean,
telemetryid: string,
}

View File

@ -6,6 +6,7 @@ import {IWorkspace} from './blocks/workspace'
import {OctoUtils} from './octoUtils'
import {IUser} from './user'
import {Utils} from './utils'
import {ClientConfig} from './config/clientConfig'
//
// OctoClient is the client interface to the server APIs
@ -64,6 +65,20 @@ class OctoClient {
localStorage.removeItem('focalboardSessionId')
}
async getClientConfig(): Promise<ClientConfig | null> {
const path = '/api/v1/clientConfig'
const response = await fetch(this.serverUrl + path, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return null
}
const json = (await this.getJson(response, {})) as ClientConfig
return json
}
async register(email: string, username: string, password: string, token?: string): Promise<{code: number, json: any}> {
const path = '/api/v1/register'
const body = JSON.stringify({email, username, password, token})

View File

@ -0,0 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export interface TelemetryHandler {
trackEvent: (userId: string, userRoles: string, category: string, event: string, props?: any) => void;
pageVisited: (userId: string, userRoles: string, category: string, name: string) => void;
}

View File

@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import '@testing-library/jest-dom'
import TelemetryClient from './telemetryClient'
describe('trackEvent', () => {
const track = jest.fn()
const page = jest.fn()
test('should call Rudder\'s track when a RudderTelemetryHandler is attached to TelemetryClient', () => {
TelemetryClient.setTelemetryHandler()
TelemetryClient.trackEvent('test', 'onClick')
TelemetryClient.pageVisited('focalboard', 'test')
expect(track).not.toHaveBeenCalled()
expect(page).not.toHaveBeenCalled()
TelemetryClient.setTelemetryHandler({trackEvent: track, pageVisited: page})
TelemetryClient.trackEvent('test', 'onClick')
TelemetryClient.pageVisited('focalboard', 'test')
expect(track).toHaveBeenCalledTimes(1)
expect(page).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IUser} from '../user'
import {TelemetryHandler} from './telemetry'
class TelemetryClient {
public telemetryHandler?: TelemetryHandler
public user?: IUser
setTelemetryHandler(telemetryHandler?: TelemetryHandler): void {
this.telemetryHandler = telemetryHandler
}
setUser(user: IUser): void {
this.user = user
}
trackEvent(category: string, event: string, props?: any): void {
if (this.telemetryHandler) {
const userId = this.user?.id
this.telemetryHandler.trackEvent(userId || '', '', category, event, props)
}
}
pageVisited(category: string, name: string): void {
if (this.telemetryHandler) {
const userId = this.user?.id
this.telemetryHandler.pageVisited(userId || '', '', category, name)
}
}
}
const telemetryClient = new TelemetryClient()
export {TelemetryClient}
export default telemetryClient