Відстеження подій на боці клієнта загалом поділяється на три категорії:

  • Відстеження через код
  • Візуальне відстеження
  • Автоматичне відстеження

Ця стаття переважно знайомить з ідеями та процесом реалізації автоматичного відстеження.

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 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 й поставте зірку. Якщо маєте запитання, створіть issue на GitHub або залиште коментар нижче!