MM-48246 - AB test open RHS with linked board (#22201)

* MM-48246 - AB test open RHS with linked board

* fix linter

* fix go vet complain

* implement pr feedback

* fix mispelling

* remove unnecesary comment

* add pr feedback

* fix translation texts

* fallback for the first system stored value

* implement PR feedback

* prevent calling property from potential null object

* use the default hash id for welcome to boards template
Этот коммит содержится в:
Pablo Andrés Vélez Vidal
2023-02-09 13:35:32 +01:00
коммит произвёл GitHub
родитель 825b281f36
Коммит bd59b112a6
4 изменённых файлов: 169 добавлений и 0 удалений

Просмотреть файл

@@ -6,6 +6,7 @@ package app
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
@@ -17,6 +18,7 @@ import (
"sort"
"strings"
fb_model "github.com/mattermost/focalboard/server/model"
"github.com/mattermost/mattermost-server/v6/app/email"
"github.com/mattermost/mattermost-server/v6/app/imaging"
"github.com/mattermost/mattermost-server/v6/app/request"
@@ -160,6 +162,128 @@ func (a *App) SoftDeleteAllTeamsExcept(teamID string) *model.AppError {
return nil
}
// MM-48246 A/B test show linked boards
const preferenceName = "linked_board_created"
func (a *App) shouldCreateOnboardingLinkedBoard(c request.CTX, teamId string) bool {
ffEnabled := a.Config().FeatureFlags.OnboardingAutoShowLinkedBoard
if !ffEnabled {
return false
}
hasBoard, err := a.HasBoardProduct()
if err != nil {
a.Log().Error("error checking the existence of boards product: ", mlog.Err(err))
return false
}
if !hasBoard {
a.Log().Warn("board product not found")
return false
}
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
var nfErr *store.ErrNotFound
if errors.As(sysValErr, &nfErr) { // if no board has been registered, it can create one for this team
return true
}
a.Log().Error("cannot get the system values", mlog.Err(sysValErr))
return false
}
// get the team list and check if the team value has been already stored, if so, no need to create a board in town square in that team
teamsList := strings.Split(data.Value, ",")
for _, team := range teamsList {
if team == teamId {
return false
}
}
return true
}
func (a *App) createOnboardingLinkedBoard(c request.CTX, teamId string) (*fb_model.Board, *model.AppError) {
const defaultTemplatesTeam = "0"
// see https://github.com/mattermost/focalboard/blob/main/server/services/store/sqlstore/board.go#L302
// and https://github.com/mattermost/mattermost-server/pull/22201#discussion_r1099536430
const defaultTemplateTitle = "Welcome to Boards!"
welcomeToBoardsTemplateId := fmt.Sprintf("%x", md5.Sum([]byte(defaultTemplateTitle)))
userId := c.Session().UserId
boardServiceItf, ok := a.Srv().services[product.BoardsKey]
if !ok {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.product_key_not_found", nil, "", http.StatusBadRequest)
}
boardService, typeOk := boardServiceItf.(product.BoardsService)
if !typeOk {
// boardServiceItf is NOT of type product.BoardsService
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.itf_not_of_type", nil, "", http.StatusBadRequest)
}
templates, err := boardService.GetTemplates(defaultTemplatesTeam, userId)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_getting_templates", nil, "", http.StatusBadRequest).Wrap(err)
}
channel, appErr := a.GetChannelByName(c, model.DefaultChannelName, teamId, false)
if appErr != nil {
return nil, appErr
}
var template *fb_model.Board = nil
for _, t := range templates {
v := t.Properties["trackingTemplateId"]
if v == welcomeToBoardsTemplateId {
template = t
break
}
}
if template == nil && len(templates) > 0 {
template = templates[0]
}
// Duplicate board From template
boardsAndBlocks, _, err := boardService.DuplicateBoard(template.ID, userId, teamId, false)
if err != nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_duplicating_board", nil, "", http.StatusBadRequest).Wrap(err)
}
if len(boardsAndBlocks.Boards) != 1 {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_no_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// link the board with the channel
patchedBoard, err := boardService.PatchBoard(&fb_model.BoardPatch{
ChannelID: &channel.Id,
}, boardsAndBlocks.Boards[0].ID, userId)
if err != nil && patchedBoard == nil {
return nil, model.NewAppError("CreateBoard", "app.team.create_onboarding_linked_board.error_patching_board", nil, "", http.StatusBadRequest).Wrap(err)
}
// Save in the system preferences that the board was already created once per team
data, sysValErr := a.Srv().Store().System().GetByName(model.PreferenceOnboarding + "_" + preferenceName)
if sysValErr != nil {
c.Logger().Error("cannot get the system preferences", mlog.Err(sysValErr))
}
teamsList := teamId
// if data is not nil, data.Value contains the list of teams where the A/B test has alredy created a channel for town square
if data != nil {
teamsList = data.Value + "," + teamId
}
if err := a.Srv().Store().System().SaveOrUpdate(&model.System{
Name: model.PreferenceOnboarding + "_" + preferenceName,
Value: teamsList,
}); err != nil {
c.Logger().Warn("encountered error saving user preferences", mlog.Err(err))
}
return patchedBoard, nil
}
func (a *App) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.AppError) {
rteam, err := a.ch.srv.teamService.CreateTeam(team)
if err != nil {
@@ -191,6 +315,20 @@ func (a *App) CreateTeam(c request.CTX, team *model.Team) (*model.Team, *model.A
}
}
// MM-48246 A/B test show linked boards. Create a welcome to boards linked board per user
if a.shouldCreateOnboardingLinkedBoard(c, team.Id) {
board, aErr := a.createOnboardingLinkedBoard(c, team.Id)
if aErr != nil || board == nil {
a.Log().Warn("Error creating the linked board, only team created", mlog.Err(err))
return rteam, nil
}
if board.ID != "" {
logInfo := fmt.Sprintf("Board created with id %s and associated to channel %s in team %s", board.ID, board.ChannelID, team.Id)
a.Log().Info(logInfo, mlog.Err(err))
}
}
return rteam, nil
}

Просмотреть файл

@@ -6495,6 +6495,30 @@
"id": "app.team.clear_cache.app_error",
"translation": "Error clearing team member cache"
},
{
"id": "app.team.create_onboarding_linked_board.error_duplicating_board",
"translation": "error duplicating board"
},
{
"id": "app.team.create_onboarding_linked_board.error_getting_templates",
"translation": "error getting boards templates"
},
{
"id": "app.team.create_onboarding_linked_board.error_no_board",
"translation": "error no board created"
},
{
"id": "app.team.create_onboarding_linked_board.error_patching_board",
"translation": "error while patching board"
},
{
"id": "app.team.create_onboarding_linked_board.itf_not_of_type",
"translation": "interface not of expected type"
},
{
"id": "app.team.create_onboarding_linked_board.product_key_not_found",
"translation": "product key not found"
},
{
"id": "app.team.get.find.app_error",
"translation": "Unable to find the existing team."

Просмотреть файл

@@ -73,6 +73,9 @@ type FeatureFlags struct {
// A/B Test on reduced onboarding task list item
ReduceOnBoardingTaskList bool
// A/B Test to control when to show onboarding linked board
OnboardingAutoShowLinkedBoard bool
ThreadsEverywhere bool
GlobalDrafts bool
@@ -107,6 +110,7 @@ func (f *FeatureFlags) SetDefaults() {
f.ThreadsEverywhere = false
f.GlobalDrafts = true
f.WysiwygEditor = false
f.OnboardingAutoShowLinkedBoard = true
f.OnboardingTourTips = true
}

Просмотреть файл

@@ -33,6 +33,9 @@ const (
PreferenceRecommendedNextSteps = "recommended_next_steps"
PreferenceNameInsights = "insights_tutorial_state"
// initial onboarding preferences
PreferenceOnboarding = "onboarding"
PreferenceCategoryTheme = "theme"
// the name for theme props is the team id