クライアント側のイベントトラッキングは、おおむね三つに分けられます。

  • コードによるトラッキング
  • ビジュアルトラッキング
  • 自動トラッキング

この記事では主に、自動トラッキングの考え方と実装過程を紹介します。

1. コードによるトラッキング

コードによるトラッキングとは、記録したい箇所でインターフェースを呼び出し、イベントデータを直接アップロードする方法です。多くの外部分析サービスもコードベースのapiを提供しており、便利です。

ただし問題もあります。トラッキングコードがビジネスロジックの各所に散らばり、強く結びついてしまいます。画面や業務フローが変わると、古いイベントをどう扱うか、新しいイベントが必要かどうかがわかりません。イベント数が増えれば、散在するコードの保守は悪夢になります。マクロやファクトリークラスでコードを簡略化しても、根本は変わりません。

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    // 埋点
    APE(kEvent4001);
}

2. ビジュアルトラッキング

ビジュアルトラッキングでは、視覚的なツールで収集箇所を設定します。クライアントが設定を自動で解析してイベントを報告し、いわゆる「コードを残さないトラッキング」を実現します。代表的なオープンソースの方法にMixpanelがあります。

3. 自動トラッキング

全イベントトラッキングとも呼ばれる自動トラッキングは、実際には計測コードが不要という意味ではありません。すべてのイベントを収集して送信し、有用なデータの絞り込みをサーバーに任せます。

さらに最適化するなら、サーバーから設定ファイルを配信し、クライアント側でイベントを絞り込めます。

KPIのために、この考え方をもとに車輪を再発明し、WKTrackingDataを作りました。

ここでは、WKTrackingDataの実装で出会った問題と考えを記録します。

実装の考え方は単純です。コードの構成は次のとおりです。

当時のWKTrackingData構成図:マネージャー、イベントパス、AOPモジュール

ここで、WKTrackingDataManagerがすべてのデータ管理とイベントトラッキング設定を担当します。

当時のイベントトラッキング設定の使用例

WKTrackingDataViewPathHelperがevent_pathを生成します。

AOPモジュールがすべてのイベントのトラッキングを担当します。

詳しい使い方はGitHubで確認できます。

イベントを一意に識別するには?

ここには二つの問題があります。イベントをどう表現するか、そして一意性をどう保証するかです。

イベントの表現

イベントの表現にはevent_pathを使います。イベントを起こしたresponderについて、そのレスポンダーチェーンをたどり、チェーンのpathを構築するのが中心となる考え方です。

生成されるレスポンダーのpathは次のようになります。

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
イベントの一意な識別

業務上の場面によっては、同じbuttonなどのコントロールでも、プロパティの変化によって異なるイベントを表すことがあります。

たとえばホーム画面のbuttonが、サインアウト中はタップしてサインイン、サインイン後も本人確認が済んでいなければ本人確認へと表示される場合です。

同じbuttonのビューツリーは変わらないため、生成されるレスポンダーのpathも同じです。

同様の場面には、UISwitchのオン・オフ、UISegmentedControlのindexSelect、UIStepper、そしてUITableViewやUICollectionViewのcellタップがあります。

これに対応するため、WKTrackingDataはevent_pathを生成するとき、コントロール自体のプロパティを選んで末尾に加えます。その結果、event_pathは次のようになります。

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
業務固有の拡張

別の場面として、ホーム画面にbannerのカルーセルがある場合を考えます。

bannerの各広告枠に表示する画像と遷移先urlはサーバーから配信され、位置も設定できます。

この場合、bannerの各indexに対応するページは固定されていません。0番のイベントはevent_pathだけでは決められません。

イベントを一意に識別するには、urlなどの具体的な業務パラメーターを追加する必要があります。

WKTrackingDataは業務パラメーターの拡張にも対応し、呼び出し側が独自のパラメーターを追加できます。

イベントへ業務パラメーターを追加する当時のコード例

イベントを追跡したくないコントロールは、wk_ignoreTrackingで除外できます。

self.slider.wk_ignoreTracking = YES;

その他の問題

異なるイベントを追跡するため、WKTrackingDataはアスペクト指向の考え方にもとづき、runtimeでメソッドを直接入れ替えます。

ただし、UIAlertView、UIActionSheet、UITableView、UICollectionViewを集計するには、それぞれのdelegateメソッドを入れ替える必要があります。

  • もしdelegate classが該当メソッドをすでに実装していれば、そのまま入れ替えます。

  • もしdelegate classが該当メソッドを実装しておらず、それでもイベントを追跡したい場合は、対応するdelegate methodの実装を手動で追加します。

- (void)wk_swizzleInstanceSelector:(SEL)origSel_ fromClass:(Class)fromClass replaceSelector:(SEL)replaceSel_ originNotImp:(SEL)notImpSel_ {
    Method originalMethod = class_getInstanceMethod([self class], origSel_);
    if (originalMethod) {
        [self wk_swizzleInstanceSelector:origSel_ fromClass:fromClass replaceSelector:replaceSel_];
    } else {
        Method notImpMethod = class_getInstanceMethod(fromClass, notImpSel_);
        // 如果delegateClass没有实现 origSel_ 方法
        // 则给delegateClass的 origSel_ 添加 orginReplaceMethod 的实现
        BOOL didAddNotImpMethod =
        class_addMethod([self class],
                        origSel_,
                        method_getImplementation(notImpMethod),
                        method_getTypeEncoding(notImpMethod));
        if (didAddNotImpMethod) {
            NSLog(@"%@ did add not imp method %@" , NSStringFromClass([self class]) , NSStringFromSelector(notImpSel_));
        }
    }
}

具体的な実装はこちらです

しかし実際には、UITableViewやUICollectionViewのdelegateオブジェクトが該当メソッドを実装していない場合、tableView:didSelectRowAtIndexPath:へ手動でimplementationを追加しても呼び出されませんでした。

まず通常のtableView:didSelectRowAtIndexPath:のコールスタックを見てみます。

テーブル行を選択したときの当時のデバッガーコールスタック

ここではUITableViewが次の四つのメソッドを順に呼び出し、cellのタップへ応答します。

  • 1. [UITableView _userSelectRowAtPendingSelectionIndexPath:]
  • 2. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
  • 3. [UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
  • 4. [TableViewController tableView:didSelectRowAtIndexPath:]

呼び出し順を見ると、UITableViewはdelegate classのtableView:didSelectRowAtIndexPath:を実際に呼ぶ前に、自身の_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:を実行しています。

直接_selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:にブレークポイントを置き、アセンブリを確認します。

delegateメソッドへの分岐を示す当時のアセンブリ画面

もしdelegate classがtableView:didSelectRowAtIndexPath:を実装していれば、すべて正常に進み、tableView:didSelectRowAtIndexPath:の呼び出しが完了します。

しかしdelegate classがtableView:didSelectRowAtIndexPath:を実装していない場合は、tableView:didSelectRowAtIndexPath:へimplementationを手動で追加しても、tableView:didSelectRowAtIndexPath:へ応答しようとしません。

アセンブリの呼び出しを追うと、別の命令へ直接jumpしていました。

WKTrackingDataでは別の方法を採り、[UITableView _userSelectRowAtPendingSelectionIndexPath:]を直接入れ替えます(UICollectionViewも同様です)。これにより、UITableViewやUICollectionViewのdelegateオブジェクトが対応するcellイベントメソッドを実装していなくても、イベントを追跡できます。

4. まとめ

同じイベントに複数のevent_pathが対応する場合

公開中のバージョンが複数あり、画面にわずかな違いがある場合を考えます。同じイベントに複数のevent_pathが存在する可能性があります。

実際には、この対応づけにはサーバー側の担当者との協力が必要です。バックエンドで設定可能にできます。

イベント分析の最適化

前述の業務固有の拡張では、業務パラメーターの追加方法を説明しました。WKTrackingDataはこれをカテゴリで実装しています。

実際に使うと、全イベントトラッキングは大量の通信量を無駄にしました。不要なイベントが多すぎたのです [(ŏ_ŏ)]。

さらに最適化するには、報告してほしいイベントをすべて列挙した設定ファイルをサーバーから配信します。設定に合うevent_pathだけをアップロードします。

ここで業務パラメーターを取得する際は、WKTrackingDataの実装を使わず、kvcで直接取得することもできます。

たとえば:

key_path = "viewController.banner.url"

ここまで読んだら、WKTrackingDataを開いてstarを付けてください。質問があればGitHubでissueを作るか、下にコメントしてください!