Railsアプリのエラー監視にSentryを使っています。依存gemのメジャーアップデートの一環でsentry-rubyを5系から6系に上げたのですが、「エラー通知が来ないのは平和な証拠」と思い込みそうになる、なかなか気づきにくい落とし穴がありました。
before_sendで個人情報などをフィルタしている実装が、6系にした途端、すべてのエラーを送信しなくなる状態になりうる、という内容です。アプリ側にはエラーも警告も出ないので、同じ構成の方の参考になればと思い、まとめてみました。
状況:before_sendでパラメータをフィルタしていた
パスワードやトークンなどがSentryに載ってしまわないよう、before_sendでイベントの中身をフィルタする設定を入れていました。sentry-raven時代から引き継がれがちな、次のような書き方です。
Sentry.init do |config|
filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
config.before_send = lambda do |event, hint|
filter.filter(event.to_h) # ← Hashを返している
end
end
5系では、これで問題なく動いていました。before_sendの戻り値がnilのときだけイベントが破棄され、Hashを返してもそのまま送信されていたためです。
何が起きるか:Hashを返すとイベントごと捨てられる
6系では挙動が変わっています。before_sendの戻り値がSentry::ErrorEventでなければイベントは破棄されます。上の実装はParameterFilter#filterがHashを返すので、アップグレード後は全エラーが破棄されることになります。
この点はsentry-rubyの6.0.0のリリースノートでも、before_sendとbefore_send_transactionがHashを返すことはサポートされなくなり、返すとイベントが破棄される、という破壊的変更として書かれています。ただ、リリースノートを丁寧に読む習慣がないと、まず気づきません。
厄介なのは、アプリ側ではほとんど何も起きない点です。例外は出ませんし、ログもdebugレベルを除いて何も出ません。SentryのStatsを見ると「Client Discard」として数字が増えているだけで、プロジェクトのIssue一覧は静かなままです。つまり、エラー監視が止まっていること自体が、エラーとして通知されません。
対処:属性を書き換えてeventを返す
推奨される直し方は、Hashを返すのをやめて、eventの属性を書き換えたうえでevent自体を返すことです。
config.before_send = lambda do |event, hint|
event.extra = filter.filter(event.extra)
event
end
フィルタしたい属性(extraやリクエストのデータなど)を個別に書き換えていく形になります。
5系と同じ範囲で全体をフィルタしたい場合
event.to_h全体をフィルタする挙動を、そのまま維持したいケースもあると思います。私が調べた限りでは、6.4.1のTransport#envelope_from_eventが送信時にevent.to_hを使っていたので、to_hの戻り値を差し替える次のような書き方でも動きました。
config.before_send = lambda do |event, hint|
filtered = filter.filter(event.to_h)
event.define_singleton_method(:to_h) { filtered }
event
end
ただし、これはSDKの内部実装に依存した書き方です。バージョンアップで送信経路が変われば、また黙って効かなくなる可能性があります。使う場合は、後述のとおり実際の送信経路を通すテストで守っておくのが安全だと思います。
テストで守る:実際の送信経路まで通す
before_sendの単体テストだけだと、「eventを返しているか」しか確認できません。上のような内部依存の書き方は特に、送信されるエンベロープの中身まで確認しておきたいところです。
Sentry::DummyTransportを使うと、本番と同じbefore_sendを設定した状態で、送信されるはずのデータを後から検査できます。
config = Sentry::Configuration.new
config.dsn = 'https://public@example.com/1'
config.transport.transport_class = Sentry::DummyTransport
config.before_send = production_before_send # 本番と同じ設定を渡す
client = Sentry::Client.new(config)
event = client.event_from_message('test')
client.send_event(event)
expect(client.transport.envelopes).not_to be_empty # 破棄されていないこと
# envelopes の中身に、フィルタ対象の値が残っていないことも検査する
ポイントは、「送信されること」と「フィルタされていること」の両方を確認することです。前者だけだと内容がフィルタされていなくても通りますし、後者だけだと破棄されていても「何も残っていない」ので通ってしまいます。
教訓:SDKのメジャーアップ後は受け入れ数を見る
今回の学びは、監視系のSDKをメジャーアップグレードしたら、監視の出力そのものが生きているかを確認する、ということです。
- SentryのStatsで、Accepted(受理)の数が極端に減っていないか、Client Discardが急に増えていないかを見る
- わざとエラーを1件起こして、Issueとして届くことを確認する
-
before_sendのようなフックは、戻り値の型が変わる可能性があるので、リリースノートの破壊的変更を確認する
エラーが来ない状態は、本当に平穏なのか、監視が止まっているのかが外から見分けにくいです。アップグレード直後に一度だけ確認しておくだけで、気づくまでの時間が大きく変わると思います。
まとめ
sentry-rubyを6系に上げると、before_sendがHashを返す実装はイベントごと破棄されるようになります。原因はEventオブジェクトを返していないことで、アプリ側には何も出力されず、SentryのStatsでClient Discardとして見えるだけでした。属性を書き換えてeventを返す形に直し、DummyTransportで実際の送信経路を検査するテストを入れておくと安心です。
「通知が来ない」ことを安心材料にしてしまいがちな領域だからこそ、メジャーアップ後は受け入れ数の確認を習慣にしたいと思いました。同じような状況の方の参考になれば嬉しいです。