良い感じにログを収集するライブラリ、Puree-Swiftをリリースしました

こんにちは。技術部モバイル基盤グループの三木(@)です。

クックパッドでは、Pureeと呼ばれるiOS/Android/ReactNative向けのログ収集ライブラリを公開しています。

モバイルアプリのログ収集ライブラリ「Puree」をリリースしました - クックパッド開発者ブログ

ログ収集ライブラリ Puree の iOS 版をリリースしました - クックパッド開発者ブログ

最近、以前開発されていたPureeをpure Swiftで書き換え、OSSとして公開しました。

この記事では、新しくなったPureeをご紹介します。

概要

クックパッドでは全社的にAmazon Redshiftを中心としたデータ活用基盤を構築しています。

クックパッドのデータ活用基盤 - クックパッド開発者ブログ

この仕組みを使い、公開している多くのモバイルアプリからも、1つのログ基盤にさまざまなログを集積させています。

しかし、モバイルアプリからのログ送信には、さまざまな状態を考慮する必要があります。 ログを送りたいタイミングに安定した通信が確保されているとは限らないですし、闇雲に送りすぎてしまうと、ユーザーさんのギガを圧迫してしまうかもしれません。

これらを解決するライブラリがPureeです。 ログをバッファリングし、まとめて送信したり、送信に失敗したログをキャッシュし、復元時にリトライする機能などを有しています。

f:id:gigi-net:20180227165235p:plain

Puree-Swiftの特徴

Puree-Swiftは、以前公開していたObjective-C版と異なり、以下のような特徴があります。

Objective-C版の設計思想を踏襲

Puree-SwiftはObjective-C版のPureeの置き換えを目指しています。そのため、タグシステムやプラグインの設計など、基本的な仕組みを踏襲しています。 詳しく知りたい方は以下の記事をご覧ください。

ログ収集ライブラリ Puree の iOS 版をリリースしました - クックパッド開発者ブログ

よりSwiftらしいインターフェイス

Objective-Cで書かれていた物をSwiftに刷新したため、よりSwiftから利用しやすいインターフェイスとなりました。

大きく変わったのはFilterOutputの実装方法で、以前は抽象クラスとして実装していたのですが、protocolを利用することができるようになり、よりSwiftらしいプロトコル指向な設計に生まれ変わりました。

依存関係の廃止

Objective-C版のPureeでは、未送信のログの永続化のため、YapDatabaseというSQLiteにアクセスするライブラリを利用していました。 しかしこのライブラリは最近メンテナンスが止まっていたり、Swiftで書かれていなかったりと、Pureeのメンテナンスを難しくする原因となっていました。 そのため、Puree-Swiftでは一切の依存関係を廃止して、iOS標準のファイルストレージを使うようにしています。

通常はこの利用方法で問題ありませんが、巨大なデータを扱いたい需要が出たときのために、LogStoreを自分でプラグインとして拡張できる設計になりました。 必要に応じてRealmやCoreDataなど、使いたいバックエンドを採用することができます。

実装例

それではさっそくPureeの実装例を見てみましょう。最終的には、以下のようなインターフェイスで任意の場所にログを送れるようになります。

ここでは、以下のようにPVログを送るまでの実装を考えてみます。

logger.postLog(["recipe_id": 42, "user_id": 100], tag: "pv.recipe.detail")

Pureeを扱うには以下の3ステップが必要です。

  1. ログを加工するFilterを実装する
  2. 収集されたログを外部に出力するOutputを実装する
  3. タグにより、どのFilterやOutputを利用するかルーティングする

より詳しい使い方はREADMEをご覧ください。

1. ログを加工するFilterを実装する

まず、Filterプロトコルを用いて、Filterを実装します。これは渡ってきた任意のデータをLogEntryに加工する役目を持っています。

ここでは単純に渡ってきたペイロードをJSONとしてエンコードして、LogEntryに格納しています。

import Foundation
import Puree

struct PVLogFilter: Filter {
    let tagPattern: TagPattern

    init(tagPattern: TagPattern, options: FilterOptions?) {
        self.tagPattern = tagPattern
    }

    func convertToLogs(_ payload: [String: Any]?, tag: String, captured: String?, logger: Logger) -> Set<LogEntry> {
        let currentDate = logger.currentDate

        let userData: Data?
        if let payload = payload {
            userData = try! JSONSerialization.data(withJSONObject: payload)
        } else {
            userData = nil
        }
        let log = LogEntry(tag: tag,
                           date: currentDate,
                           userData: userData)
        return [log]
    }
}

このFilter上で、全てのログに共通して付加したいペイロードを載せることもできます。 例えば、ユーザー情報などが考えられます。

2. 収集されたログを外部に出力するOutputを実装する

次に、収集されたログを外部に出力するためにOutputを実装します。

以下は渡ってきたLogEntryのペイロードを標準出力に出力するだけのOutputです。

class ConsoleOutput: Output {
    let tagPattern: String

    init(logStore: LogStore, tagPattern: String, options: OutputOptions?) {
        self.tagPattern = tagPattern
    }

    func emit(log: Log) {
        if let userData = log.userData {
            let jsonObject = try! JSONSerialization.jsonObject(with: log.userData)
            print(jsonObject)
        }
    }
}

BufferedOutput

Outputを用いると、ログが送信され、即座に出力されますが、代わりにBufferedOutputを用いると、一定数のログが溜まるまでバッファリングし、定期的にログを送ることができます。 以下のようにAPIリクエストを伴うようなログ送信に適しています。

class LogServerOutput: BufferedOutput {
    override func write(_ chunk: BufferedOutput.Chunk, completion: @escaping (Bool) -> Void) {
        let payload = chunk.logs.flatMap { log in
            if let userData = log.userData {
                return try? JSONSerialization.jsonObject(with: userData, options: [])
            }
            return nil
        }
        if let data = try? JSONSerialization.data(withJSONObject: payload, options: []) {
            let task = URLSession.shared.uploadTask(with: request, from: data)
            task.resume()
        }
    }
}

クックパッドでは、最初に紹介したログ基盤を利用するための、APIを提供しており、社内ライブラリとして、そのAPIに送信を行うOutputを提供しています。

このように、自前で用意したあらゆるログ基盤に出力することができますし、Firebase AnalyticsなどのmBaaSに対応することもできるでしょう。

3. タグにより、どのFilterやOutputを利用するかルーティングする

最後に、実装したFilterやOutputをどのログに対して適応するかのルーティングを定義しましょう。 Pureeは、ログに付加されたタグを元に、どのような処理を行うかを決定します。

import Puree

let configuration = Logger.Configuration(filterSettings: [
                                             FilterSetting(PVLogFilter.self,
                                                           tagPattern: TagPattern(string: "pv.**")!),
                                         ],
                                         outputSettings: [
                                             OutputSetting(ConsoleOutput.self,
                                                           tagPattern: TagPattern(string: "activity.**")!),
                                             OutputSetting(ConsoleOutput.self,
                                                           tagPattern: TagPattern(string: "pv.**")!),
                                             OutputSetting(LogServerOutput.self,
                                                           tagPattern: TagPattern(string: "pv.**")!),
                                         ])
let logger = try! Logger(configuration: configuration)
logger.postLog(["page_name": "top", "user_id": 100], tag: "pv.top")

例えば、上記のような定義ですと、それぞれのタグについて、以下のように処理が行われます。 これにより、ログの種類によって加工方法や出力先を変えることもできます。

tag name -> [ Filter Plugin ] -> [ Output Plugin ]
pv.recipe.list -> [ PVLogFilter ] -> [ ConsoleOutput ], [ LogServerOutput ]
pv.recipe.detail -> [ PVLogFilter ] -> [ ConsoleOutput ], [ LogServerOutput ]
activity.recipe.tap -> ( no filter ) -> [ ConsoleOutput ]
event.special -> ( no filter ) -> ( no output )

まとめ

  • iOSのログ収集ライブラリ、Puree-Swiftをリリースしました
  • すでにクックパッドアプリでは使われており、開発中の他のアプリでも利用される予定です
  • Outputを追加すれば、さまざまなログバックエンドに対応することができます

どうぞご利用ください。

try!Swift

f:id:gigi-net:20180227165258j:plain

ところで、明日3/1から開催のtry! Swift Tokyo 2018にクックパッドもブースを出展いたします。 オリジナルグッズの配布も行いますので、クックパッドでのiOS開発に興味のある方は是非遊びに来てください。

私もスピーカーとして登壇します。当日お会いしましたらよろしくおねがいします。

TOKYO - try! Swift Conference

高速に仮説を検証するために ~A/Bテスト実践~

会員事業部エンジニアの佐藤です。クックパッドでは日々データと向き合い、データを基にした施策作りに関わっています。

Cookpad TechConf 2018で新井が発表した「クックパッドの "体系的" サービス開発」の中で、社内で仮説検証を行う際に使われているツールについて触れている箇所がありました。 本記事ではそのツールと実際の取組み方について、実際の流れを踏まえながらもう少し詳しく説明していきます。

仮説検証

仮説検証は以下のフローで進んでいきます。

  1. 前提条件を確認する
  2. 検証の設計をする
  3. 各パターンの機能を実装する
  4. 各パターンにログを仕込む
  5. デプロイ後の監視
  6. 検証結果の振り返りとネクストアクション

小さく・手戻りなく・高速な検証を行うためには手を動かす前の段階、上記フローにおける1・2のステップが重要となります。

具体例として「朝と夜はプレミアム献立の需要が高まる」という仮説の検証フローを見ていきます。 これは献立プレミアム献立のアクセス分布をみると朝と夜にもアクセスが増加していたことから得た仮説です。

前提条件を確認する

下記の2つの点について合意が取れている必要があります。

  • 確かめたい価値(仮説)が明確化されている
  • その検証にA/Bテストを用いる

今回は話をわかりやすくするため「朝と夜はプレミアム献立の需要が高まる」という仮説が既にたっており、それをA/Bテストで確かめるという流れになります。ですが実際にはそもそも仮説が検証可能な状態にまで明確化されていないといった状況が考えられます。 手段を具体化する前にチームで方針決定・合意形成がなければ検証は始まりません。 ごく当たり前のように感じますが、いつでも振り替えられるよう土台を固めておくことが大事です。

検証の設計をする

仮説を確かめるためにA/Bテストの設計を行います。 まず、仮説を確かめるために何と何を比較するか考えます。 この記事で例題として扱う仮説は「朝と夜はプレミアム献立の需要が高まる」という仮説でした。 前提知識として人気順検索とプレミアム献立では人気順検索の方が需要があり、単純に人気順検索の枠をプレミアム献立に差し替えて比較すると前者が有効であることがわかっているとします。 よって、今回は「普段は人気順検索での訴求に使っていた枠を朝と夜の時間にだけプレミアム献立に切り替える」施策に取り組みます。

  • パターンA: 人気順検索(通常)
  • パターンB: 朝と夜だけプレミアム献立、それ以外の時間帯は人気順検索

パターンA パターンB
f:id:ragi256:20180221172307p:plain f:id:ragi256:20180221172324p:plain

この時、対象も出来る限り明確にしておきます。 今回はサイト内の該当部分を訪れたプレミアム会員以外の全てのユーザーを対象とすることにします。 また、検証の結果がどうなったかによって次にとるアクションまで決めます。

次にA/Bテストで監視・比較をするKPIも設定します。今回はプレミアムサービス会員の転換率(CVR)をKPIとします。 KPIが決定したことで同時に具体的なログの測定箇所と測定内容も決定します。 今回はそれぞれのパターンにおける訪問ユーザー数とプレミアムサービスへの転換数が必要となります。

ここで検証期間の見積もりを行うため、必要となるサンプルサイズを算出しておきます。 サンプルサイズの算出には「A/B両パターンの平均値」と「求める確度」を事前に決めておく必要があります。 言い方を変えると「どれだけの改善を確認したいのか」と「どれだけ偶然を排除したいか」という点です。 統計学では前者を効果量、後者を有意水準と検出力と呼びます。 詳しくは「仮説検証とサンプルサイズの基礎」を御覧ください。 これらを基にしてサンプルサイズを計算し、サンプルサイズと現状のUUから今回の仮説検証に必要とする日数を求めます。

そして検証設計の最後に、検証期間が経過した時点でどういう結果だったらどうするということを決めておきます。 実際に手を動かす前に、最終的な結果を大雑把に場合分けして次の行動を決めておくことが手戻りの防止につながります。

両パターンを実装する

パターンAには従来通りの挙動を、パターンBには時間帯によって枠内表示が変わるように実装をします。 この際、プロトタイプ開発用プラグインである「Chanko」とChankoのA/Bテスト用拡張である「EasyAb」を使うことで下記のように書くことができます。

パターンの制御を行うChanko内部のコントローラー

module TimeSlotPsKondate
  include Chanko::Unit
  include EasyAb

  split_test.add('default', partial: 'default_view')
  split_test.add('time_slot_ps_kondate', partial: 'time_slot_ps_kondate_view`')

  split_test.log_template('show', 'ab_test.time_slot_ps_kondate.[name].show')
  split_test.log_template('click', 'ab_test.time_slot_ps_kondate.[name].click')

  split_test.define(:card) do
      next run_default if premium_service_user? # プレミアムサービスユーザーは対象としない
      ab_process.log('show') # 訪問ユーザー数カウント用ログ
      render ab_process.fetch(:partial), time_slot: target_time?
  end
end

パターンの差し替えを行うChanko外部のviewファイル(haml)

-# 対象となるviewの書かれているファイル

= invoke(:time_slot_ps_kondate, card) do
  -# 差し替え部分 
end

これらに加え、パーシャルとして必要となる default_card.hamltime_slot_ps_kondate_card.hamlとCSSを追加すれば実装は完了です。 既存コードとの接点はinvokeメソッドの部分のみであるため、A/Bテストのon/offはごくわずかな変更で制御することができます。 Chankoは既存のコードと切り離された場所に置かれるため、検証の後始末もスムーズに行えます。

このように、ChankoとEasyAbを使うことで必要最低限のコードのみで検証を行えます。

両パターンにログを仕込む

今回はCVRをKPIとして追いかけていく必要があります。 不要なログを大量にとっても仕方がないのでログは必要最低限に留めるべきですが、後になって「あのログをとっておけばよかった」と後悔しても遅いため必要なログに抜け漏れがないよう列挙しておきます。 今回は該当部分のページに訪れた人(show)とプレミアムサービス枠をクリックした人(click)のログを取ります。 実際にCVRを取るには前者だけで十分なのですが、後者のログもとっておくことでCTRを算出できるようになり、クリエイティブに問題があったかどうか振り返るのに役立ちます。

A/Bテストに限らず一時的なログをササッと仕込みたい場合、社内ではKPI管理ツール「Hakari2」を使っています。 Ruby・JavaScript・HTMLそれぞれで利用することができます。

Ruby

Hakari2Logger.post("ab_test.hakari_log.ruby.A", user: user, request: request)

JavaScript

hakari2.post(['ab_test.hakari_log.javascript.A'])

HTML(hamlで書いた場合)

= link_to xx_path, class: 'track_hakari2', data: { hakari2_keywords: 'ab_test.hakari_log.html.A' }

このようにしてクライアント側でセットされたログは共通ログ基盤Figlogを経由し、最終的にDWHチームの管理するRedshift内へ格納されます。 A/Bテストを開始した後、ログの監視や分析を行う時にはこのログを他のデータと組み合わせて利用します。 今回の検証で必要となるCVRはshowのログとプレミアム会員登録のログを結合することで求まります。

デプロイ後の監視

A/Bテスト用の実装をデプロイし、公開した後に実装やログ取得にミスがないか確認をする必要があります。 日次の集計結果などはcookpadの管理用アプリケーション「papa」上のダッシュボードで確認できます。 検証期間後の最終的な検証結果もこちらで確認します。

f:id:ragi256:20180221172442p:plain

検証結果の振り返りとネクストアクション

「仮説検証の設計をする」の段階であらかじめ決めておいた目標サンプルサイズに到達したところで検証を終えます。 その時点で再度ダッシュボードを確認し、今回の施策の結果がどうであったかを結論づけます。

ダッシュボードでは集計値だけでなく、数値をもとにして描かれた確率分布や計測期間中の推移を見ることができます。 これらをみることで有意差がありそうかどうか、特定日時のイベントによる影響がないかどうかを確認します。

確率分布 時系列変化
f:id:ragi256:20180221172500p:plain f:id:ragi256:20180221172509p:plain

このステップでは知見を得るための考察や議論を行いますが、よほど想定外の結果にならない限り「検証の設計」で決めた方針に従い次の行動を決定します。 この施策に関しては当初想定していた量の改善が得られなかったため、仮説の正しさを証明する結果が得られませんでした。 この仮説は献立とプレミアム献立のアクセス分布から得た仮説でしたが、その仮説を得る過程をアプローチ方法から見直す必要があります。

まとめ

クックパッドで高速に仮説を検証するために普段行っている作業についてお話しました。 6ステップに分けて説明をしてきましたが、「前提条件の確認」と「検証の設計」までがキチンとこなせていればその後は特に考えることなく実行することができます。 このサイクルを回す作業に慣れていくことで、実際に手を動かす作業よりもサービス改善のためにどうするべきか頭を使う作業へ労力を割くことができるようになります。

また、今回はwebでのA/Bテストの説明をしましたが、iOS/Androidでも同様にA/Bテスト用のツールを利用することで手軽に仮説検証を行うことができます。

クックパッドでは日々このように各種ツールを利用してサービス改善のサイクルを高速にまわしています。

Cookpad TechConf 2018 開催報告

こんにちは、技術広報を担当している外村です。

f:id:hokaccha:20180210115823j:plain

2018年2月10日にエンジニア向けのカンファレンス、Cookpad TechConf 2018を開催しました。当日はたくさんの方に参加いただき、活気あるカンファレンスになりました。ご来場の皆様本当にありがとうざいました。

新しい試みとして、当日の司会をAmazon Pollyの音声合成で行なったのですが、こちらもみなさんにお楽しみいただけたようでした。

講演資料・動画

当日の講演資料および動画を公開いたしましたので是非ご覧になってください。

基調講演: 毎日の料理を楽しみにする挑戦をし続けた20年 by 橋本 健太

コーポレート戦略部本部長の橋本による基調講演でイベントはスタートしました。クックパッドはテックカンパニーとしてどのように成長してきたか、グローバル展開をどのように行ってきたか、現在取り組んでいる新プロジェクトについての話などがありました。

講演資料・動画

クックパッドの "体系的" サービス開発 by 新井 康平

会員事業部の新井の講演は、クックパッドではサービス開発の難しさにどのように立ち向かっているか、という内容です。こちらの講演の捕捉記事を公開していますのでこちらもご覧になってください。

"体系的" に開発サイクルを回して "効果的" に学びを得るには - クックパッド開発者ブログ

講演資料・動画

クックパッドクリエイティブワークフロー by 辻 朝也

会員事業部デザイナの辻の講演は、クックパッドにおけるサービス開発のフローについての話です。やるべき施策を決めてリリースし、分析して評価するという一連のサイクルの中で具体的にどういったことをおこなっているのか、というのがよくまとまっていました。

講演資料・動画

What/How to design test automation for mobile by 松尾 和昭

海外事業部にてサービスの品質向上やテストを担当している松尾の講演は、モバイルテストの自動化についての話です。モバイルのテストで重要なトピックをSPLIT(Scope, Phase, Level, sIze, Type)というキーワードにまとめて解説しました。

講演資料・動画

Rubyの会社でRustを書くということ by 小林 秀和

インフラストラクチャー部の小林の講演は、Rustを使ったプロダクト開発についての話です。CookpadはRubyを使ってサービス開発をすることが多いですが、そういった環境でRustを採用した経緯や、実際にRustを導入したプロダクトで得られた知見を紹介しました。

講演資料・動画

cookpad storeTV 〜クックパッド初のハードウェア開発〜 by 今井 晨介

メディアプロダクト開発部の今井の講演は、cookpad storeTVについての話です。cookpad storeTVはスーパーに設置する料理動画を配信するサイネージで、クックパッドがハードウェアの開発から手がけました。今井はその開発を担当しており、実際に発生した問題や具体的な開発フローについて紹介しました。

講演資料・動画

Challenges for Global Service from a Perspective of SRE by 渡辺 喬之

インフラストラクチャー部SRCグループの渡邉の講演は、クックパッドのグローバルサービスのSREとしてどのような取り組みをしてきたか、という内容です。グローバルサービスならではの課題というのはどういったものがあり、それをどう解決したのかという、あまり他では聞くことが少ない興味深い話でした。

講演資料・動画

動き出したクックパッドのCtoCビジネス by 村本 章憲

Komerco事業部の村本からの講演は、クックパッドの新規事業であるKomercoについての話です。Komercoとはどのようなサービスなのか、どのようなチーム・技術スタック・フローで開発しているかということについて紹介しました。

講演資料・動画

Solve "unsolved" image recognition problems in service applications by 菊田 遥平

研究開発部の菊田の講演は、機械学習による画像分析の取り組みについての話です。機械学習をサービスに活かすうえで難しい問題はどういったところにあるのか、それを実際の業務でどのように解決したか、という内容でした。

講演資料・動画

基調講演: Beyond the Boundaries by 成田 一生

取りをつとめたのはCTOの成田による基調講演でした。クックパッドの技術スタックはどのようなものか、エンジニアの行動指針である「Beyond the Boundaries」とは何なのか、エンジニアが成長できるために具体的にどういった取り組みを行っているか、といった話でイベントを締めました。

講演資料・動画

Lifestyle Product Award授賞式

講演の途中に、昨年開催した2017 Lifestyle Product Award by Cookpadの表彰式をおこないました。今回は優秀賞としてGOKURIが選出されました。

GOKURIは嚥下機能、飲み込みの能力を計測するためのデバイスです。基礎的な研究は数年前から行なわれていたものの、精度が課題となっていました。昨年、深層学習による精度向上によってプロダクトとしてリリースできる水準となり、リハビリ学会などでその成果が発表されました。

感想エントリ

当日参加いただいた方の感想エントリを以下にまとめました。素敵な記事を書いていただき、ありがとうございます。

他にもありましたら@hokacchaまでお知らせください!

まとめ

クックパッドにおけるサービス開発の手法やプロダクト開発の事例、その背景にある技術的なトピックなど、幅広い領域の講演をお届けしました。当日参加いただいた沢山の方に楽しんでいただいたようです。

クックパッドでは引き続き、このようなイベントを開催していきます。ぜひ、楽しみにしていてください!