Skip to content

Commit

Permalink
Add telemetry to Todo (#101)
Browse files Browse the repository at this point in the history
* Add telemetry to Todo

* Fix bugs with telemetry

* Fix lint

* Go mod tidy

* Close telemetry client on deactivate

* Fix typo

* Type telemetry source
  • Loading branch information
larkox authored Sep 7, 2020
1 parent b2a69b7 commit bfa5742
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 27 deletions.
4 changes: 4 additions & 0 deletions build/custom.mk
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Include custom targets and environment variables here
ifndef MM_RUDDER_WRITE_KEY
MM_RUDDER_WRITE_KEY = 1d5bMvdrfWClLxgK1FvV3s4U1tg
endif
GO_BUILD_FLAGS += -ldflags '-X "github.com/mattermost/mattermost-plugin-api/experimental/telemetry.rudderWriteKey=$(MM_RUDDER_WRITE_KEY)"'
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module github.com/mattermost/mattermost-plugin-todo
go 1.13

require (
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200723144633-ed34468996e6
github.com/mattermost/mattermost-plugin-api v0.0.11
github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200804063212-d4dac31b042a
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
)
42 changes: 30 additions & 12 deletions go.sum

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*mo
var handler func([]string, *model.CommandArgs) (bool, error)
if lengthOfArgs == 1 {
handler = p.runListCommand
p.trackCommand(args.UserId, "")
} else {
command := stringArgs[1]
p.trackCommand(args.UserId, command)
if lengthOfArgs > 2 {
restOfArgs = stringArgs[2:]
}
Expand Down Expand Up @@ -146,6 +148,8 @@ func (p *Plugin) runSendCommand(args []string, extra *model.CommandArgs) (bool,
return false, err
}

p.trackSendIssue(extra.UserId, sourceCommand, false)

p.sendRefreshEvent(extra.UserId)
p.sendRefreshEvent(receiver.Id)

Expand Down Expand Up @@ -173,6 +177,8 @@ func (p *Plugin) runAddCommand(args []string, extra *model.CommandArgs) (bool, e
return false, err
}

p.trackAddIssue(extra.UserId, sourceCommand, false)

p.sendRefreshEvent(extra.UserId)

responseMessage := "Added Todo."
Expand Down
11 changes: 11 additions & 0 deletions server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"reflect"

"github.com/mattermost/mattermost-plugin-api/experimental/bot/logger"
"github.com/mattermost/mattermost-plugin-api/experimental/telemetry"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -95,5 +97,14 @@ func (p *Plugin) OnConfigurationChange() error {
p.sendConfigUpdateEvent()
}

enableDiagnostics := false
if config := p.API.GetConfig(); config != nil {
if configValue := config.LogSettings.EnableDiagnostics; configValue != nil {
enableDiagnostics = *configValue
}
}
logger := logger.NewLogger(logger.Config{}, p.API, nil, "")
p.tracker = telemetry.NewTracker(p.telemetryClient, p.API.GetDiagnosticId(), p.API.GetServerVersion(), manifest.Id, manifest.Version, "todo", enableDiagnostics, logger)

return nil
}
60 changes: 60 additions & 0 deletions server/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"
"time"

"github.com/mattermost/mattermost-plugin-api/experimental/telemetry"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/plugin"
"github.com/pkg/errors"
Expand Down Expand Up @@ -57,6 +58,9 @@ type Plugin struct {
configuration *configuration

listManager ListManager

telemetryClient telemetry.Client
tracker telemetry.Tracker
}

func (p *Plugin) OnActivate() error {
Expand All @@ -77,9 +81,25 @@ func (p *Plugin) OnActivate() error {

p.listManager = NewListManager(p.API)

p.telemetryClient, err = telemetry.NewRudderClient()
if err != nil {
p.API.LogWarn("telemetry client not started", "error", err.Error())
}

return p.API.RegisterCommand(getCommand())
}

func (p *Plugin) OnDeactivate() error {
if p.telemetryClient != nil {
err := p.telemetryClient.Close()
if err != nil {
p.API.LogWarn("OnDeactivate: failed to close telemetryClient", "error", err.Error())
}
}

return nil
}

// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world.
func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
Expand All @@ -95,13 +115,41 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req
p.handleAccept(w, r)
case "/bump":
p.handleBump(w, r)
case "/telemetry":
p.handleTelemetry(w, r)
case "/config":
p.handleConfig(w, r)
default:
http.NotFound(w, r)
}
}

type telemetryAPIRequest struct {
Event string
Properties map[string]interface{}
}

func (p *Plugin) handleTelemetry(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("Mattermost-User-ID")
if userID == "" {
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}

var telemetryRequest *telemetryAPIRequest
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&telemetryRequest)
if err != nil {
p.API.LogError("Unable to decode JSON err=" + err.Error())
p.handleErrorWithCode(w, http.StatusBadRequest, "Unable to decode JSON", err)
return
}

if telemetryRequest.Event != "" {
p.trackFrontend(userID, telemetryRequest.Event, telemetryRequest.Properties)
}
}

type addAPIRequest struct {
Message string `json:"message"`
SendTo string `json:"send_to"`
Expand Down Expand Up @@ -133,6 +181,7 @@ func (p *Plugin) handleAdd(w http.ResponseWriter, r *http.Request) {
p.handleErrorWithCode(w, http.StatusInternalServerError, "Unable to add issue", err)
return
}
p.trackAddIssue(userID, sourceWebapp, addRequest.PostID != "")
replyMessage := fmt.Sprintf("@%s attached a todo to this thread", senderName)
p.postReplyIfNeeded(addRequest.PostID, replyMessage, addRequest.Message)
return
Expand All @@ -152,6 +201,7 @@ func (p *Plugin) handleAdd(w http.ResponseWriter, r *http.Request) {
p.handleErrorWithCode(w, http.StatusInternalServerError, "Unable to add issue", err)
return
}
p.trackAddIssue(userID, sourceWebapp, addRequest.PostID != "")
replyMessage := fmt.Sprintf("@%s attached a todo to this thread", senderName)
p.postReplyIfNeeded(addRequest.PostID, replyMessage, addRequest.Message)
return
Expand All @@ -165,6 +215,8 @@ func (p *Plugin) handleAdd(w http.ResponseWriter, r *http.Request) {
return
}

p.trackSendIssue(userID, sourceWebapp, addRequest.PostID != "")

receiverMessage := fmt.Sprintf("You have received a new Todo from @%s", senderName)
p.sendRefreshEvent(receiver.Id)
p.PostBotCustomDM(receiver.Id, receiverMessage, addRequest.Message, issueID)
Expand Down Expand Up @@ -224,6 +276,7 @@ func (p *Plugin) handleList(w http.ResponseWriter, r *http.Request) {
lt := time.Unix(lastReminderAt/1000, 0).In(timezone)
if nt.Sub(lt).Hours() >= 1 && (nt.Day() != lt.Day() || nt.Month() != lt.Month() || nt.Year() != lt.Year()) {
p.PostBotDM(userID, "Daily Reminder:\n\n"+issuesListToString(issues))
p.trackDailySummary(userID)
err = p.saveLastReminderTimeForUser(userID)
if err != nil {
p.API.LogError("Unable to save last reminder for user err=" + err.Error())
Expand Down Expand Up @@ -271,6 +324,8 @@ func (p *Plugin) handleAccept(w http.ResponseWriter, r *http.Request) {
return
}

p.trackAcceptIssue(userID)

userName := p.listManager.GetUserName(userID)

message := fmt.Sprintf("@%s accepted a Todo you sent: %s", userName, todoMessage)
Expand Down Expand Up @@ -303,6 +358,7 @@ func (p *Plugin) handleComplete(w http.ResponseWriter, r *http.Request) {
p.handleErrorWithCode(w, http.StatusInternalServerError, "Unable to complete issue", err)
return
}
p.trackCompleteIssue(userID)

userName := p.listManager.GetUserName(userID)
replyMessage := fmt.Sprintf("@%s completed a todo attached to this thread", userName)
Expand Down Expand Up @@ -344,6 +400,8 @@ func (p *Plugin) handleRemove(w http.ResponseWriter, r *http.Request) {
return
}

p.trackRemoveIssue(userID)

userName := p.listManager.GetUserName(userID)
replyMessage := fmt.Sprintf("@%s removed a todo attached to this thread", userName)
p.postReplyIfNeeded(issue.PostID, replyMessage, issue.Message)
Expand Down Expand Up @@ -388,6 +446,8 @@ func (p *Plugin) handleBump(w http.ResponseWriter, r *http.Request) {
return
}

p.trackBumpIssue(userID)

if foreignUser == "" {
return
}
Expand Down
55 changes: 55 additions & 0 deletions server/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

type telemetrySource string

const (
sourceCommand telemetrySource = "command"
sourceWebapp telemetrySource = "webapp"
)

func (p *Plugin) trackCommand(userID, command string) {
p.tracker.TrackUserEvent("command", userID, map[string]interface{}{
"command": command,
})
}

func (p *Plugin) trackAddIssue(userID string, source telemetrySource, attached bool) {
p.tracker.TrackUserEvent("add_issue", userID, map[string]interface{}{
"source": source,
"attached": attached,
})
}

func (p *Plugin) trackSendIssue(userID string, source telemetrySource, attached bool) {
p.tracker.TrackUserEvent("send_issue", userID, map[string]interface{}{
"source": source,
"attached": attached,
})
}

func (p *Plugin) trackCompleteIssue(userID string) {
p.tracker.TrackUserEvent("complete_issue", userID, map[string]interface{}{})
}

func (p *Plugin) trackRemoveIssue(userID string) {
p.tracker.TrackUserEvent("remove_issue", userID, map[string]interface{}{})
}

func (p *Plugin) trackAcceptIssue(userID string) {
p.tracker.TrackUserEvent("accept_issue", userID, map[string]interface{}{})
}

func (p *Plugin) trackBumpIssue(userID string) {
p.tracker.TrackUserEvent("bump_issue", userID, map[string]interface{}{})
}

func (p *Plugin) trackFrontend(userID, event string, properties map[string]interface{}) {
if properties == nil {
properties = map[string]interface{}{}
}
p.tracker.TrackUserEvent("frontend_"+event, userID, properties)
}

func (p *Plugin) trackDailySummary(userID string) {
p.tracker.TrackUserEvent("daily_summary_sent", userID, map[string]interface{}{})
}
7 changes: 7 additions & 0 deletions webapp/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export const getPluginServerRoute = (state) => {
return basePath + '/plugins/' + pluginId;
};

export const telemetry = (event, properties) => async (dispatch, getState) => {
await fetch(getPluginServerRoute(getState()) + '/telemetry', Client4.getOptions({
method: 'post',
body: JSON.stringify({event, properties}),
}));
};

export const add = (message, sendTo, postID) => async (dispatch, getState) => {
await fetch(getPluginServerRoute(getState()) + '/add', Client4.getOptions({
method: 'post',
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/components/post_type_todo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {remove, complete, accept} from '../../actions';
import {remove, complete, accept, telemetry} from '../../actions';
import {getSiteURL} from '../../selectors';

import PostTypeTodo from './post_type_todo';
Expand All @@ -23,6 +23,7 @@ function mapDispatchToProps(dispatch) {
remove,
complete,
accept,
telemetry,
}, dispatch),
};
}
Expand Down
17 changes: 13 additions & 4 deletions webapp/src/components/post_type_todo/post_type_todo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class PostTypeTodo extends React.PureComponent {
complete: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
accept: PropTypes.func.isRequired,
telemetry: PropTypes.func.isRequired,
}).isRequired,
};

Expand Down Expand Up @@ -48,16 +49,24 @@ export default class PostTypeTodo extends React.PureComponent {
>
<RemoveButton
issueId={this.props.post.props.issueId}
remove={this.props.actions.remove}
list={'my'}
remove={(issueID) => {
this.props.actions.telemetry('custom_post_remove');
this.props.actions.remove(issueID);
}}
/>
<AcceptButton
issueId={this.props.post.props.issueId}
accept={this.props.actions.accept}
accept={(issueID) => {
this.props.actions.telemetry('custom_post_accept');
this.props.actions.accept(issueID);
}}
/>
<CompleteButton
issueId={this.props.post.props.issueId}
complete={this.props.actions.complete}
complete={(issueID) => {
this.props.actions.telemetry('custom_post_complete');
this.props.actions.complete(issueID);
}}
/>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/components/sidebar_buttons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {list, updateRhsState} from '../../actions';
import {list, updateRhsState, telemetry} from '../../actions';

import SidebarButtons from './sidebar_buttons.jsx';

Expand All @@ -22,6 +22,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
list,
updateRhsState,
telemetry,
}, dispatch),
};
}
Expand Down
Loading

0 comments on commit bfa5742

Please sign in to comment.