Keen IOによるやさしいユーザー行動ログ収集

この記事は[CrowdWorks Advent Calendar 2017](https://qiita.com/advent-calendar/2017/crowdworks)の19日目の記事です。

ユーザー行動ログの収集に[Keen IO](https://keen.io/)というサービスがとても便利だったため、今回はこのサービスについて選定の理由と簡単な使い方について紹介していきます。


# 行動ログの収集の目的

ひとことでいえば、「ユーザーの嗜好を汲み取ってサービスを改善するため」です。

行動ログを分析することによって、ユーザーからのフィードバックを得ることができます。この行動ログによるフィードバックは、直接的なフィードバックよりも取得できるデータの量が多く幅も広いのがメリットです。

また、直接的なフィードバックでは「声の大きい人の意見の印象が強くなる」や「背景に真の目的があるにも関わらずフィードバックをそのまま受け取ってしまう」といったバイアスに注意する必要がありますが、行動ログではデータという事実から分析することができます。

CrowdWorksではこの行動ログを用い、推薦アルゴリズムによって提示した内容をチューニングし、マッチングのユーザー体験を改善していこうとしています。マッチングに関するCrowdWorksの取り組みについては、次の記事に記載されていますのでご覧ください。

- [クラウドワークスの技術戦略とアドベントカレンダー始めますのご紹介 - クラウドワークス エンジニアブログ](http://engineer.crowdworks.jp/entry/2017/11/30/092240)
- [機械学習とかつかっていいかんじのマッチングを実現するプロジェクトでのプロダクトオーナーの役割 - Qiita](https://qiita.com/yo-iida/items/053f7613a68e086a01a4)
- [「うちのサービスに人工知能を使って何か面白いことやってよ」と言われた時に考えるべきこと - Qiita](https://qiita.com/akiray03/items/b5bb12771c5ce90050e7)

もちろん行動ログだけに頼るのではなく、状況に応じて直接的なフィードバックを踏まえながらバランスよくサービス改善に役立てることが重要です。


# サービスの選定

行動ログはページビュー数以上のデータ量になるため、それに耐えうるインフラを設計してメンテナンスしなければなりません。そのためのコストを考えると内製は避け、外部サービスを検討することにしました。

検討は主に「[16 best alternatives to Mixpanel as of 2017 - Slant](https://www.slant.co/options/1405/alternatives/~mixpanel-alternatives)」に挙がっているサービスを対象にしました。追加で[Segment](https://segment.com/)というサービスも検討しました。


## 選定基準

検討にあたって以下の基準を設けました。


### 必要な言語に対応したSDKが存在すること

CrowdWorksはWebアプリケーション(Rails)だけでなく、スマートフォン向けのアプリケーション(iOS / Android)も提供しています。これらのプラットフォームに対応するために、JavaScriptとRuby、Swift、JavaのSDKが欲しいです。

### 外部データストアへのストリーミングができること

その外部サービスにだけでなく、信頼性のあるS3やBigQueryなどにも永続化しておきたいです。CrowdWorks本体のデータはRedshiftに同期しており、そのデータと組み合わせて分析したいため、同じAWSサービスであるS3の方が望ましいです。

また、自前で定期的にエクスポートなんてことはしたくないため、ストリーミングにも対応していて欲しいです。


### 価格が妥当であること

既にGoogle Analyticsによってページビューは分かるため、それに対して取得したいイベントの種類の数を掛け算してイベント数を見積もります。サービスによってはイベント数ではなく、月次トラッキングユーザー数で料金計算する場合があります。


### 公式ドキュメントが充実していること

これは実際にSDKを利用するときも重要なのですが、それ以前に上記の条件を満たしているのかを調べるにあたっても大事な要素になります。


## Keen IOの選定理由

Keen IOは上記の条件に加え、必要な機能を備えつつ不要な機能が少なく、シンプルでわかりやすいという特徴があります。

他のサービスは分析までサービス内で完結できるようにビジュアライゼーションの機能が充実しているものが多いように見受けられました。今回の我々のニーズは分析を既存のRedashの基盤に統一したかったため、サービス側の豊富なビジュアライゼーション機能は不要でした。

それに対し、Keen IOはビジュアライゼーションの機能はシンプルなのに対し、Web Auto-Collection(後述)といったデータ収集に便利な機能を備えています。

また、Keen IOがドキュメントがとてもわかりやすいです。他のサービスは何ができるかを探すために読み解くのが結構大変でした。


# Keen IOを使ってみる

ここからは実際にKeen IOをRailsアプリケーションに組み込んで使う例を紹介していきます。今回はJavaScriptによる[Web Auto-Collection](https://keen.io/docs/streams/web-auto-collection/)をカスタマイズして利用してみます。

また、アカウントの作成手順については省略します。以下は既にアカウントを作成してある前提で話を進めていきますので、必要に応じて[Keen IOのサイト](https://keen.io/)でアカウントを作成してください。

[$20を超えない使用量であれば無料](https://keen.io/pricing/)ですので、ぜひ試してみてください。

## Web Auto-Collectionとは

Web Auto-Collectionでは次のイベントを自動的に収集することができます。

- ページビュー
- クリック (ボタンやリンクだけでなくすべてのクリック)
- フォーム送信 (送信された内容も含む)

JavaScriptの定義を数行入れるだけで動くようになるため手軽ではあるのですが、クリックはリンクだけでよかったり、追加でスクロールのイベントを取得したかったりします。

嬉しいことにこの機能には柔軟性があり、少し設定してあげるだけでWeb Auto-Collectionの恩恵を受けながらカスタマイズできます。


## アクセスキーの作成

Rails側にコードを書いていく前に、まずはアプリケーションからKeen IOにイベントを記録するためのアクセスキーを発行します。

新規にプロジェクトを作成するか、アカウント登録時に作成されたプロジェクトを利用します。

<img alt="New Project" src="https://qiita-image-store.s3.amazonaws.com/0/7055/cd98d5a8-5e7e-ab43-14ac-07382f0b1de7.png" width="400">

プロジェクトの「Access」タブからアクセスキーの発行が行えます。

<img alt="Access Tab" src="https://qiita-image-store.s3.amazonaws.com/0/7055/557a59c5-cfaf-44d8-9afa-10cce34a1470.png" width="400">

「Project Details」に各種キー情報が載っていますが、これは何かあったときに丸ごと再生成しなければならないため、用途に応じてアクセスキーを個別に発行するのがよいです。

<img alt="Project Details" src="https://qiita-image-store.s3.amazonaws.com/0/7055/7126d275-3e27-bffd-e757-67e019f4a50f.png" width="400">

アクセスキーは「Project Details」の下にある「Access Keys」から作成できます。

<img alt="Access Keys" src="https://qiita-image-store.s3.amazonaws.com/0/7055/f49d5995-deef-0655-a706-269118a5ed10.png" width="400">

アクセスキーには細かい権限が設定できます。

<img alt="New Access Key" src="https://qiita-image-store.s3.amazonaws.com/0/7055/9bb016ed-995f-b952-6016-5433bfa88171.png" width="400">

デフォルトでは色々書かれていて「うっ」となる人もいるかもしれませんが、アプリケーションから利用する分には書き込みさえできればよいので次のようにします。

```json
{
  "is_active": true,
  "name": "Sample Access Key",
  "options": {
    "writes": {
      "autofill": {}
    }
  },
  "permitted": [
    "writes"
  ]
}
```


## Railsへの設定

まずは上記で作成したキーを設定ファイルに追加しておきましょう。`config/environments`配下の各環境用のファイルに次のように記述します。

```ruby:config/environments/development.rb
Sample::Application.configure do
  ...

  # Keen IO
  config.keen_tracking_enabled = true # あとでKeen IOの有効・無効を切り替えられるように実装します
  config.keen_project_id = '********************' # 「Project Details」の「Project ID」を設定します
  config.keen_write_key = '*********************' # 上記で作成したアクセスキーを設定します
end
```

次にKeen IOを利用するためのJavaScriptの定義をビューに実装していきます。

[keen-tracking.js/auto-tracking.md](https://github.com/keen/keen-tracking.js/blob/7500e1977872657127979c2f322edc46e4095374/docs/auto-tracking.md)を参考にして設定します。

```html+erb:app/views/shared_partials/_keen_tracking_tag.html.erb
<script>
  <%# Keen IOのライブラリを非同期に取得する。 %>
  // Loads the library asynchronously from any URI
  !function(name,path,ctx){
    var latest,prev=name!=='Keen'&&window.Keen?window.Keen:false;ctx[name]=ctx[name]||{ready:function(fn){var h=document.getElementsByTagName('head')[0],s=document.createElement('script'),w=window,loaded;s.onload=s.onreadystatechange=function(){if((s.readyState&&!(/^c|loade/.test(s.readyState)))||loaded){return}s.onload=s.onreadystatechange=null;loaded=1;latest=w.Keen;if(prev){w.Keen=prev}else{try{delete w.Keen}catch(e){w.Keen=void 0}}ctx[name]=latest;ctx[name].ready(fn)};s.async=1;s.src=path;h.parentNode.insertBefore(s,h)}}
  }('Keen','https://d26b395fwzu5fz.cloudfront.net/keen-tracking-1.4.0.min.js',this);

  <%# Keen IOのライブラリ取得が完了した後に処理を実行する。Keenオブジェクトはグローバルに参照可能。 %>
  Keen.ready(function (){
    <%# 他の箇所でKeenに認証情報がセットされたクライアントを利用できるようにグローバルに配置する %>
    window.KeenClient = new Keen({
      projectId: '<%= @keen_project_id %>',
      writeKey: '<%= @keen_write_key %>'
    });

    <%# イベントに付加情報を追加する %>
    KeenClient.extendEvents(function (){
      return {
        http_request_uuid: '<%= @keen_request_uuid %>',
        user: <%= raw @keen_user_attributes.to_json %>
      };
    });

    <%# Auto Trackingのどの機能を有効にするかを制御する %>
    KeenClient.initAutoTracking({
      ignoreDisabledFormFields: false,
      ignoreFormFieldTypes: ['password'],
      recordClicks: true,
      recordFormSubmits: true,
      recordPageViews: true,
      recordScrollState: true
    });

    <%# 少なくとも一度だけスクロールしたらイベントを飛ばすサンプル %>
    Keen.utils.listener('window').once('scroll', function (_event){
      KeenClient.recordEvent('scroll', {
        scroll: {
          leastOnce: true
        }
      });
    });
  });
</script>
```

Web Auto-Collectionでは`keen-web-autocollector-1.0.8.min.js`というJavaScriptをロードするのですが、カスタマイズする場合は`keen-tracking-1.4.0.min.js`を利用します。

こちらのコードを利用した場合はクリックイベントの収集の対象がデフォルトで`a`要素に限定されます。

また、イベントリスナーの設定を便利にしてくれる[`Keen.utils.listener`](https://github.com/keen/keen-tracking.js/blob/b1a22f9d940e085be9b61d9066225edc59db1c84/docs/listeners.md)を利用することにより、「少なくとも一度だけスクロールした」という行動も取得できるため設定しています。

そしてこのビューをレンダリングするためのヘルパーを用意します。設定が有効なときのみにビューをレンダリングするようにして機能の有効・無効を制御しています。

```ruby:app/helpers/application_helper.rb
module ApplicationHelper
  ...

  # Keen IOのトラッキングタグを表示
  def keen_tracking_tag
    return unless keen_tracking_tag_available?

    @keen_request_uuid = request.uuid
    @keen_project_id = Rails.application.config.keen_project_id
    @keen_write_key = Rails.application.config.keen_write_key

    # イベントに付属させるデータを作る
    @keen_user_attributes = keen_user_attributes

    render 'shared_partials/keen_tracking_tag'
  end

  ...

  private

  # Keen IOのトラッキングタグを出すか否かを判定する
  def keen_tracking_tag_available?
    Rails.application.config.keen_tracking_enabled
  end

  # Keen IOに送信するユーザ属性情報を組み立てる
  def keen_user_attributes
    {
      user_id: current_user.try(:id),
      ...
    }
  end

  ...
end
```

あとは`application.html.erb`の`head`要素に埋め込んであげるだけです。

```html+erb:app/views/layouts/application.html.erb
...
<head>
  ...
  <%= keen_tracking_tag %>
</head>
...
```

Railsアプリケーションを起動していろいろ触ってみましょう。その後、Keen IOプロジェクトページの「Streams」タブを開きます。

<img alt="Streams Tab" src="https://qiita-image-store.s3.amazonaws.com/0/7055/3a75be51-fa57-ca45-0074-7cc1d475dcdd.png" width="400">

すると次のような画面が現れます。

※ グラフの描画はリアルタイムではないようなので初回アクセスだと何もない状態かもしれません。

<img alt="Event Streams" src="https://qiita-image-store.s3.amazonaws.com/0/7055/c1c55ce5-df04-2d3d-0f3f-a6b9d1fb5276.png" width="400">

イベント名がリンクになっているので選択するとイベントの内容を参照することができます。

<img alt="Streams clicks" src="https://qiita-image-store.s3.amazonaws.com/0/7055/88d83213-5d04-1e38-b04a-54602f8912b7.png" width="400">

この画面を見ながらRailsアプリケーションを操作するとイベントが飛んでくる様子がわかるので面白いです。


## S3へのストリーミング

おまけでS3へのイベントデータのストリーミング設定も紹介します。

[Amazon S3 Integration | Keen IO](https://keen.io/docs/integrations/amazon-s3/)

S3へのストリーミング設定は驚くほど簡単です。あらかじめバケットを作成しておいてください。

「Streams」タブを開きます。

<img alt="Streams Tab" src="https://qiita-image-store.s3.amazonaws.com/0/7055/3a75be51-fa57-ca45-0074-7cc1d475dcdd.png" width="400">

ここに「Amazon S3」の設定があります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/08ef7dc6-a464-fdbd-5e0f-8ad07e9e0c22.png" width="400">

「Configure S3 Streaming」ボタンを選択するとバケット名の入力フォームと設定手順が表示されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/b82d81ed-f6a1-7aab-a45c-afed31d6e019.png" width="400">

設定の流れとしては対象のバケット名を入力し、設定手順に記載されているIDを使ってKeen IOのAWSアカウントのアクセスをバケットに指定するだけです。

AWS Management Consoleに移動し、作成したバケットの「Permissions」タブを開きます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/9747f161-12a5-85ea-cf40-3919877eeae8.png" width="400">

「Access for other AWS accounts」から「Add account」でアクセスを許可するAWSアカウントを追加します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/0996b0c9-8ca8-7844-6083-c2ed668f4d2a.png" width="400">

手順に記載されているIDを入力し、すべての権限にチェックを入れます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/0fb8b911-41ee-753e-9d75-59a57a8b1416.png" width="400">

入力内容を保存したらアカウント名が「dan」になっていることを確認してください。これがKeen IOのAWSアカウント名です。

<img src="https://qiita-image-store.s3.amazonaws.com/0/7055/0e8d7c7f-fd74-7a8c-3728-dce0f1f20ec1.png" width="300">

ストリーミングの設定をすると、次の形式でオブジェクトが5分ごとに保存されていきます。

```
<Bucket名>/<プロジェクトID>/<ISO-8601形式のタイムスタンプ>/<イベント名>/<プロジェクトID>-<イベント名>-<ISO-8601形式のタイムスタンプ>.json.gz
```

このJSONファイルはStreamタブで参照可能な形式に、タイムスタンプとプロジェクトIDが格納された`keen`キーが付与された形式となっています。

```json
{
  "keen": {
    "timestamp": "2017-12-18T08:56:26.968000+00:00",
    "created_at": "2017-12-18T08:56:27.092133+00:00",
    "id": "<プロジェクトID>"
  },
  "tracked_by": "keen-tracking-1.4.0",
  "geo": {
    "city": "Tokyo",
    "province": "Tokyo",
    "country": "Japan",
    "country_code": "JP",
    "continent": "Asia",
    "postal_code": "100-0001",
    "coordinates": [
      139.7677,
      35.6427
    ]
  },
  ...
}
```


# Q&A

## 他のJavaScriptのイベントを妨害したりしない？

JavaScriptによるイベントの記録は[`EventTarget.addEventListener`](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)によってリスナーが設定されるため、基本的に副作用はありません。([lib/utils/listener.js](https://github.com/keen/keen-tracking.js/blob/73856e1acd85f9c860e5c5c3cf1813560e1a44c1/lib/utils/listener.js#L149-L155))

しかし、問題が発生するパターンは存在します。

自分が遭遇したパターンではjQueryプラグインによってクリックイベントのリスナーが仕掛けられた`div`要素の子要素として`a`要素が存在していました。Keen IOをロードすると`a`要素にクリックイベントのリスナーが仕掛けられるため、次のようになります。

```html
<div class="plugin-target"><!-- <= プラグインによってクリックイベントリスナーが仕掛けられる -->
  <a>Sample</a><!-- <= Keen IOによってクリックイベントリスナーが仕掛けられる -->
</div>
```

この状態でこの要素が表示されている部分をクリックすると、`div`要素のクリックイベントではなく`a`要素のクリックイベントが発動し、`href`属性がないため現在のURLがリロードされるという動きをしてしまいます。

プラグイン側でイベントリスナーを仕掛ける対象の要素を操作できる場合は`a`要素に向ければよいのですが、今回は操作できなかったため、`a`要素に対して[`event.preventDefault`](https://developer.mozilla.org/ja/docs/Web/API/Event/preventDefault)を呼び出すことによって回避しました。

```coffeescript
  $('.plugin-target a').on('click', (e) ->
    e.preventDefault()
  )
```

このようにもし問題が発生した場合は、Keen IOの呼び出し側で除外コードを実装するのではなく、元のアプリケーションコードを修正したほうがよいでしょう。除外ルールの実装とルール自体が増えていくことによりコードが複雑化しますし、問題が発生するのは元の構造に原因がある可能性が高いためです。


## `clicks` event collectionで`data`属性の値を取得できる？

イベントを区別するために`a`要素の`data`属性に識別子を埋め込んでおきたかったのですが、現時点では残念ながら`data`属性の値はイベントに含まれません。[取得する属性が限定されているため](https://github.com/keen/keen-tracking.js/blob/9bbc71e8b2a0e301aab51ab677877172e8cc0dd2/lib/helpers/getDomNodeProfile.js)です。

Auto-Collectionの機構に乗っかるのであれば`class`属性を利用するのが安い代替案かと思います。


# おわりに

サービスの改善をしようと思ったときに蓄積されたデータがあるととても有益です。

[完全にWeb Auto-Collectionに任せる](https://keen.io/docs/streams/web-auto-collection/)のであればさらに簡単に実装することができますが、データは取得したいけど本番環境となるとカスタマイズしたくなるニーズは高いと思います。そんな方にこの記事がお役に立てば幸いです。