Edited at
GoodpatchDay 15

Bugsnag の Data access API でエラーイベントを取得する

More than 1 year has passed since last update.


Bugsnag の Data access API でエラーイベント全てを取得する

この記事は Goodpatch Advent Calendar 2017 - Qiita の15日目の記事です。

エラー検知ツールとして Bugsnag を使っています。

エラー別にfilterかけられるし検索性は良い気がするので結構重宝しています。

ある日、とある重大なエラーが発生し、ユーザーのリクエストの一部が処理されていないことがわかりました。

その時にBugsnagに溜まった687件のエラーイベントから、リクエストのパラメータを抜き出して個別に処理した甘酸っぱい思い出を語ります。


Bugsnag Data access APIとは

Bugsnag docs › API › Data access


Access information about your organization, projects, errors, and more to build custom integrations


要はBugsnagに登録されたエラーなどのデータにアクセスできるみたい。

V1はDeprecatedになっているのでV2で行こうぜ。

V2 のリファレンスは Bugsnag Data Access API · Apiary

Ruby のAPI toolkitがあるみたいだから、Rubyでやろ。

GitHub - bugsnag/bugsnag-api-ruby: Bugsnag API toolkit for Ruby


Ruby で Data access APIを使う初期設定

bugsnag-api-ruby/README.md にほとんど書いてある。


gem のインストール

gemをインストールする。

pryで動かしたいのでpryも入れとくか。

gem 'bugsnag-api', '~> 2.0'

gem 'pry'

もちろん gem install bugsnag-api でも良い。

Gemfileにした場合はもちろんbundle install しよう。


アクセストークンの取得

そしてアクセストークンを取得しよう。

これは バグを報告するAPIのキーとは別物

Bugsnagにログインして、

Settings > My account > Personal auth tokens

にて生成する。

Generate new token + > GENERATE したらTOKEN_KEYが取れるのでちゃんとコピーしよう。

まぁコピー忘れても再度Token作ろう。


アクセスしてみる

require 'bugsnag/api'

# Provide authentication credentials
Bugsnag::Api.configure do |config|
config.auth_token = 'ここにTOKEN_KEYを入れまっしょ。'
end

# Access API methods
organizations = Bugsnag::Api.organizations

これで自分の所属するorganizationが取れた。うふふ。


該当のエラーを取得する

まずはエラーの取得まで行ってみよう。

# Get a single error

error = Bugsnag::Api.error("project-id", "error-id")

そのためにproject-idとerror-idの特定が必要みたい。


project-idの取得

ここが最初の引っ掛かりポイント、

project-id管理画面とかのアクセスに使うURLに含まれてるものではなかった!

https://app.bugsnag.com/:organization/:project/errors

みたいなURLの :project の部分だと思ってたよ。

project-id はAPIで取得する必要があった。

# List organization projects

projects = Bugsnag::Api.projects("organization-id")

これを使ってprojectsの一覧を取得すれば良いのだね。

organizationはさっき取れたからこれでいける

organizations = Bugsnag::Api.organizations

organization_id = organizations.first.id
projects = Bugsnag::Api.projects(organization_id)

よしよし、projectの一覧が取れたよ。

そしたら名前かpathからidを特定しよう。

さっき勘違いしたpath名はproject#slugのことみたいだ。

project_id = projects.select{|p| p.slug == 'projectのURLの:projectの部分'}&.first&.id

# Hashにして確認するなら

pry(main)> projects.map{|p| {id: p.id, name: p.name, slug: p.slug}}
=> [{:id=>"123456", :name=>"project A", :slug=>"project_a"},
{:id=>"654321", :name=>"CHOTO NEMUI", :slug=>"choto_nemui"},
{:id=>"135924", :name=>"Target Project", :slug=>"projectのURLの:projectの部分"}]

うひょひょ。取れたよー。


error-idの取得

こちらは 管理画面のアクセスに使う

https://app.bugsnag.com/:organization/:project/errors/:error_id

のURLの一部をそのまま使えば良い。

でもせっかくだからAPIから特定してみよう。

# List project errors

errors = Bugsnag::Api.errors("project-id")

これを使おう。

pry(main)> errors.count

=> 30

あれあれ、これで全部かな・・・。headerも見れるみたいだから見てみよう。

last_response = Bugsnag::Api.last_response

last_response.headers

last_response.headers['x-total-count'] で全件数がわかる。

絞ってないから全部のエラーだともっと数は多い

どうやらこのAPIには30件の制限がある模様。

READMEの #pagenation によると

errors = Bugsnag::Api.errors("project-id", per_page: 100)

last_response = Bugsnag::Api.last_response
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
errors.concat last_response.data
end

こうしてconcatしなされと。

やってみたら全部取れたけど数回アクセスしてたから per_page は効いてない模様・・・。

error のレスポンスには色々入ってる。 see: Bugsnag Data Access API · Apiary / View an error

contexterror_class で絞れそうですね。でもfilterがrubyからは叩けないっぽいので、全部とった結果から特定するか。

error_id = errors.select{|e| e.context == ':context' && e.error_class == ':error_class'}&.first&.id

まぁ管理画面から取った方が楽だったね。

これでやっと該当の error-id が取れたよ。


エラーからイベントを全て取得しパラメータのリストを作る

ここからがやっと本題。

エラーの全イベントからパラメーターを抽出すべし。

# List error events

events = Bugsnag::Api.error_events("project-id", "error-id")

これじゃな。

全件取得したいのでpagenate処理もしておく。

events = Bugsnag::Api.error_events(project_id, error_id)

last_response = Bugsnag::Api.last_response
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
events.concat last_response.data
end

すると、

フハハ!エラーになったよ。

Bugsnag::Api::RateLimitExceeded: GET https://api.bugsnag.com/projects/135246/errors/123xyz/events?(略): 429 -

まぁそうですよね。普通RateLimitかけるよね。


RateLimit対策

ただ、 DocumentにRateLimitの話が一切ない。

headerを調べるとそれらしいものを発見。

{

"x-ratelimit-limit"=>"10",
"x-ratelimit-remaining"=>"0",
}

そこで何秒でlimitが解除されるかを確認

until last_response.rels[:next].nil?

begin
puts "remaining:#{last_response.headers['x-ratelimit-remaining']}"
last_response = last_response.rels[:next].get
puts 'success'
sleep_count = 1
events.concat last_response.data
rescue
puts Time.now
puts "sleep_count:#{sleep_count}"
sleep sleep_count
sleep_count += 1
end
end

と、こんな感じで待ち時間を確認して見た。

そして、少し調整したけど、だいたい1分間に10アクセスくらいっぽい。

つまり60秒に10アクセスまでなら概ね問題なさそう。

なので、最終的にこんな感じで取得することにしました。

events = Bugsnag::Api.error_events(project_id, error_id)

last_response = Bugsnag::Api.last_response
wait_a_minite = true
until last_response.rels[:next].nil?
begin
last_response = last_response.rels[:next].get
events.concat last_response.data
wait_a_minite = true unless wait_a_minite
rescue Bugsnag::Api::RateLimitExceeded
if wait_a_minite
puts 'RateLimitExceeded sleep:1 minute'
sleep 60
wait_a_minite = false
else
puts 'RateLimitExceeded sleep:1 sec'
sleep 1
end
end
end

そんなに待たなくても良いかもしれないけど、適宜調整が必要かも。

そしてちょっとコードがダサい。。。

まぁでもこれでやっと全イベントが取れたよ!

pry(main)> last_response.headers['x-total-count']

=> "687"
pry(main)> events.count
=> 687


イベントの詳細を取得する

しかし、このAPIで返ってくるeventはis_full_report==false なので、リクエストのパラメータまでは取れなかった・・・

see: Bugsnag Data Access API · Apiary / List the Events on an Error

# Get a single event

event = Bugsnag::Api.event("project-id", "event-id")

これで愚直に取るかぁー

event_details = []

events.each do |event|
begin
detail = Bugsnag::Api.event(project_id, event.id)
event_details << detail
wait_a_minite = true unless wait_a_minite
rescue Bugsnag::Api::RateLimitExceeded
if wait_a_minite
puts 'RateLimitExceeded sleep:1 minute'
sleep 60
wait_a_minite = false
else
puts 'RateLimitExceeded sleep:1 sec'
sleep 1
end
end
end

めちゃめちゃ時間かかったけど、取れたよ・・・。


イベント詳細を解析して、パラメータを取得

詳細まで行けば結構属性取れるっぽい。

pry(main)> event_details.first.instance_variable_get(:@attrs).keys

=> [:id, :url, :project_url, :is_full_report, :error_id, :received_at, :exceptions, :threads, :metaData, :customDiagnostics, :request, :app, :device, :user, :breadcrumbs, :context, :severity, :unhandled, :derived]

あとはこんな感じでごにょごにょすれば、、

event_details.each do |detail|

puts detail.request.params
# 実際はここのパラメータをCSVに吐いたりした。
end

できたー


こうして取得したCSVのパラメータを元に、失敗したデータの再処理を行うことができました。

めでたしめでたし。


まとめ

こうしてなんとかBugsnagからパラメータ取得し、平和は保たれましたが、意外と骨が折れました。

今回利用したRubyのtoolkitがまだまだイケてないというのもあったかもしれませんが、直接叩きにいった方がfilterなど使えてよかったのかもしれません。

それかむしろPRのチャンスですかね。

冬休みの宿題にします :hugging:


追記

体調を崩して15日に投稿できませんでした。すみません :bow: