Redashによるアプリ障害最速検知

f:id:atsushi-ohta:20180822042307p:plain

こんにちは。 プロダクトアプリグループの太田です。 今回は実際に起きた障害事例から、いかに最速で検知できるか模索中のお話をします。

障害が起きた

アプリのリリースを行った翌日いつものKPIモニタリングをしている時でした。 前日から若干のサインアップ率の低下が確認できました。 この時は アプリリリース前からダウントレンド で何かしらの外的要因で低下したのではないかとアプリの障害を全く疑っていませんでした。

f:id:atsushi-ohta:20190410171620p:plain

そしてさらに翌日…

f:id:atsushi-ohta:20190410171625p:plain

もっと落ちてるーーー (;゚Д゚) ナイアガラ

何かが起きている… ようやくアプリの障害を疑い調査を始めます。 その結果アプリ側のメールサインアップ時の障害であることがわかり即対応版をリリースしました。

f:id:atsushi-ohta:20190410171635p:plain

回復っ…!泣

AppStoreの審査に提出し通るまで気が気ではありませんでした。

起きていたこと

  • Xcode10移行の際にUniversal Linksをハンドリングするメソッドの仕様変更があったが、その対応が漏れており会員登録アクティベーションが動作しなくなっていた
  • 結果メール内のアクティベーションURLでアプリが開くがサインアップが完了しない
  • たちの悪いことに (不幸中の幸いだけど) SNS経由の登録には問題ないため、すべての経路で登録できないわけではない

対応版リリースまでは一時的にUniversal Linksの設定を外し、メールアクティベーションはWebで完了するよう対策しています。 結果UXとして最悪な状態に… アプリで登録試みたのにWebに着地し、自らアプリに戻ってもログイン状態になっていない。泣 ユーザの皆様にご迷惑をかけてしまいました…

リリース直後に検知したい

この障害検知が遅れた原因は主に以下です。

  • アプリリリース後の最新バージョンの伝搬が緩やかに進むため、障害の発生もゆるやか
  • サインアップできる経路もあり全員ができないわけではないためユーザのお問い合わせもインシデントレベルで上がってこない

本来であれば障害起こすものをリリース前に防ぐのが正しいですが、今回のように万が一リリースしてしまった場合の検知方法を考えたいと思います。

サーバサイドの障害であれば、エラーのアラートが即座に上がりサーバ側で対処できることがほとんどです。クライアント側の障害である今回の場合は通信が発生していない (アプリが起動するだけだったため) ためエラーログも送られていません。 そもそもアプリは通信の発生しない動作が多くあるためエラーログでは検知できない。 各行動イベントに対して問題が起きてないか調べるのはキリがないので、KGIの直結する 重要なKPIをバージョンごとに分析 することで、いつもと違う動きをしている場合はその途中途中の行動に問題が起きていることがわかるのではないか? 問題が起きていることがわかれば、何の問題なのかそのバージョンの更新状態からすぐに障害が特定できるはず!

こうなった

yomoyamablog.coconala.co.jp

みんな大好きfirebaseイベントをRedashで分析します。 各バージョンごとの購入転換率とサインアップ率を障害検知する指標としました。 数を調べるのは最新バージョンの伝搬がすぐには起こらないため率を調べます。

f:id:atsushi-ohta:20190410171643p:plain

時たま跳ねているものは、該当バージョンのシェアが落ちて母数がかなり減っているため率として増減がかなり激しくなります。直近のバージョンの動きを見たいので特に影響はありません。

WITH dau AS (
    SELECT
        e.event_date AS event_date
        , e.app_info.version AS version
        , count(distinct e.user_pseudo_id) AS cnt
    FROM
      `coconala-*******.events_*` AS e
    WHERE
        e.app_info.id = '**********'
        AND e._TABLE_SUFFIX BETWEEN FORMAT_DATE("%Y%m%d", DATE_ADD(CURRENT_DATE("Asia/Tokyo"),INTERVAL -15 day))  AND FORMAT_DATE("%Y%m%d", CURRENT_DATE("Asia/Tokyo"))
    GROUP BY
        event_date
        , version
), event AS (
    SELECT
        e.event_date AS event_date
        , e.event_name AS event_name
        , e.app_info.version AS version
        , count(distinct e.user_pseudo_id) AS cnt
    FROM
      `coconala-*******.events_*` AS e
    WHERE
        e.event_name IN ('first_open', 'finish_loggedin', 'finish_registration', 'view_service', 'ecommerce_purchase')
        AND e.app_info.id = '**********'
        AND e._TABLE_SUFFIX BETWEEN FORMAT_DATE("%Y%m%d", DATE_ADD(CURRENT_DATE("Asia/Tokyo"),INTERVAL -15 day))  AND FORMAT_DATE("%Y%m%d", CURRENT_DATE("Asia/Tokyo"))
    GROUP BY
        event_date
        , event_name
        , version
), sum_event AS (
    SELECT
        event_date
        , version
        , sum(if( event_name = 'first_open',cnt,0)) AS first_open
        , sum(if( event_name = 'finish_loggedin',cnt,0)) AS finish_loggedin
        , sum(if( event_name = 'finish_registration',cnt,0)) AS finish_registration
        , sum(if( event_name = 'view_service',cnt,0)) AS view_service
        , sum(if( event_name = 'ecommerce_purchase',cnt,0)) AS ecommerce_purchase
    FROM
        event
    GROUP BY
        event_date
        , version
), sum_all_event AS (
    SELECT
        e.event_date AS event_date
        , e.version AS version
        , d.cnt AS dau
        , first_open
        , finish_loggedin
        , finish_registration
        , view_service
        , ecommerce_purchase
    FROM
        sum_event AS e
        INNER JOIN
            dau AS d
        ON
            d.event_date = e.event_date
            AND d.version = e.version
    WHERE
        e.version IN ('3.25.0', '3.26.0', '3.27.0', '3.28.0')
)
SELECT
    *
    , SAFE_CAST((CASE
        WHEN dau != 0 THEN finish_loggedin/dau
        ELSE 0 END) as float64) as loggedin_rate
    , SAFE_CAST((CASE
        WHEN first_open != 0 THEN finish_registration/first_open
        ELSE 0 END) as float64) as signup_rate
    , SAFE_CAST((CASE
        WHEN view_service != 0 THEN ecommerce_purchase/view_service
        ELSE 0 END) as float64) as buy_rate
FROM
    sum_all_event
ORDER BY event_date DESC

そしてこれを当時のバージョンで見てみると…

f:id:atsushi-ohta:20190410171648p:plain

明らかに初速から違う…! これでリリース直後に検知が可能そうです!

まとめ

その後モニタリング継続してますが、幸いにも大きな障害は発生していません。障害を発生させない努力をするのが最も大切ですが、発生した場合に検知する仕組みとリカバリ方法を常に考えておくのも重要です。 避難訓練ではないですが、毎回のリリース時に障害が発生した場合どうリカバリするか常に考え続けることで万が一の事態にも対応スピードが上がってくるかと思います。目指せ障害最速対応!(障害0!)