package plugin import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/mattermost/mattermost/server/public/model" ) // handleEventAlert обрабатывает вебхук события Sentry func (p *Plugin) handleEventAlert(w http.ResponseWriter, r *http.Request) { var payload SentryPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { p.API.LogError("Failed to decode Sentry event alert payload", "error", err.Error()) w.WriteHeader(http.StatusBadRequest) return } event := payload.Data.Event 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 } if !project.Hooks.EventAlert { p.API.LogDebug("Event alert hook disabled for project", "project", project.Slug) w.WriteHeader(http.StatusOK) return } environment := getTagFromArray(event.Tags, "environment") release := getTagFromArray(event.Tags, "release") user := getTagFromArray(event.Tags, "user") attachment := &model.SlackAttachment{ Color: levelToColor(event.Level), Title: event.Title, TitleLink: event.WebURL, Text: event.Message, 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: "Issue ID", Value: event.IssueID, Short: true}, {Title: "Environment", Value: environment, Short: true}, {Title: "Level", Value: event.Level, Short: true}, {Title: "Culprit", Value: event.Culprit, Short: false}, {Title: "Logger", Value: event.Logger, Short: true}, {Title: "Platform", Value: event.Platform, Short: true}, {Title: "Release", Value: release, Short: true}, {Title: "User", Value: user, Short: true}, }, } if event.Exception != nil && len(event.Exception.Values) > 0 { for _, ex := range event.Exception.Values { attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{ Title: "Exception", Value: fmt.Sprintf( "Type: %s\nValue: %s\nStacktrace:\n%s", ex.Type, ex.Value, formatStacktrace(&ex), ), Short: true, }) } } for _, tag := range event.Tags { if len(tag) != 2 { continue } key := tag[0] value := tag[1] if key == "environment" || key == "release" || key == "user" { continue } attachment.Fields = append(attachment.Fields, &model.SlackAttachmentField{ Title: key, Value: value, Short: true, }) } post := &model.Post{ UserId: p.botUserID, ChannelId: project.ChannelID, Props: map[string]interface{}{ "attachments": []*model.SlackAttachment{attachment}, }, } if _, err := p.API.CreatePost(post); err != nil { p.API.LogError("Failed to create post", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } // handleComment обрабатывает вебхук комментария Sentry func (p *Plugin) handleComment(w http.ResponseWriter, r *http.Request) { var payload SentryCommentPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { p.API.LogError("Failed to decode Sentry comment payload", "error", err.Error()) w.WriteHeader(http.StatusBadRequest) return } project, err := p.getProjectBySlug(payload.Data.ProjectSlug) if err != nil || project == nil || project.ChannelID == "" { p.API.LogWarn("No channel linked for project", "slug", payload.Data.ProjectSlug) w.WriteHeader(http.StatusOK) return } if !project.Hooks.Comment { p.API.LogDebug("Comment hook disabled for project", "project", project.Slug) w.WriteHeader(http.StatusOK) return } attachment := &model.SlackAttachment{ Color: "#439FE0", Title: "💬 New Sentry comment", Text: fmt.Sprintf( "*%s* commented on issue `%d`\n\n>%s", payload.Actor.Name, payload.Data.IssueID, payload.Data.Comment, ), Fields: []*model.SlackAttachmentField{ {Title: "Project", Value: project.Name, Short: true}, {Title: "Action", Value: payload.Action, Short: true}, {Title: "Comment ID", Value: strconv.Itoa(payload.Data.CommentID), Short: true}, }, } post := &model.Post{ UserId: p.botUserID, ChannelId: project.ChannelID, Props: map[string]interface{}{ "attachments": []*model.SlackAttachment{attachment}, }, } if _, err := p.API.CreatePost(post); err != nil { p.API.LogError("Failed to create comment post", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } // handleMetricAlert обрабатывает вебхук метрического алерта Sentry func (p *Plugin) handleMetricAlert(w http.ResponseWriter, r *http.Request) { var payload SentryMetricAlertPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { p.API.LogError("Failed to decode metric alert payload", "error", err.Error()) w.WriteHeader(http.StatusBadRequest) return } alert := payload.Data.MetricAlert rule := alert.AlertRule var project *LinkedProject if len(alert.Projects) > 0 { project, _ = p.getProjectBySlug(alert.Projects[0]) } if project == nil || project.ChannelID == "" { p.API.LogWarn("No channel linked for metric alert", "projects", alert.Projects) w.WriteHeader(http.StatusOK) return } if !project.Hooks.MetricAlert { p.API.LogDebug("Metric alert hook disabled for project", "project", project.Slug) w.WriteHeader(http.StatusOK) return } color := "#E01E5A" // default: critical switch payload.Action { case "resolved": color = "#2EB67D" case "warning": color = "#ECB22E" } attachment := &model.SlackAttachment{ Color: color, Title: payload.Data.DescriptionTitle, TitleLink: payload.Data.WebURL, Text: payload.Data.DescriptionText, Fields: []*model.SlackAttachmentField{ { Title: "Rule", Value: rule.Name, Short: true, }, { Title: "Aggregate", Value: rule.Aggregate, Short: true, }, { Title: "Query", Value: rule.Query, Short: false, }, { Title: "Window (min)", Value: strconv.Itoa(rule.TimeWindow), Short: true, }, { Title: "Action", Value: payload.Action, Short: true, }, }, } post := &model.Post{ UserId: p.botUserID, ChannelId: project.ChannelID, Props: map[string]interface{}{ "attachments": []*model.SlackAttachment{attachment}, }, } if _, err := p.API.CreatePost(post); err != nil { p.API.LogError("Failed to create metric alert post", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } // handleIssue обрабатывает вебхук issue Sentry func (p *Plugin) handleIssue(w http.ResponseWriter, r *http.Request) { var payload SentryIssuePayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { p.API.LogError("Failed to decode issue payload", "error", err.Error()) w.WriteHeader(http.StatusBadRequest) return } issue := payload.Data.Issue project, err := p.getProjectBySlug(issue.Project.Slug) if err != nil || project == nil || project.ChannelID == "" { p.API.LogWarn("No channel linked for issue", "project", issue.Project.Slug) w.WriteHeader(http.StatusOK) return } if !project.Hooks.Issue { p.API.LogDebug("Issue hook disabled for project", "project", project.Slug) w.WriteHeader(http.StatusOK) return } color := "#E01E5A" // default: red switch payload.Action { case "resolved": color = "#2EB67D" case "archived": color = "#6B7280" case "unresolved": color = "#F97316" } attachment := &model.SlackAttachment{ Color: color, Title: fmt.Sprintf("[%s] %s", issue.ShortID, issue.Title), TitleLink: issue.WebURL, Text: fmt.Sprintf( "**Action:** `%s`\n**Status:** `%s`\n**Level:** `%s`\n**Priority:** `%s`", payload.Action, issue.Status, issue.Level, issue.Priority, ), Fields: []*model.SlackAttachmentField{ {Title: "Project", Value: issue.Project.Name, Short: true}, {Title: "Platform", Value: issue.Platform, Short: true}, {Title: "Type", Value: issue.IssueType, Short: true}, {Title: "Category", Value: issue.IssueCategory, Short: true}, {Title: "Events", Value: issue.Count, Short: true}, {Title: "Users", Value: strconv.Itoa(issue.UserCount), Short: true}, }, } post := &model.Post{ UserId: p.botUserID, ChannelId: project.ChannelID, Props: map[string]interface{}{ "attachments": []*model.SlackAttachment{attachment}, }, } if _, err := p.API.CreatePost(post); err != nil { p.API.LogError("Failed to create issue post", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }