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

# Bugsnag の Data access API でエラーイベント全てを取得する 
この記事は [Goodpatch Advent Calendar 2017 - Qiita](https://qiita.com/advent-calendar/2017/goodpatch) の15日目の記事です。

エラー検知ツールとして [Bugsnag](https://www.bugsnag.com/) を使っています。
エラー別にfilterかけられるし検索性は良い気がするので結構重宝しています。

ある日、とある重大なエラーが発生し、ユーザーのリクエストの一部が処理されていないことがわかりました。
その時にBugsnagに溜まった687件のエラーイベントから、リクエストのパラメータを抜き出して個別に処理した甘酸っぱい思い出を語ります。

## Bugsnag Data access APIとは
[Bugsnag docs › API › Data access](https://docs.bugsnag.com/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](https://bugsnagapiv2.docs.apiary.io/)

Ruby のAPI toolkitがあるみたいだから、Rubyでやろ。
[GitHub - bugsnag/bugsnag-api-ruby: Bugsnag API toolkit for Ruby](https://github.com/bugsnag/bugsnag-api-ruby/)

## Ruby で Data access APIを使う初期設定
[bugsnag-api-ruby/README.md](https://github.com/bugsnag/bugsnag-api-ruby/blob/master/README.md) にほとんど書いてある。

### gem のインストール

gemをインストールする。
pryで動かしたいのでpryも入れとくか。

```Gemfile
gem 'bugsnag-api', '~> 2.0'
gem 'pry'
```
もちろん `gem install bugsnag-api` でも良い。

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

### アクセストークンの取得
そしてアクセストークンを取得しよう。
これは **バグを報告するAPIのキーとは別物**。
Bugsnagにログインして、
`Settings`  > `My account` > `Personal auth tokens`
にて生成する。

![Bugsnag_personal_token.png](https://qiita-image-store.s3.amazonaws.com/0/25926/7df29465-7b99-74e2-e5a3-18700e390c13.png)

`Generate new token +`  > `GENERATE` したらTOKEN_KEYが取れるのでちゃんとコピーしよう。
まぁコピー忘れても再度Token作ろう。

### アクセスしてみる
```ruby 
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が取れた。うふふ。

## 該当のエラーを取得する
まずはエラーの取得まで行ってみよう。

```ruby
# 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で取得する必要があった。

```ruby
# List organization projects
projects = Bugsnag::Api.projects("organization-id")
```

これを使ってprojectsの一覧を取得すれば良いのだね。
organizationはさっき取れたからこれでいける

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

よしよし、projectの一覧が取れたよ。
そしたら名前かpathからidを特定しよう。
さっき勘違いしたpath名は`project#slug`のことみたいだ。

```ruby
project_id = projects.select{|p| p.slug == 'projectのURLの:projectの部分'}&.first&.id
```
```ruby
# 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から特定してみよう。

```ruby
# List project errors
errors = Bugsnag::Api.errors("project-id")
```

これを使おう。
 
```ruby
pry(main)> errors.count
=> 30
```
あれあれ、これで全部かな・・・。headerも見れるみたいだから見てみよう。

```ruby
last_response = Bugsnag::Api.last_response
last_response.headers
```
`last_response.headers['x-total-count']` で全件数がわかる。
絞ってないから全部のエラーだともっと数は多い
どうやらこのAPIには30件の制限がある模様。

[READMEの #pagenation](https://github.com/bugsnag/bugsnag-api-ruby/#pagination) によると

```ruby
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](https://bugsnagapiv2.docs.apiary.io/#reference/errors/errors/view-an-error)
`context` と `error_class`  で絞れそうですね。でも`filter`がrubyからは叩けないっぽいので、全部とった結果から特定するか。

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

まぁ管理画面から取った方が楽だったね。
これでやっと該当の `error-id` が取れたよ。

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

ここからがやっと本題。
エラーの全イベントからパラメーターを抽出すべし。

```ruby
# List error events
events = Bugsnag::Api.error_events("project-id", "error-id")
```

これじゃな。

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

```ruby
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を調べるとそれらしいものを発見。

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

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

```ruby
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秒に１0アクセスまでなら概ね問題なさそう。
なので、最終的にこんな感じで取得することにしました。

```ruby
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
    retry
  end
end
```

そんなに待たなくても良いかもしれないけど、適宜調整が必要かも。
そしてちょっとコードがダサい。。。
まぁでもこれでやっと全イベントが取れたよ！

```ruby
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](https://bugsnagapiv2.docs.apiary.io/#reference/errors/events/list-the-events-on-an-error)

```ruby
# Get a single event
event = Bugsnag::Api.event("project-id", "event-id")
```
これで愚直に取るかぁー

```ruby
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
    retry
  end
end
```
めちゃめちゃ時間かかったけど、取れたよ・・・。

### イベント詳細を解析して、パラメータを取得
詳細まで行けば結構属性取れるっぽい。

```ruby
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]
```
あとはこんな感じでごにょごにょすれば、、

```ruby
event_details.each do |detail|
  puts detail.request.params
  # 実際はここのパラメータをCSVに吐いたりした。
end
```
できたー

---

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

## まとめ
こうしてなんとかBugsnagからパラメータ取得し、平和は保たれましたが、意外と骨が折れました。
今回利用したRubyのtoolkitがまだまだイケてないというのもあったかもしれませんが、直接叩きにいった方がfilterなど使えてよかったのかもしれません。

それかむしろPRのチャンスですかね。
冬休みの宿題にします :hugging: 

### 追記

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