370 lines
13 KiB
Ruby
370 lines
13 KiB
Ruby
#!/usr/bin/env ruby
|
|
|
|
require 'bugsnag/api'
|
|
|
|
class BugsnagApiClient
|
|
def initialize
|
|
@api_key = ENV.fetch('BUGSNAG_DATA_API_KEY')
|
|
@project_id = ENV['BUGSNAG_PROJECT_ID'] # Optional - needed only for error-specific commands
|
|
|
|
validate_api_key
|
|
configure_api
|
|
end
|
|
|
|
def list_errors(limit: 20, status: nil, severity: nil)
|
|
errors_data = fetch_errors(limit: limit, status: status, severity: severity)
|
|
format_errors_list(errors_data)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении списка ошибок")
|
|
end
|
|
|
|
def get_error_details(error_id)
|
|
require_project_id!
|
|
|
|
response = Bugsnag::Api.client.error(@project_id, error_id)
|
|
format_error_details(response)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении деталей ошибки")
|
|
end
|
|
|
|
def resolve_error(error_id)
|
|
require_project_id!
|
|
|
|
# Try to resolve via API first
|
|
begin
|
|
Bugsnag::Api.client.update_errors(@project_id, [error_id], "resolve")
|
|
"✅ Ошибка `#{error_id}` успешно отмечена как выполненная!"
|
|
rescue Bugsnag::Api::Error => e
|
|
# Fallback to adding a resolution comment
|
|
begin
|
|
comment_text = "🔧 **MARKED AS RESOLVED** - Эта ошибка была помечена как выполненная через Bugsnag skill."
|
|
Bugsnag::Api.client.create_comment(@project_id, error_id, comment_text)
|
|
"✅ Ошибка `#{error_id}` помечена как выполненная через комментарий. Пожалуйста, закройте ошибку вручную в Bugsnag dashboard."
|
|
rescue Bugsnag::Api::Error => comment_error
|
|
handle_api_error(comment_error, "пометки ошибки как выполненной")
|
|
end
|
|
end
|
|
end
|
|
|
|
def add_comment(error_id, message)
|
|
require_project_id!
|
|
|
|
Bugsnag::Api.client.create_comment(@project_id, error_id, message)
|
|
"✅ Комментарий успешно добавлен к ошибке `#{error_id}`"
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "добавлении комментария")
|
|
end
|
|
|
|
def get_error_events(error_id, limit: 10)
|
|
require_project_id!
|
|
|
|
options = {}
|
|
options[:limit] = limit if limit
|
|
|
|
response = Bugsnag::Api.client.error_events(@project_id, error_id, options)
|
|
format_events_list(response)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении событий ошибки")
|
|
end
|
|
|
|
def analyze_errors
|
|
errors_data = fetch_errors(limit: 50)
|
|
errors = normalize_errors_array(errors_data)
|
|
analyze_error_patterns(errors)
|
|
end
|
|
|
|
def list_organizations
|
|
response = Bugsnag::Api.client.organizations
|
|
format_organizations_list(response)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении списка организаций")
|
|
end
|
|
|
|
def list_projects
|
|
# Get all organizations first
|
|
orgs = Bugsnag::Api.client.organizations
|
|
all_projects = []
|
|
|
|
# Get projects for each organization
|
|
orgs.each do |org|
|
|
org_projects = Bugsnag::Api.client.projects(org['id'])
|
|
all_projects.concat(org_projects)
|
|
rescue Bugsnag::Api::Error
|
|
# Skip this org if error, continue with others
|
|
next
|
|
end
|
|
|
|
format_projects_list(all_projects)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении списка проектов")
|
|
end
|
|
|
|
def list_comments(error_id)
|
|
require_project_id!
|
|
|
|
response = Bugsnag::Api.client.comments(@project_id, error_id)
|
|
format_comments_list(response)
|
|
rescue Bugsnag::Api::Error => e
|
|
handle_api_error(e, "получении комментариев")
|
|
end
|
|
|
|
private
|
|
|
|
def fetch_errors(limit: 20, status: nil, severity: nil)
|
|
require_project_id!
|
|
|
|
options = {}
|
|
options[:limit] = limit if limit
|
|
options[:status] = status if status
|
|
options[:severity] = severity if severity
|
|
|
|
Bugsnag::Api.client.errors(@project_id, nil, options)
|
|
end
|
|
|
|
def normalize_errors_array(errors_data)
|
|
errors_data.is_a?(Array) ? errors_data : errors_data['errors'] || []
|
|
end
|
|
|
|
def validate_api_key
|
|
unless @api_key
|
|
raise "Missing required environment variable: BUGSNAG_DATA_API_KEY"
|
|
end
|
|
end
|
|
|
|
def require_project_id!
|
|
unless @project_id
|
|
raise "Missing required environment variable: BUGSNAG_PROJECT_ID\n\n" \
|
|
"This command requires a project ID. Set it with:\n" \
|
|
"export BUGSNAG_PROJECT_ID='your_project_id'\n\n" \
|
|
"Use 'projects' command to list available project IDs."
|
|
end
|
|
end
|
|
|
|
def configure_api
|
|
Bugsnag::Api.configure do |config|
|
|
config.auth_token = @api_key
|
|
end
|
|
end
|
|
|
|
def handle_api_error(error, operation)
|
|
case error
|
|
when Bugsnag::Api::ClientError
|
|
"❌ Ошибка при #{operation}: ошибка клиента API - #{error.message}"
|
|
when Bugsnag::Api::ServerError
|
|
"❌ Ошибка при #{operation}: ошибка сервера Bugsnag - #{error.message}"
|
|
when Bugsnag::Api::InternalServerError
|
|
"❌ Ошибка при #{operation}: внутренняя ошибка сервера Bugsnag - #{error.message}"
|
|
else
|
|
"❌ Ошибка при #{operation}: #{error.message}"
|
|
end
|
|
end
|
|
|
|
def format_errors_list(errors_data)
|
|
errors = errors_data.is_a?(Array) ? errors_data : errors_data['errors'] || []
|
|
|
|
output = ["📋 Найдено ошибок: #{errors.length}\n"]
|
|
|
|
errors.each do |error|
|
|
status_emoji = case error['status']
|
|
when 'open' then '❌'
|
|
when 'resolved' then '✅'
|
|
when 'ignored' then '🚫'
|
|
else '❓'
|
|
end
|
|
|
|
output << "#{status_emoji} **#{error['error_class']}** (#{error['events']} событий)"
|
|
output << " ID: `#{error['id']}`"
|
|
output << " Severity: #{error['severity']}"
|
|
output << " Первое появление: #{error['first_seen']}"
|
|
output << " Последнее: #{error['last_seen']}"
|
|
output << " URL: #{error['url']}" if error['url']
|
|
output << ""
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
|
|
def format_error_details(error_data)
|
|
error = error_data['error'] || error_data
|
|
|
|
output = []
|
|
output << "🔍 **Детали ошибки:** #{error['error_class']}"
|
|
output << ""
|
|
output << "**Основная информация:**"
|
|
output << "• ID: `#{error['id']}`"
|
|
output << "• Статус: #{error['status']}"
|
|
output << "• Критичность: #{error['severity']}"
|
|
output << "• Событий: #{error['events']}"
|
|
output << "• Пользователи затронуто: #{error['users']}"
|
|
output << ""
|
|
|
|
if error['first_seen'] && error['last_seen']
|
|
output << "**Временные рамки:**"
|
|
output << "• Первое появление: #{error['first_seen']}"
|
|
output << "• Последнее: #{error['last_seen']}"
|
|
output << ""
|
|
end
|
|
|
|
output << "**Контекст:**"
|
|
output << "• App Version: #{error.dig('app', 'version') || 'N/A'}"
|
|
output << "• Release Stage: #{error.dig('app', 'releaseStage') || 'N/A'}"
|
|
output << "• Language: #{error['language'] || 'N/A'}"
|
|
output << "• Framework: #{error['framework'] || 'N/A'}"
|
|
output << ""
|
|
|
|
if error['url']
|
|
output << "**URL:** #{error['url']}"
|
|
output << ""
|
|
end
|
|
|
|
if error['message']
|
|
output << "**Сообщение:**"
|
|
output << "```"
|
|
output << error['message']
|
|
output << "```"
|
|
output << ""
|
|
end
|
|
|
|
output
|
|
end
|
|
|
|
def format_events_list(events_data)
|
|
events = events_data['events'] || []
|
|
|
|
output = ["📊 События ошибки (#{events.length}):\n"]
|
|
|
|
events.each_with_index do |event, index|
|
|
output << "**Событие #{index + 1}:**"
|
|
output << "• ID: `#{event['id']}`"
|
|
output << "• Время: #{event['receivedAt']}"
|
|
output << "• App Version: #{event['app']['releaseStage'] || 'N/A'}"
|
|
output << "• OS: #{event['device']['osName'] || 'N/A'} #{event['device']['osVersion'] || ''}"
|
|
|
|
if event['user']
|
|
output << "• Пользователь: #{event['user']['name'] || event['user']['id'] || 'N/A'}"
|
|
end
|
|
|
|
if event['message']
|
|
output << "• Сообщение: #{event['message']}"
|
|
end
|
|
|
|
output << ""
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
|
|
def analyze_error_patterns(errors)
|
|
critical_errors = errors.select { |e| e['severity'] == 'error' && e['status'] == 'open' }
|
|
warnings = errors.select { |e| e['severity'] == 'warning' && e['status'] == 'open' }
|
|
|
|
output = ["📈 **Анализ ошибок в проекте:**\n"]
|
|
|
|
output << "🔴 **Критичные ошибки (#{critical_errors.length}):**"
|
|
if critical_errors.any?
|
|
critical_errors.first(5).each do |error|
|
|
events_count = error['events'] || error['events_count'] || 0
|
|
output << "• #{error['error_class']} - #{events_count} событий (ID: #{error['id']})"
|
|
end
|
|
else
|
|
output << "• Нет критичных ошибок!"
|
|
end
|
|
output << ""
|
|
|
|
output << "🟡 **Предупреждения (#{warnings.length}):**"
|
|
if warnings.any?
|
|
warnings.first(5).each do |error|
|
|
events_count = error['events'] || error['events_count'] || 0
|
|
output << "• #{error['error_class']} - #{events_count} событий (ID: #{error['id']})"
|
|
end
|
|
else
|
|
output << "• Нет предупреждений!"
|
|
end
|
|
output << ""
|
|
|
|
# Частые паттерны ошибок
|
|
error_classes = errors.group_by { |e| e['error_class'] }
|
|
frequent_errors = error_classes.select { |klass, errs| errs.length > 1 }
|
|
|
|
if frequent_errors.any?
|
|
output << "🔄 **Повторяющиеся паттерны:**"
|
|
frequent_errors.each do |error_class, errors|
|
|
total_events = errors.sum { |e| e['events'] || e['events_count'] || 0 }
|
|
output << "• #{error_class}: #{errors.length} экземпляров, #{total_events} событий"
|
|
end
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
|
|
def format_organizations_list(orgs_data)
|
|
orgs = orgs_data.is_a?(Array) ? orgs_data : orgs_data['organizations'] || []
|
|
|
|
output = ["🏢 Доступные организации: #{orgs.length}\n"]
|
|
|
|
orgs.each_with_index do |org, index|
|
|
output << "#{index + 1}. **#{org['name']}** (ID: `#{org['id']}`)"
|
|
output << " Создана: #{org['created_at']}" if org['created_at']
|
|
output << " Коллабораторов: #{org['collaborators_count']}" if org['collaborators_count']
|
|
output << " Проектов: #{org['projects_count']}" if org['projects_count']
|
|
output << " URL: #{org['url']}" if org['url']
|
|
output << ""
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
|
|
def format_projects_list(projects_data)
|
|
projects = projects_data.is_a?(Array) ? projects_data : projects_data['projects'] || []
|
|
|
|
output = ["📦 Доступные проекты: #{projects.length}\n"]
|
|
|
|
projects.each_with_index do |project, index|
|
|
output << "#{index + 1}. **#{project['name']}** (ID: `#{project['id']}`)"
|
|
output << " Тип: #{project['type']}" if project['type']
|
|
output << " Открытых ошибок: #{project['open_error_count']}" if project['open_error_count']
|
|
output << " Коллабораторов: #{project['collaborators_count']}" if project['collaborators_count']
|
|
|
|
if project['release_stages'] && project['release_stages'].any?
|
|
output << " Стадии: #{project['release_stages'].join(', ')}"
|
|
end
|
|
|
|
output << " URL: #{project['url']}" if project['url']
|
|
output << ""
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
|
|
def format_comments_list(comments_data)
|
|
comments = comments_data.is_a?(Array) ? comments_data : comments_data['comments'] || []
|
|
|
|
output = ["💬 Комментарии (#{comments.length}):\n"]
|
|
|
|
if comments.empty?
|
|
output << "Нет комментариев для этой ошибки."
|
|
return output.join("\n")
|
|
end
|
|
|
|
comments.each_with_index do |comment, index|
|
|
output << "**Комментарий #{index + 1}:**"
|
|
output << "• ID: `#{comment['id']}`"
|
|
|
|
# Author info
|
|
author = if comment['user'] && comment['user']['name']
|
|
comment['user']['name']
|
|
elsif comment['user'] && comment['user']['email']
|
|
comment['user']['email']
|
|
else
|
|
'Unknown'
|
|
end
|
|
output << "• Автор: #{author}"
|
|
|
|
output << "• Время: #{comment['created_at']}" if comment['created_at']
|
|
output << "• Текст: #{comment['message']}" if comment['message']
|
|
output << ""
|
|
end
|
|
|
|
output.join("\n")
|
|
end
|
|
end |