Webサービスを支えるユーザログ基盤開発@Retty

はじめに

Retty Inc. Advent Calendar 2018 2日目の記事です。

toCで展開している我々のようなサービスでは、 A/Bテスト等を通じて機能開発に対する分析を行っています。

それらを支えるユーザイベントのロギングは サービスの持続的な開発を支える重要な要素の一つです。

この記事を書くにあたり、前調査で調べてみたところ システムログやアプリケーションログの文脈でのログ設計は言及されるものが多いものの サービスにおけるユーザログ設計に関して語られることは意外と少ないもようです。

Web における集客効果を測定する代表的なツールとしては Google AnalyticsGoogle Tag Manager といったサービスが使われており スマートフォンアプリの文脈では、Firebase Loggingなどの強力なサービスが 既にあることから 独自に設計するケースが少ないからかもしれません。

しかしながら、持続的な開発していく上で 設計や開発方針で色々考えることがあるなと思い この記事を書き連ねてみることにしました。

自己紹介が遅れました。 Rettyでソフトウェアエンジニアをやっています @takegueです。

免責事項

  • 本記事では、システムやアプリケーションのログについては言及しません。
  • ログを受けて転送するためのバックエンドシステム (ログの収集〜転送〜分析) も 同様に重要ではあるのですが、これについては今回触れません。
  • ロギングに関する具体的なフレームワークの話にスコープを絞ります。

なぜ大事なのか?

冒頭では、なぜ重要かまでについて触れませんでしたが 最初に、ユーザログに関する戦略および設計の重要性について触れておきたいと思います。

データの質が分析の質につながる

当たり前ですが、ログとして取得していないことは分かりません。 ユーザログの分解能が、分析でわかるユーザ行動の最大の分解能です。 そのため質の高い分析の為には、質の良いユーザログが必要です。

場当たりなログ取得を行っていると、例えばの以下の様な場合に問題が発生します。 「この新機能やたらと最近人気高いけど、周辺行動ってどうなってるの?」 「この前のoooする人のユーザは、このページでどういう行動をしているの?」

これらの分析に「あー、ログがないので分かりませんね」となると ログの仕込みから入ることになるため、所謂PDCAスピードの劣化の原因に繋がります。

「最初は大ざっぱに作って後から細かく分析する」といったcoarse-to-fineな戦略は 分析のためのログ設計として、スピード感を大事にする場合にはとても相性が悪い、と肌感として感じています。

詳細なログから周辺化して、行動の粒度を粗くする手法を考える方が良くうまくいきます。

ユーザログはビジネスロジックなどに密結合する

ユーザログの集計値がサービス改善のための目標値になることは多いはずです。 そもそもの目的はそれらの状況把握だからです。

ユーザ行動の“重要なイベント” は “分析においても重要"であることから、 どのようなユーザログを送るかは、即ちビジネスロジックと呼ばれるようなサービス自身の ドメインに密に紐付きます。

つまり開発としても非常にセンシティブなものになり、開発の際に憂慮するべきことが一つ増えます。

さらにログの埋め込みは、凝集性が低くなりやすく それこそprintデバッグのようにあちこちに埋められることになります。 そして事故で大事なログを他の人が誤って消さないように、コメントで

*ここはとても大事なところなので消さないこと!**”

のような文字列が埋め込まれるようになります

フロントエンド開発の激化

この数年でフロントエンド開発の様相が一変してしまいました。

テンプレートエンジンを用いてバックエンド側でHTMLを生成し、 ちょっとした動きをつけるために jQueryを使ってjavascriptを書いて時代から Vue.jsやReactといったフレームワークを利用し、javascriptMVCを記述する時代に変わりました。 ServiceWorkerといったブラウザ技術やSPA, AMPなどのWeb技術の周辺技術の発展も目覚ましいものがあります

とりわけサービスのフロント部分はtoC企業において、開発によって変更がなされる最も激しい部分です

ユーザログ基盤の設計は、これらの激しい変化に耐えうるものでなければなりません。

「サービス開発を続けていくうちに気づけばログが取れていない」の様な自体は必ず避けなければなりません。

Web開発におけるユーザログ設計の留意点

じゃあいかに設計するの?という時の観点として、僕の場合は以下の様な観点で考えてみました。

変更のしやすさ / 頑健性のバランスの問題

printデバッグほどの手軽さで追加/削除できるようにすれば良いかというと ログ量の設計の問題や大事なログを誤って消さないようにする必要もあるため、 変更のしやすさだけを追求すれば良いというわけではありません。

かといって、実装を隠蔽すれば良いかというとそういうことでもなく ログの内容はドメインの内容にそのまま直接紐付くことが多いため 隠蔽に苦労します。 ガチガチに設計として堅くしたとして、それがボトルネックになって 開発全体の速度が落ちることになることだけは、必ず避けなければなりません。

テスタビリティも課題の一つといえます。 何よりあらゆる多重発火や欠損を防ぎたいため、結合テストがある方が安心して開発できますが 結合テスト単体テスト比べると高コストです

何をユーザログとして取得すべきか?

分析の質はログの質次第、ということを言ったように 何を取得すべきかは非常に大事な要素です。

何が必要か分からんから、あらゆる情報をログに詰め込む!みたいなことができるかというと それをしてしまうとログの転送量の問題やパフォーマンスの問題に直面することになるでしょう 外部サービスを利用しているならば上限設定もあるはずです。

ユーザの行動ログを送る処理が重いせいで UXが悪くなってしまったでは本末転倒です ユーザの抱えるデバイスについても、格安SIM等のような帯域の厳しいデバイスも多く存在します。

プライバシーの問題もあります。 データの主権者がユーザであり、ユーザはそのデータの取り扱いに対して権利を持つことが GDPRによって定められ、EU圏内では個人データに関する厳しい取り決めがなされたことは まだ記憶に新しいでしょう。

コンポーネントからドメインを分離する

例えばレコメンドなどの機能開発を行ったとして、枠ごとにログを仕込むことを考えましょう

Vue.jsの場合、例えば以下の簡単なコンポーネントが考えられます

<!--
 RecommendedItem.vue
-->
<template>
  <article>
    <h2>{{ title }}</h2>
    <!-- some contens... -->
  </article>
</template>

<script>
export default {
  props: ['title'],
};
</script>
<!--
  RecommendedList.vue
-->
<template>
  <ul>
    <recommended-item
      v-for="item in items"
      v-bind:title="item.title"
      />
  </ul>
</template>

<script>

import RecommendedItem from './RecommendedItem';

export default {
  components: {
    'recommended-item': RecommendedItem,
  },
  data() {
    return {
      items: [
        { title: 'あなたが読むべきたった一つの本' },
        { title: 'おら、東京さ、行くぞ' },
        { title: ‘突破力 〜成功するたった一つの方法〜' },
      ],
    };
  },
};
</script>

これに対するログを考えたとき、以下のような情報は取得するでしょう

  • ページに関する pageviewログ
  • List 自体の in-viewログ
  • ListItem に対する in-viewログ
  • ListItem に対する click ログ

これだけのログがあれば

  • レコメンド自体のinview率 = List Inview数 / PV 数
  • レコメンド自体のクリック率 = ListItem click数 / List in-view 数
  • レコメンドで表示されるアイテムに関するクリック率 = ListItem click数 / ListItem in-view 数

のような分析程度が行えます。

問題となるのが ListItemに関するログです。 より詳細な分析を行おうと考えた場合、以下の様なコンテキストが欲しくなります

  • List自体の表示のロジック (e.g. 価格順 / オススメ度順 / 人気順 ...)
  • ListItemが何番目に表示されているか?
  • ListItemに表示しているコンテンツの情報 (e.g. 商品ID / 文言等 …)

これらの情報を ListItem自体のclickやinviewの周辺情報として仕込むことになりますが 一方でコンポーネントの設計として考えると、これらの情報は ログのためだけに必要な情報であって、コンテンツ表示のために必要な情報ではありません。 そのためListItemにこれらの情報を与えるのは知りすぎ、ということになります。

コンポーネント指向で設計を考えると、下位のコンポーネントほど使いまわしがしやすいようにシンプルに保ちたいですが ユーザログ的には 下位のコンポーネントほどユーザ行動に関係するため 詳細なログを仕込みたくなります。

これはアプリケーションの好ましい設計と相反し、コンポーネント自体が情報を持ちすぎる原因にもなります。

ログは容易にドメインロジックと密結合しやすいという話を先ほどしましたが コンポーネントが情報を知りすぎて、果てはビジネスロジックもガチガチに埋め込まれている… といった情報は避けたいところです。

どのようになったか?

ここから具体的な開発な話になります。

Rettyで採用しているフレームワークはVue.jsですが Vue.jsに用意されているプラグイン機構を使いうことで 全てのVueコンポーネントに対してmixinとして共通処理を記述することができます。

今回作ったログ機構は、これを使うことで本体側のコンポーネント設計と切り離しました。

また フレームワークとの密結合 / 外部サービスとの 密結合を避けるため ログ機構自体を 以下の3つのパートに分けています。データ基盤とかでよく見るETL処理の様な構成にしています。

Extract部
Transform部
Load部
コンポーネント(DOM)ツリーのイベントの管理と
コンテキスト情報の埋め込みを担う
ログとして送信するための
情報の加工を行う。ドメイン特有のロジック等も入ってくる

ユーザログの送信とその受け手となるサービスとの調整を担う
  • フレームワークと結合部分
  • イベントの定義
  • イベントのハンドラ管理
  • ユーザ情報の定義
  • フィルタリング
  • データの変形
  • ドメイン特有のロジック

Extract部は、最も開発が激しく変化の激しいくフレームワークとも結合する部分です フレームワークのライフサイクルに合わせた開発を行います。 下位コンポーネントから上位コンポーネントへ伝播することで、 上位コンポーネントは下位コンポーネントに対してコンテキスト情報の付加を行えるようにしています。 これにより下位コンポーネントが知りすぎることはなくなります。

Transform部では、Extract部から送られてきた情報の加工およびフィルタリング等を行います。 ドメイン特有のロジック等もここに入れ込むことで、集約化を図ります また変化の激しいExtractから切り離すことで、ドメイン特有のロジックを保護する目的もあります

Load部では送信のために必要な外部サービスとの連携の役割を担います。 効果的にデータを送信するためのバッファリングだったり、失敗した場合のフォールバック処理等も行います。

フレームワークとの密結合するExtrac部に集約されるため、Transform部やLoad部は疎に保たれ テスタビリティを高く確保した実装ができます。

typescriptを入れ込むこむとでより幸せになれます。 上記の様な構成では、interafceやデータ型の恩恵を大きく受けることができます。 これらのやりとりの際に、型があるとログの形式に関するバリデーションコストが低くなります。 Vue.js は TSサポートしていますが、少々扱いにくい点があったりしますので Vue部分のTS化は様子を鑑みてかなと考えています。

おわりに

本記事では、ユーザログの設計と開発についてまとめました。 今回は現段階のログ基盤についてお話しいたしました。いかがでしたでしょうか?

ベンチャー企業としてのアジリティ、理想のデータ基盤を実現するため 上記のような開発をしてたりします。

ご興味をもたれた方、盛り上げてやるか!と思って頂いた方 Rettyでは一緒に働く仲間を絶賛募集中です。

corp.retty.me