refactor Sentry plugin commands and enhance autocomplete functionality. Introduced a new method for fetching project details and improved error handling. Updated command responses for linking and unlinking projects, and added support for additional fields in webhook setup.
This commit is contained in:
parent
e04079cb0b
commit
ced59c39d2
@ -29,8 +29,7 @@ func (p *Plugin) registerCommands() error {
|
|||||||
DisplayName: "Sentry",
|
DisplayName: "Sentry",
|
||||||
Description: "Manage Sentry alerts and integrations",
|
Description: "Manage Sentry alerts and integrations",
|
||||||
AutoComplete: true,
|
AutoComplete: true,
|
||||||
AutoCompleteDesc: "Available commands: help, link, unlink, list",
|
AutocompleteData: p.getAutocompleteData(),
|
||||||
AutoCompleteHint: "help | link <project> | unlink <project> | list",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.API.RegisterCommand(command); err != nil {
|
if err := p.API.RegisterCommand(command); err != nil {
|
||||||
@ -67,3 +66,49 @@ func (p *Plugin) ExecuteCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) getAutocompleteData() *model.AutocompleteData {
|
||||||
|
sentry := model.NewAutocompleteData(
|
||||||
|
"sentry",
|
||||||
|
"",
|
||||||
|
"Sentry integration commands",
|
||||||
|
)
|
||||||
|
|
||||||
|
setup := model.NewAutocompleteData(
|
||||||
|
"setup",
|
||||||
|
"",
|
||||||
|
"Initial setup of Sentry integration",
|
||||||
|
)
|
||||||
|
|
||||||
|
help := model.NewAutocompleteData(
|
||||||
|
"help",
|
||||||
|
"",
|
||||||
|
"Show help for Sentry commands",
|
||||||
|
)
|
||||||
|
|
||||||
|
list := model.NewAutocompleteData(
|
||||||
|
"list",
|
||||||
|
"",
|
||||||
|
"List linked Sentry projects",
|
||||||
|
)
|
||||||
|
|
||||||
|
link := model.NewAutocompleteData(
|
||||||
|
"link",
|
||||||
|
"[project]",
|
||||||
|
"Link a Sentry project to this channel",
|
||||||
|
)
|
||||||
|
|
||||||
|
unlink := model.NewAutocompleteData(
|
||||||
|
"unlink",
|
||||||
|
"[project]",
|
||||||
|
"Unlink a Sentry project from this channel",
|
||||||
|
)
|
||||||
|
|
||||||
|
sentry.AddCommand(setup)
|
||||||
|
sentry.AddCommand(help)
|
||||||
|
sentry.AddCommand(list)
|
||||||
|
sentry.AddCommand(link)
|
||||||
|
sentry.AddCommand(unlink)
|
||||||
|
|
||||||
|
return sentry
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
@ -34,13 +35,21 @@ func (p *Plugin) ensureBot() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) getChannelForProject(project string) (string, error) {
|
func (p *Plugin) getChannelForProject(projectID string) (string, error) {
|
||||||
key := "sentry:project:" + project
|
key := "ru.loop.plugin.sentry:project:" + projectID
|
||||||
|
|
||||||
data, appErr := p.API.KVGet(key)
|
data, appErr := p.API.KVGet(key)
|
||||||
if appErr != nil || data == nil {
|
if appErr != nil {
|
||||||
return "", appErr
|
return "", appErr
|
||||||
}
|
}
|
||||||
|
if data == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
return string(data), nil
|
var project LinkedProject
|
||||||
|
if err := json.Unmarshal(data, &project); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return project.ChannelID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,11 +12,26 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Plugin) fetchSentryProjectID(projectSlug string) (string, error) {
|
var availableFields = []*model.PostActionOptions{
|
||||||
|
{Text: "Project", Value: "project"},
|
||||||
|
{Text: "Project ID", Value: "project_id"},
|
||||||
|
{Text: "Issue ID", Value: "issue_id"},
|
||||||
|
{Text: "Environment", Value: "environment"},
|
||||||
|
{Text: "Level", Value: "level"},
|
||||||
|
{Text: "Culprit", Value: "culprit"},
|
||||||
|
{Text: "Logger", Value: "logger"},
|
||||||
|
{Text: "Platform", Value: "platform"},
|
||||||
|
{Text: "Release", Value: "release"},
|
||||||
|
{Text: "User", Value: "user"},
|
||||||
|
{Text: "Exception", Value: "exception"},
|
||||||
|
{Text: "Tags", Value: "tags"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) fetchSentryProject(projectSlug string) (*LinkedProject, error) {
|
||||||
cfg := p.GetConfiguration()
|
cfg := p.GetConfiguration()
|
||||||
|
|
||||||
if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" {
|
if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" {
|
||||||
return "", errors.New("sentry is not configured")
|
return nil, errors.New("sentry is not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf(
|
url := fmt.Sprintf(
|
||||||
@ -28,7 +43,7 @@ func (p *Plugin) fetchSentryProjectID(projectSlug string) (string, error) {
|
|||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Authorization", "Bearer "+cfg.SentryAuthToken)
|
req.Header.Set("Authorization", "Bearer "+cfg.SentryAuthToken)
|
||||||
@ -37,27 +52,34 @@ func (p *Plugin) fetchSentryProjectID(projectSlug string) (string, error) {
|
|||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
return "", fmt.Errorf("sentry api error (%d): %s", resp.StatusCode, string(body))
|
return nil, fmt.Errorf("sentry api error (%d): %s", resp.StatusCode, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.ID == "" {
|
if result.ID == "" {
|
||||||
return "", errors.New("project id not found")
|
return nil, errors.New("project id not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.ID, nil
|
return &LinkedProject{
|
||||||
|
ID: result.ID,
|
||||||
|
Slug: result.Slug,
|
||||||
|
Name: result.Name,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) {
|
func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.CommandResponse, *model.AppError) {
|
||||||
@ -68,68 +90,56 @@ func (p *Plugin) commandLink(args *model.CommandArgs, split []string) (*model.Co
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := p.GetConfiguration()
|
|
||||||
if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" {
|
|
||||||
return &model.CommandResponse{
|
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
|
||||||
Text: "❌ Sentry is not configured. Please fill plugin settings.",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
projectSlug := split[2]
|
projectSlug := split[2]
|
||||||
channelID := args.ChannelId
|
channelID := args.ChannelId
|
||||||
|
|
||||||
// fetch project id from Sentry
|
project, err := p.fetchSentryProject(projectSlug)
|
||||||
projectID, err := p.fetchSentryProjectID(projectSlug)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.API.LogError("Failed to fetch Sentry project", "project", projectSlug, "err", err.Error())
|
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: "❌ Failed to fetch Sentry project: " + err.Error(),
|
Text: "❌ " + err.Error(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// projectID -> channel
|
project.ChannelID = channelID
|
||||||
if err := p.API.KVSet("sentry:project:"+projectID, []byte(channelID)); err != nil {
|
|
||||||
|
// сохраняем проект
|
||||||
|
bytes, _ := json.Marshal(project)
|
||||||
|
if err := p.API.KVSet("ru.loop.plugin.sentry:project:"+project.ID, bytes); err != nil {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: "❌ Failed to save project mapping",
|
Text: "❌ Failed to save project",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// store list of linked projects
|
// обновляем список IDs
|
||||||
data, _ := p.API.KVGet("sentry:projects")
|
data, _ := p.API.KVGet("ru.loop.plugin.sentry:projects")
|
||||||
var projects []string
|
var projects []string
|
||||||
if data != nil {
|
if data != nil {
|
||||||
projects = strings.Split(string(data), ",")
|
_ = json.Unmarshal(data, &projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
exists := false
|
||||||
for _, id := range projects {
|
for _, id := range projects {
|
||||||
if id == projectID {
|
if id == project.ID {
|
||||||
found = true
|
exists = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !exists {
|
||||||
projects = append(projects, projectID)
|
projects = append(projects, project.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = p.API.KVSet("sentry:projects", []byte(strings.Join(projects, ",")))
|
updated, _ := json.Marshal(projects)
|
||||||
|
_ = p.API.KVSet("ru.loop.plugin.sentry:projects", updated)
|
||||||
p.API.CreatePost(&model.Post{
|
|
||||||
UserId: p.botUserID,
|
|
||||||
ChannelId: channelID,
|
|
||||||
Message: fmt.Sprintf(
|
|
||||||
"✅ Sentry project `%s` (id: %s) linked to this channel",
|
|
||||||
projectSlug,
|
|
||||||
projectID,
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: "🔗 Linked project `" + projectSlug + "`",
|
Text: fmt.Sprintf(
|
||||||
|
"✅ Linked **%s** (`%s`) to this channel",
|
||||||
|
project.Name,
|
||||||
|
project.Slug,
|
||||||
|
),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,44 +151,74 @@ func (p *Plugin) commandUnlink(args *model.CommandArgs, split []string) (*model.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
project := split[2]
|
slug := split[2]
|
||||||
|
|
||||||
// удаляем привязку project -> channel
|
data, _ := p.API.KVGet("ru.loop.plugin.sentry:projects")
|
||||||
p.API.KVDelete("sentry:project:" + project)
|
if data == nil {
|
||||||
|
return &model.CommandResponse{
|
||||||
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
Text: "ℹ️ No linked projects",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// обновляем список проектов
|
|
||||||
data, _ := p.API.KVGet("sentry:projects")
|
|
||||||
var projects []string
|
var projects []string
|
||||||
if data != nil {
|
_ = json.Unmarshal(data, &projects)
|
||||||
projects = strings.Split(string(data), ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
var newProjects []string
|
var (
|
||||||
for _, p := range projects {
|
newProjects []string
|
||||||
if p != project {
|
removed *LinkedProject
|
||||||
newProjects = append(newProjects, p)
|
)
|
||||||
|
|
||||||
|
for _, id := range projects {
|
||||||
|
pData, _ := p.API.KVGet("ru.loop.plugin.sentry:project:" + id)
|
||||||
|
if pData == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var proj LinkedProject
|
||||||
|
_ = json.Unmarshal(pData, &proj)
|
||||||
|
|
||||||
|
if proj.Slug == slug {
|
||||||
|
removed = &proj
|
||||||
|
_ = p.API.KVDelete("ru.loop.plugin.sentry:project:" + id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newProjects = append(newProjects, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.API.KVSet("sentry:projects", []byte(strings.Join(newProjects, ",")))
|
if removed == nil {
|
||||||
|
return &model.CommandResponse{
|
||||||
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
Text: "❌ Project not found",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, _ := json.Marshal(newProjects)
|
||||||
|
_ = p.API.KVSet("ru.loop.plugin.sentry:projects", bytes)
|
||||||
|
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: "✅ Unlinked project `" + project + "`",
|
Text: fmt.Sprintf(
|
||||||
|
"✅ Unlinked **%s** (`%s`)",
|
||||||
|
removed.Name,
|
||||||
|
removed.Slug,
|
||||||
|
),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||||
// Для MVP мы перебираем все известные ключи в KV
|
data, _ := p.API.KVGet("ru.loop.plugin.sentry:projects")
|
||||||
// Так как KVListKeys нет, можно завести slice всех проектов,
|
if data == nil {
|
||||||
// которые были когда-либо linked через /sentry link
|
return &model.CommandResponse{
|
||||||
// Эти проекты мы храним отдельно в ключе "sentry:projects"
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
data, _ := p.API.KVGet("sentry:projects")
|
Text: "_No linked Sentry projects_",
|
||||||
var projects []string
|
}, nil
|
||||||
if data != nil {
|
|
||||||
projects = strings.Split(string(data), ",")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var projects []string
|
||||||
|
_ = json.Unmarshal(data, &projects)
|
||||||
|
|
||||||
if len(projects) == 0 {
|
if len(projects) == 0 {
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
@ -188,27 +228,34 @@ func (p *Plugin) commandList(args *model.CommandArgs) (*model.CommandResponse, *
|
|||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
for _, project := range projects {
|
for _, id := range projects {
|
||||||
channelData, _ := p.API.KVGet("sentry:project:" + project)
|
pData, _ := p.API.KVGet("ru.loop.plugin.sentry:project:" + id)
|
||||||
if channelData == nil {
|
if pData == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
channelID := string(channelData)
|
|
||||||
|
|
||||||
channel, err := p.API.GetChannel(channelID)
|
var project LinkedProject
|
||||||
channelName := channelID
|
_ = json.Unmarshal(pData, &project)
|
||||||
if err == nil && channel != nil {
|
|
||||||
channelName = "~" + channel.Name
|
channelName := project.ChannelID
|
||||||
|
if ch, err := p.API.GetChannel(project.ChannelID); err == nil {
|
||||||
|
channelName = "~" + ch.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
lines = append(lines, "• "+project+" → "+channelName)
|
lines = append(
|
||||||
|
lines,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"• **%s** (`%s`) → %s",
|
||||||
|
project.Name,
|
||||||
|
project.Slug,
|
||||||
|
channelName,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
text := "🔗 **Linked Sentry projects:**\n\n" + strings.Join(lines, "\n")
|
|
||||||
|
|
||||||
return &model.CommandResponse{
|
return &model.CommandResponse{
|
||||||
ResponseType: model.CommandResponseTypeEphemeral,
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
Text: text,
|
Text: "🔗 **Linked Sentry projects:**\n\n" + strings.Join(lines, "\n"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,12 +277,8 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
channels, _ := p.API.GetChannelsForTeamForUser(
|
// Получаем каналы пользователя
|
||||||
args.TeamId,
|
channels, _ := p.API.GetChannelsForTeamForUser(args.TeamId, args.UserId, false)
|
||||||
args.UserId,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
options := []*model.PostActionOptions{}
|
options := []*model.PostActionOptions{}
|
||||||
for _, ch := range channels {
|
for _, ch := range channels {
|
||||||
options = append(options, &model.PostActionOptions{
|
options = append(options, &model.PostActionOptions{
|
||||||
@ -244,6 +287,9 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Поля webhook (для CSV ввода)
|
||||||
|
fieldList := "project,project_id,issue_id,environment,level,culprit,logger,platform,release,user,exception,tags"
|
||||||
|
|
||||||
modal := &model.Dialog{
|
modal := &model.Dialog{
|
||||||
Title: "Sentry Setup",
|
Title: "Sentry Setup",
|
||||||
CallbackId: "sentry_setup",
|
CallbackId: "sentry_setup",
|
||||||
@ -257,21 +303,39 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse,
|
|||||||
Default: args.ChannelId,
|
Default: args.ChannelId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DisplayName: "Sentry Project",
|
DisplayName: "Sentry Project Slug",
|
||||||
Name: "project",
|
Name: "project_slug",
|
||||||
Type: "text",
|
Type: "text",
|
||||||
|
Placeholder: "Enter project slug",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "Webhook fields",
|
||||||
|
Name: "fields",
|
||||||
|
Type: "text",
|
||||||
|
Default: fieldList,
|
||||||
|
Placeholder: "Comma-separated list of fields to show in webhook",
|
||||||
|
HelpText: "Example: project,issue_id,environment,level,exception",
|
||||||
|
},
|
||||||
|
{DisplayName: "Fatal color", Name: "color_fatal", Type: "text", Default: "#B10DC9"},
|
||||||
|
{DisplayName: "Error color", Name: "color_error", Type: "text", Default: "#FF4136"},
|
||||||
|
{DisplayName: "Warning color", Name: "color_warning", Type: "text", Default: "#FF851B"},
|
||||||
|
{DisplayName: "Info color", Name: "color_info", Type: "text", Default: "#0074D9"},
|
||||||
|
{DisplayName: "Debug color", Name: "color_debug", Type: "text", Default: "#2ECC40"},
|
||||||
|
{DisplayName: "Log color", Name: "color_log", Type: "text", Default: "#AAAAAA"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
req := model.OpenDialogRequest{
|
req := model.OpenDialogRequest{
|
||||||
TriggerId: args.TriggerId,
|
TriggerId: args.TriggerId,
|
||||||
URL: "/plugins/" + "ru.loop.plugin.sentry" + "/dialog/submit",
|
URL: "/plugins/ru.loop.plugin.sentry/dialog/submit",
|
||||||
Dialog: *modal,
|
Dialog: *modal,
|
||||||
}
|
}
|
||||||
|
|
||||||
if appErr := p.API.OpenInteractiveDialog(req); appErr != nil {
|
if appErr := p.API.OpenInteractiveDialog(req); appErr != nil {
|
||||||
return nil, appErr
|
return &model.CommandResponse{
|
||||||
|
ResponseType: model.CommandResponseTypeEphemeral,
|
||||||
|
Text: "❌ Failed to open setup dialog: " + appErr.Error(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &model.CommandResponse{}, nil
|
return &model.CommandResponse{}, nil
|
||||||
|
|||||||
@ -54,3 +54,12 @@ type SentryPayload struct {
|
|||||||
TriggeredRule string `json:"triggered_rule"`
|
TriggeredRule string `json:"triggered_rule"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkedProject struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
LevelColors map[string]string `json:"level_colors"`
|
||||||
|
Fields []string `json:"fields"`
|
||||||
|
}
|
||||||
|
|||||||
@ -38,6 +38,22 @@ func getTagFromArray(tags [][]string, key string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) getProject(projectID string) (*LinkedProject, error) {
|
||||||
|
key := "ru.loop.plugin.sentry:project:" + projectID
|
||||||
|
|
||||||
|
data, appErr := p.API.KVGet(key)
|
||||||
|
if appErr != nil || data == nil {
|
||||||
|
return nil, appErr
|
||||||
|
}
|
||||||
|
|
||||||
|
var project LinkedProject
|
||||||
|
if err := json.Unmarshal(data, &project); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
func formatStacktrace(ex *SentryExceptionValue) string {
|
func formatStacktrace(ex *SentryExceptionValue) string {
|
||||||
if ex == nil || len(ex.Stacktrace.Frames) == 0 {
|
if ex == nil || len(ex.Stacktrace.Frames) == 0 {
|
||||||
return ""
|
return ""
|
||||||
@ -69,6 +85,13 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
project, err := p.getProject(strconv.Itoa(event.Project))
|
||||||
|
if err != nil || project == nil || project.ChannelID == "" {
|
||||||
|
p.API.LogWarn("No channel linked for project", "project", event.Project)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
environment := getTagFromArray(event.Tags, "environment")
|
environment := getTagFromArray(event.Tags, "environment")
|
||||||
release := getTagFromArray(event.Tags, "release")
|
release := getTagFromArray(event.Tags, "release")
|
||||||
user := getTagFromArray(event.Tags, "user")
|
user := getTagFromArray(event.Tags, "user")
|
||||||
@ -79,6 +102,11 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
TitleLink: event.WebURL,
|
TitleLink: event.WebURL,
|
||||||
Text: event.Message,
|
Text: event.Message,
|
||||||
Fields: []*model.SlackAttachmentField{
|
Fields: []*model.SlackAttachmentField{
|
||||||
|
{
|
||||||
|
Title: "Project",
|
||||||
|
Value: fmt.Sprintf("%s (`%s`)", project.Name, project.Slug),
|
||||||
|
Short: true,
|
||||||
|
},
|
||||||
{Title: "Project ID", Value: strconv.Itoa(event.Project), Short: true},
|
{Title: "Project ID", Value: strconv.Itoa(event.Project), Short: true},
|
||||||
{Title: "Issue ID", Value: event.IssueID, Short: true},
|
{Title: "Issue ID", Value: event.IssueID, Short: true},
|
||||||
{Title: "Environment", Value: environment, Short: true},
|
{Title: "Environment", Value: environment, Short: true},
|
||||||
@ -91,15 +119,15 @@ func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// if event.Exception != nil && len(event.Exception.Values) > 0 {
|
if event.Exception != nil && len(event.Exception.Values) > 0 {
|
||||||
// for _, ex := range event.Exception.Values {
|
for _, ex := range event.Exception.Values {
|
||||||
// attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{
|
attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{
|
||||||
// Title: "Exception",
|
Title: "Exception",
|
||||||
// Value: fmt.Sprintf("Type: %s\nValue: %s\nStacktrace:\n%s", ex.Type, ex.Value, formatStacktrace(&ex)),
|
Value: fmt.Sprintf("Type: %s\nValue: %s\nStacktrace:\n%s", ex.Type, ex.Value, formatStacktrace(&ex)),
|
||||||
// Short: true,
|
Short: true,
|
||||||
// })
|
})
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
for _, tag := range event.Tags {
|
for _, tag := range event.Tags {
|
||||||
if len(tag) != 2 {
|
if len(tag) != 2 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user