Timee Product Team Blog

タイミー開発者ブログ

モバイルアプリ開発におけるメトリクスを改善することで、SLO違反の予兆や改善の傾向を認知しやすくした話

はじめに

はじめまして、Androidエンジニアのmurata(@orerus)です。

アイラ系ウイスキーを愛していますが、肝臓が弱まってきた為最近は専ら0.5%ハイボールを愛飲しています。

本記事では、タイミーのモバイルアプリ開発におけるSLO(サービスレベル目標)を設けているメトリクスのちょっとした改善事例について紹介します。

SLOとは何かといった話やタイミーで運用しているSLOについてはこちらの記事にて詳しく紹介していますので是非ご覧ください!

本記事の概略

タイミーのワーカーチームでは、モバイルアプリ開発における指標の一つであるクラッシュフリーレートをSLI(サービスレベル指標)としてSLOの運用を行っています。

しかし、長く運用する中で、SLO運用に期待されている「当たり前品質と攻めたリリースのバランスを取る」「当たり前品質の低下をいち早く検知する」「適切なタイミングでプロダクト品質への改善圧をかける」といった役割が果たされていないと感じられるケースが度々発生していました。

そこで、モバイルアプリ開発メンバーが普段観測しているクラッシュフリーレートのメトリクスを改善することにより問題を解消した事例について紹介します。

目次

現状と課題

タイミーではSLOを運用しているメトリクスが日に1度slackに投稿されるようになっています。

実際にタイミーで運用している、とある日の改善前のクラッシュフリーレートのメトリクスがこちらです。

f:id:orerus:20220331142322p:plain
表1. 過去30日間、7日間におけるクラッシュフリーレート

  • ① : 過去30日間、7日間におけるクラッシュフリーレート
  • ② : プラットフォーム/アプリバージョン単位でのメトリクス詳細 ※今回は触れません
    • 表の左から
      • プラットフォーム (iOS or Android
      • アプリバージョン
      • クラッシュに遭遇しなかったユーザー数
      • クラッシュに遭遇したユーザー数
      • クラッシュフリーレート

こちらのメトリクスにおいて、ワーカーチームでは以下のSLOを策定していました。

  • iOS : クラッシュフリーレート 99.95% 以上
  • Android OS: クラッシュフリーレート 99.60% 以上

ここで先程のメトリクスを見てみると、Android側はすでに過去30日間におけるクラッシュフリーレートにおいてSLO違反を起こしてしまっており、プロダクト品質への改善圧をかけるべき状態となっていることが分かります。

しかしこの時、Android開発メンバーの間では「直近で品質改善の施策を行ったので既に改善されているはずだ」という認識を持っていました。

このような事例の他、幾つかの事例からこのメトリクスによるSLO運用には以下の課題があることが分かりました。

  • メトリクス単体で指標値の変化の傾向が掴めず、問題発生の検知が遅れ早期の対応が行えない
  • SLO違反を引き起こしている要因をメトリクスから把握することが難しい
  • 品質改善などの施策の結果がメトリクスに反映されるまで時間がかかり、かつ反映されるまでの時間の予測も立ちづらく、施策実施後もSLO違反による改善圧がしばらくかかり続けてしまう

これらの課題を眺めていると、いずれも過去30日間/7日間のクラッシュフリーレートという施策の実施から結果が反映されるまでに時間のかかる指標のみに頼ってSLOを運用している為に発生しているものである、という予測が立ちました。

行った改善策

先述の課題解決の為、以下を目的としたメトリクス改善を行うことにしました。

  • 指標値の変化の傾向をメトリクスから掴めるようになる
  • 指標値が大きく変化したタイミングを的確に把握できる

その結果、追加したメトリクスがこちらです。(こちらのメトリクスは表1と同日に計測したもの)

f:id:orerus:20220331142933p:plain
表2-1. クラッシュフリーレートSMA

f:id:orerus:20220331143010p:plain
表2-2. クラッシュフリーレートSMAグラフのみ

  • ① : プラットフォーム単位での過去60日間におけるメトリクス
    • クラッシュフリー率 : 1日毎のプラットフォーム単位でのクラッシュフリーレート
    • 7日間SMA : 各日付を含む過去7日間の移動平均線(Simple Moving Average)
    • 30日間SMA : 各日付を含む過去30日間の移動平均線
  • ② : プラットフォーム/日付単位でのメトリクス詳細 ※今回は触れません
    • 表の左から
      • 日付
      • クラッシュに遭遇しなかったユーザー数
      • クラッシュに遭遇したユーザー数
      • クラッシュフリーレート
      • 7日間SMA
      • 30日間SMA

※上記メトリクスの算出方法については文末のAPPENDIXにて紹介します

この表2のメトリクス追加により、表1のメトリクスと組み合わせて以下のことが分かります。

  • Android
    • 表1の「30Daysクラッシュフリーレート」においてSLO違反を起こしているが、表2の「7日間SMA」から過去1〜2週の間に改善施策が行われていて改善傾向にある
    • よって今後のメトリクスに置いても「30Daysクラッシュフリーレート」が改善されることが予測される
  • iOS
    • 「30Daysクラッシュフリーレート」がSLO違反スレスレの値ではあるが、こちらも「7日間SMA」「30日間SMA」を見ると上昇傾向にある為、直ちに対策を取る必要性は無さそう

上記の例はポジティブな結果のみのサンプルではありますが、ネガティブなケースでも同様に傾向を把握することができそうです。

結果変わったこと

今回のメトリクス改善を行い、実体験として変化があったと思うものを以下に挙げます。

  1. SLO違反の予兆や改善の傾向が把握できるようになった
  2. 施策の結果を客観的な指標値で速やかに把握することができるようになった
  3. 過去のメトリクスを見返す必要がなくなった
  4. メトリクスに対する心理的変化が発生した

1 〜 3 の結果、当初のSLO運用の目的である「当たり前品質と攻めたリリースのバランス」の判断をより精度高く判断することができるようになった為、この改善はやってよかったと感じています。

また、1 〜 3 の変化については当初の目的通りですが、4については思わぬ変化でした。以前のメトリクスでは点でしか指標値を見ることができず、結果をSLO違反しているかしていないかのゼロイチでしか受け取ることができていませんでした。それが今では線で指標値を見ることができるようになり、プロダクトの当たり前品質の機微な変化を捉えやすく、またメトリクスを施策の結果を速やかにフィードバックしてくれるパートナーとして認識できるようになったため、自然と前向きにメトリクスを確認しに行くことができるようになりました。

最後に

前項にも触れた通り、SLOおよびメトリクスは改善圧をかけてくる敵ではなく、プロダクトの当たり前品質という健康状態を維持する手助けをしてくれるパートナーです。是非味方につけて、プロダクトの体験改善の為の攻めたリリースと当たり前品質の維持を効率よく両立していきましょう!

また、現在はモバイルアプリ開発においては今回取り上げたクラッシュフリーレートというアプリのクラッシュにまつわる指標でしかSLOを運用できていませんが、クラッシュには至らずとも機能が正しく動いていないケース*1、機能は動いているがユーザーが満足に使えていないケース*2など、プロダクトの体験改善の為に注視していくべき観点はたくさんあります。今後も体験改善に繋げることのできる指標を測定可能にし、攻めたリリースを行うためのSLO運用を行っていく所存です。

その為に脳みそを貸してくれる仲間、また美味しいウイスキー銘柄を教えてくれる仲間を絶賛募集中です 😆

APPENDIX

表2の①のグラフを作成する為に使用しているSQLを記載します。

こちらで抽出した結果をGoogle Data Studioを用いてグラフ化しています。

なお、こちらのSQLはFirebase CrashlyticsのデータをBigQueryに転送している環境( 参考URL )であればこのまま利用可能です。

スキーマ名は各自の環境に合わせて修正ください

-- TemporaryTable1. BigQueryからクラッシュ情報を抽出
WITH userCrashes AS(
  SELECT
    event_date,
    user_pseudo_id,
    platform,
    MAX(
      event_name = 'app_exception'
    ) hasCrash,
    MAX(
      event_name = 'app_exception'
    AND (
        select
          value.int_value = 1
        from
          unnest(event_params)
        where
          key = 'fatal'
      )
    ) hasFatalCrash
  FROM
    `analytics_17541xxxx.events_*` -- 各環境のスキーマ名に修正してください
  WHERE
    _TABLE_SUFFIX BETWEEN FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE("Asia/Tokyo"), INTERVAL 60 DAY)) -- 過去60日間
AND FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE("Asia/Tokyo"), INTERVAL 1 DAY))
AND platform = "ANDROID" -- 測定したいプラットフォーム IOS or ANDROID
  GROUP BY
    event_date,
    user_pseudo_id,
    platform
),

-- TemporaryTable2. TemporaryTable1から日付単位でクラッシュ数やクラッシュフリーレートを集計
userCrashesCount AS(
  SELECT
    event_date,
    platform,
    IF(hasCrash, 'crashed', 'crash-free') crashState,
    IF(hasFatalCrash, 'crashed fatal', 'crash-free') fatalCrashState,
    COUNT(DISTINCT user_pseudo_id) AS crashFreeUsers,
    (
      SELECT
        COUNT(DISTINCT user_pseudo_id)
      FROM
        userCrashes AS uc2
      WHERE
        uc2.event_date = userCrashes.event_date
      AND uc2.platform = userCrashes.platform
    ) - COUNT(DISTINCT user_pseudo_id) AS crashUsers,
    ROUND(COUNT(DISTINCT user_pseudo_id) / (
        SELECT
          COUNT(DISTINCT user_pseudo_id)
        FROM
          userCrashes AS uc2
        WHERE
          uc2.event_date = userCrashes.event_date
        AND uc2.platform = userCrashes.platform
      ), 4) AS crashFreeUserShare,
    (
      SELECT
        COUNT(DISTINCT user_pseudo_id)
      FROM
        userCrashes AS uc2
      WHERE
        uc2.event_date = userCrashes.event_date
      AND uc2.platform = userCrashes.platform
    ) AS users
  FROM
    userCrashes -- TemporaryTable1
  WHERE
    hasCrash = false
  GROUP BY
    event_date,
    platform,
    crashState,
    fatalCrashState
  ORDER BY
    event_date
)

-- 抽出結果. Table2から30日間SMA、7日間SMAを算出
SELECT
  platform,           -- プラットフォーム
  event_date,         -- 日付
  crashFreeUsers,     -- クラッシュに遭遇しなかったユーザー数
  crashUsers,         -- クラッシュに遭遇したユーザー数
  crashFreeUserShare, -- クラッシュフリーレート
  CASE
    WHEN 7 = COUNT(crashFreeUserShare) OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
  THEN
    AVG(crashFreeUserShare) OVER (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
  END sevenDaysSMA,   -- 7日間SMA、過去7日分の数値が取れない日付の分は空にする
  CASE
    WHEN 30 = COUNT(crashFreeUserShare) OVER (ROWS BETWEEN 29 PRECEDING AND CURRENT ROW)
  THEN
    AVG(crashFreeUserShare) OVER (ROWS BETWEEN 29 PRECEDING AND CURRENT ROW)
  END thirtyDaysSMA   -- 30日間SMA、過去30日分の数値が取れない日付の分は空にする
FROM
  userCrashesCount    -- TemporaryTable2
ORDER BY
  event_date

なお、上記SQLを構築するにあたり、こちらのStackOverFlowの回答を参考にさせていただきました。

*1:APIの4xxエラー等の監視は別途行っています!

*2:APIの422エラー(バリデーションエラー)の発生件数を監視することで、分かりやすいUI/UXを実現できているかを確認しています