package plugin import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/mattermost/mattermost/server/public/model" ) func levelToColor(level string) string { switch strings.ToLower(level) { case "fatal": return "#B10DC9" // фиолетовый, критический case "error": return "#FF4136" // красный case "warning": return "#FF851B" // оранжевый case "log": return "#AAAAAA" // серый case "info": return "#0074D9" // синий case "debug": return "#2ECC40" // зелёный default: return "#AAAAAA" // серый для неизвестных } } func getTagFromArray(tags [][]string, key string) string { for _, t := range tags { if len(t) == 2 && t[0] == key { return t[1] } } return "" } func formatStacktrace(ex *SentryExceptionValue) string { if ex == nil || len(ex.Stacktrace.Frames) == 0 { return "" } lines := make([]string, 0, len(ex.Stacktrace.Frames)) for _, f := range ex.Stacktrace.Frames { lines = append(lines, fmt.Sprintf("%s:%d %s - %s", f.Filename, f.Lineno, f.Function, f.ContextLine)) } return strings.Join(lines, "\n") } func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var payload SentryPayload if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { p.API.LogError("Failed to decode Sentry payload", "error", err.Error()) w.WriteHeader(http.StatusBadRequest) return } event := payload.Data.Event channelID, err := p.getChannelForProject(strconv.Itoa(event.Project)) if err != nil || channelID == "" { p.API.LogWarn("No channel linked for project", "project", event.Project) 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 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: 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) }