diff --git a/server/plugin/commands.go b/server/plugin/commands.go index d6895ba..7cdd327 100644 --- a/server/plugin/commands.go +++ b/server/plugin/commands.go @@ -135,7 +135,10 @@ func (p *Plugin) commandHelp() *model.CommandResponse { } func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + p.API.LogDebug("commandSetup called", "triggerId", args.TriggerId) + if args.TriggerId == "" { + p.API.LogWarn("commandSetup: triggerId is empty") return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: "This command must be run from Mattermost UI", @@ -145,20 +148,61 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, // Получаем каналы пользователя channels, appErr := p.API.GetChannelsForTeamForUser(args.TeamId, args.UserId, false) if appErr != nil { + p.API.LogError("Failed to load channels", "error", appErr.Error()) return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: "❌ Failed to load channels", }, nil } - options := []*model.PostActionOptions{} + channelOptions := []*model.PostActionOptions{} for _, ch := range channels { - options = append(options, &model.PostActionOptions{ + channelOptions = append(channelOptions, &model.PostActionOptions{ Text: ch.DisplayName, Value: ch.Id, }) } + // Получаем проекты из Sentry + sentryProjects, err := p.fetchSentryProjects() + var projectElement model.DialogElement + + if err != nil { + // Если не удалось получить проекты, используем текстовое поле как fallback + p.API.LogWarn("Failed to load Sentry projects, using text input", "error", err.Error()) + projectElement = model.DialogElement{ + DisplayName: "Sentry project slug", + Name: "project_slug", + Type: "text", + Placeholder: "frontend-app", + } + } else if len(sentryProjects) == 0 { + // Если список проектов пустой, используем текстовое поле + p.API.LogWarn("No Sentry projects found, using text input") + projectElement = model.DialogElement{ + DisplayName: "Sentry project slug", + Name: "project_slug", + Type: "text", + Placeholder: "frontend-app", + } + } else { + // Формируем опции для select проектов + projectOptions := []*model.PostActionOptions{} + for _, proj := range sentryProjects { + projectOptions = append(projectOptions, &model.PostActionOptions{ + Text: fmt.Sprintf("%s (%s)", proj.Name, proj.Slug), + Value: proj.Slug, + }) + } + projectElement = model.DialogElement{ + DisplayName: "Sentry project", + Name: "project_slug", + Type: "select", + Options: projectOptions, + } + p.API.LogDebug("Loaded Sentry projects", "count", len(sentryProjects)) + } + modal := &model.Dialog{ Title: "Sentry Setup", CallbackId: "sentry_setup", @@ -169,15 +213,10 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, DisplayName: "Default channel", Name: "default_channel_id", Type: "select", - Options: options, + Options: channelOptions, Default: args.ChannelId, }, - { - DisplayName: "Sentry project slug", - Name: "project_slug", - Type: "text", - Placeholder: "frontend-app", - }, + projectElement, // ───── Webhook types ───── {DisplayName: "Event alerts", Name: "hook_event_alert", Type: "bool", Default: "true"}, @@ -187,6 +226,8 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, }, } + p.API.LogDebug("Opening dialog", "elements_count", len(modal.Elements)) + req := model.OpenDialogRequest{ TriggerId: args.TriggerId, URL: "/plugins/ru.loop.plugin.sentry/dialog/submit", @@ -194,16 +235,17 @@ func (p *Plugin) commandSetup(args *model.CommandArgs) (*model.CommandResponse, } if appErr := p.API.OpenInteractiveDialog(req); appErr != nil { + p.API.LogError("Failed to open setup dialog", "error", appErr.Error()) return &model.CommandResponse{ ResponseType: model.CommandResponseTypeEphemeral, Text: "❌ Failed to open setup dialog: " + appErr.Error(), }, nil } + p.API.LogDebug("Dialog opened successfully") return &model.CommandResponse{}, nil } - func (p *Plugin) commandSetupHook( args *model.CommandArgs, hook string, diff --git a/server/plugin/sentry_client.go b/server/plugin/sentry_client.go index 40032c5..f30b0e9 100644 --- a/server/plugin/sentry_client.go +++ b/server/plugin/sentry_client.go @@ -66,6 +66,62 @@ func (p *Plugin) fetchSentryProject(projectSlug string) (*LinkedProject, error) }, nil } +// SentryProject представляет проект из Sentry API +type SentryProject struct { + ID string `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` +} + +// fetchSentryProjects получает список всех проектов из Sentry API +func (p *Plugin) fetchSentryProjects() ([]SentryProject, error) { + cfg := p.GetConfiguration() + + if cfg.SentryUrl == "" || cfg.SentryOrganisationName == "" || cfg.SentryAuthToken == "" { + return nil, errors.New("sentry is not configured") + } + + url := fmt.Sprintf( + "%s/api/0/organizations/%s/projects/", + strings.TrimRight(cfg.SentryUrl, "/"), + cfg.SentryOrganisationName, + ) + + p.API.LogDebug("Fetching Sentry projects", "url", url) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + p.API.LogError("Failed to create request", "error", err.Error()) + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+cfg.SentryAuthToken) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + p.API.LogError("Failed to execute request", "error", err.Error()) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + p.API.LogError("Sentry API error", "status", resp.StatusCode, "body", string(body)) + return nil, fmt.Errorf("sentry api error (%d): %s", resp.StatusCode, body) + } + + var projects []SentryProject + if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil { + p.API.LogError("Failed to decode response", "error", err.Error()) + return nil, err + } + + p.API.LogDebug("Fetched Sentry projects", "count", len(projects)) + return projects, nil +} + // linkProjectToChannel связывает проект Sentry с каналом Mattermost func (p *Plugin) linkProjectToChannel( projectSlug string,