Webアプリケーションのクラッシュレポートを自動送信する + バグトラッカー(FogBugz)へ保存する

Webアプリケーションのクラッシュレポートを作成し、バグトラッカーへ登録する仕組みを構築した。

クラッシュレポートとは、Webアプリケーションに限らず、ソフトウェアが例外やクラッシュで停止してしまった時に、クラッシュ情報(バックトレースや、ユーザー環境情報、停止したソースコードの位置)を集めて、開発者へ通知するしくみである。信頼性の高いソフトウェアを作るために、クラッシュレポートがどれほど役立つかの考察は、敬愛する Joel Spolsky の 「ユーザからクラッシュレポートを自動的に取得する方法」を読んでほしいと思う。

Joel on Software

Joel on Software

Ruby on Rails 3.x でクラッシュ情報を収集するには?

Rails::Application の config.exceptions_app を利用する。

初期状態では config.exceptions_app は未指定となっているそうで、下記のように処理を追加することで、例外を補足することができる。

  config.exceptions_app = lambda do |env|
    case env["action_dispatch.exception"]
      # ...
    end
  end

私は、下記のクラッシュ情報を収集するようにしている。

情報 詳細 格納されている場所
エラー内容   env['action_dispatch.exception']
ビルドバージョン クラッシュが発生したアプリケーションのバージョン  
バックトレース クラッシュした箇所のソースコード上の位置と、コールスタック env['action_dispatch.exception'].backtrace
HTTP リクエストメソッド (例)GET, POST, DELETE ... env['REQUEST_METHOD']
HTTP リクエスURI   env['REQUEST_URI']
HTTP リクエストパラメータ   env['action_dispatch.request.parameters']
HTTP リクエストフォーマット   env['action_dispatch.request.formats']
Webサーバーの種類 (例) Apache/2.2.x env['SERVER_SOFTWARE']
ユーザー情報   env['HTTP_USER_AGENT']
エラー発生時のセッション   env['rack.session']




収集した情報を、バグトラッカー(FogBugz)へ登録する方法

FogBugz

バグ管理 + プロジェクト管理 + カスタマーサービスの機能を備えた、有償の Web アプリケーション。1ユーザー月額$25。しかし45日間の無償試用期間があり、すばらしいのは Students and Startups Edition(学生、及び2ユーザーまでのスタートアップビジネス)ならばすべての機能が使えて、ずっとタダ!
しかし残念なことに、日本語にローカライズされていないためか、国内の利用例をあまり聞かない。

今回は、FogBugz にクラッシュレポートを自動登録する方法を紹介する。

FogBugz へのチケット登録は、FogBugz XML API を利用する。

XML API を扱うための Python ライブラリは標準で提供されているようだが、今回は Rails なので XML API を直接操作することにする。

XML APIRubyで扱えるようにした gem もいくつか公開されている。

この中の「ruby-fogbugz」というのは実際に試してみたところ、結構使いやすかった。しかし今回は、そこまで複雑な操作はしないので、直接 XML API を使って処理を実装した。



 

クラッシュ情報を収集して、FogBugz へ送信するソースコード

ソースを見てもらったほうが早いので、全文を掲載する。私はとりあえず config/application.rb にベタ書きしている。

# ...

# 定数
FOGBUGZ_DOMAIN   = "example.fogbugz.com"
FOGBUGZ_USER     = "komiyak@example.com"
FOGBUGZ_PASSWORD = "**** ****"

FOGBUGZ_PROJECT_ID     = 3 # バグを登録するプロジェクトの FogBugz 上のID (ixProject)
FOGBUGZ_ASSING_USER_ID = 3 # 新規バグがアサインされる仮想ユーザー(new bugs user みたいな感じ)

module Example
  class Application < Rails::Application

    # ...

    # Production 環境で発生したエラーを検知
    # FogBugz(バグトラッカー)へ、エラー情報を送信する
    config.exceptions_app = lambda do |env|
    
      # バグの重複登録を避けるために、バグのキーに対してほぼ一意な識別値をあたえる
      # この識別値は検索にも利用する
      bug_key = env['action_dispatch.exception'].to_s + ':' + env['action_dispatch.exception'].backtrace.first + ':'  + config.build_version
      bug_key_hash_full = Digest::SHA1.hexdigest(bug_key) # ほぼ一意な識別値
      bug_key_hash_10   = bug_key_hash_full[0, 10]        # 先頭の 10 文字のみ抽出
      
      # タイトル: ユニークキー+ビルドバージョン+例外の内容
      unsafe_bug_title = "[#{bug_key_hash_10}...] #{config.build_version}:#{env['action_dispatch.exception'].to_s}"
      # 本文
      unsafe_bug_event = ""
      
      # バグの内容を生成
      unsafe_bug_event << "EXCEPTION:\r\n  #{env['action_dispatch.exception']} \r\n"
      unsafe_bug_event << "EXCEPTION HASH:\r\n  #{bug_key_hash_full} \r\n"
      unsafe_bug_event << "BUILD_VERSION:\r\n  #{config.build_version} \r\n"
      unsafe_bug_event << "REQUEST_METHOD:\r\n  #{env['REQUEST_METHOD']} \r\n"
      unsafe_bug_event << "REQUEST_URI:\r\n  #{env['REQUEST_URI']} \r\n"
      unsafe_bug_event << "SERVER_SOFTWARE:\r\n  #{env['SERVER_SOFTWARE']} \r\n"
      unsafe_bug_event << "HTTP_USER_AGENT:\r\n  #{env['HTTP_USER_AGENT']} \r\n"
      unsafe_bug_event << "SESSION:\r\n  #{env['rack.session']} \r\n"
      unsafe_bug_event << "REQUEST PARAMETERS:\r\n  #{env['action_dispatch.request.parameters']} \r\n"
      unsafe_bug_event << "REQUEST FORMATS:\r\n  #{env['action_dispatch.request.formats']} \r\n"

      unsafe_bug_event << "\r\n \r\nBACK TRACE (depth #{env['action_dispatch.exception'].backtrace.count}) \r\n \r\n"

      env['action_dispatch.exception'].backtrace.each_with_index do |trace, i|
        #
        # FogBugz API からのチケット登録には、本文のサイズに内部的な制約があるらしい。
        #
        # 大きな文字列を登録しようとしたら、「404 Not Found」がリターンされてしまった。
        # すべてのバックトレースをもたせると、上記の問題がおきてしまうので、
        # チケットに登録するのは、一部のバックトレースだけにする。
        #
        # TODO: 将来的には全てのバックトレースを送付したい。
        #       バックトレースデータをファイルにして、それを添付ファイルとして POST 
        #       するしか方法は無さそう。
        #
        if i >= 3
          unsafe_bug_event << "  ... \r\n"
          break
        end
        unsafe_bug_event << "  #{trace} \r\n"
      end
      
      # FogBugz API に接続
      https = Net::HTTP.new(FOGBUGZ_DOMAIN, 443)
      https.use_ssl = true
      https.verify_mode = OpenSSL::SSL::VERIFY_PEER
      https.verify_depth = 5
      https.start {

        # Login
        response = https.get("/api.asp?cmd=logon&email=#{FOGBUGZ_USER}&password=#{FOGBUGZ_PASSWORD}")
        doc = REXML::Document.new( response.body )
  
        # FogBugz onetime token
        onetime_token = doc.elements['response/token'].text
  
        # Search
        response = https.get(
          "/api.asp?token=#{onetime_token}&cmd=search&cols=sTitle&max=1&q=#{bug_key_hash_full}"
        )
        doc = REXML::Document.new( response.body )
        
        
        # 送信データを URLエスケープ しておく
        bug_title = URI.escape(unsafe_bug_title)
        bug_event = URI.escape(unsafe_bug_event)
        

        # Duplication check
        if doc.elements['response/cases'].count == 0
        
          # 該当なし → New Case
          response = https.get(
            "/api.asp?token=#{onetime_token}&cmd=new&sTitle=#{bug_title}&sEvent=#{bug_event}&ixProject=#{FOGBUGZ_PROJECT_ID}&ixPersonAssignedTo=#{FOGBUGZ_ASSING_USER_ID}"
          )
        else
        
          # すでに同じ内容のチケットが登録されているようだったら、そのチケットに対して追記する形をとる
          # 重複したチケットが、いくつもいくつも増えたら嫌なので。
          ix_bug = doc.elements['response/cases/case'].attributes["ixBug"].to_i
          response = https.get( "/api.asp?token=#{onetime_token}&cmd=edit&&ixBug=#{ix_bug}&ixProject=#{FOGBUGZ_PROJECT_ID}&sEvent=#{bug_event}" )
        end
  
        # Logoff
        response = https.get("/api.asp?cmd=logoff&token=#{onetime_token}")
      }
      
      render file: 'public/500.html'
    end


上記のソースは development 環境では動作しない(config.exceptions_app が、development では動作しないため)。 production 環境で、例外やクラッシュが発生すると、上記の処理を通過し、クラッシュ情報を FogBugz へ送信してくれる。

FogBugz 側を見てみよう。



ポイント
  • 予め、FogBugz 上に仮想ユーザー(例えば、New Bugs)を作成しておき、新しいバグはそのユーザーに割り当てる
  • バグを修正するときは、仮想ユーザーからチケットを受け取ってから作業する
  • 似たようなバグが発生した場合は、それに該当するケースを探してきて、それに追記する形で登録を行う

まだまだ、運用実績が乏しいので、今後修正をどんどん加えていくと思う。その時は、この記事に情報を追記していく予定だ。今後の課題としては、このままではユーザーに内緒で、クラッシュレポートを送信してしまうことになるので、クラッシュ時に簡単なフォームを表示して、ユーザーがクラッシュレポートを送信するか、しないのかを選べるようにしたいと思っている。