Відстеження подій на боці клієнта загалом поділяється на три категорії:
- Відстеження через код
- Візуальне відстеження
- Автоматичне відстеження
Ця стаття переважно знайомить з ідеями та процесом реалізації автоматичного відстеження.
1. Відстеження через код
Відстеження через код означає виклик інтерфейсу в кожній потрібній точці з безпосереднім надсиланням даних події. Більшість сторонніх сервісів аналітики надають api для такого відстеження, що досить зручно.
Але виникає проблема: код відстеження розкиданий по всій бізнес-логіці й тісно з нею пов'язаний. Коли змінюються екрани або бізнес-процеси, незрозуміло, що робити зі старими подіями й чи потрібні нові. Зі зростанням кількості подій супровід розрізненого коду відстеження стає кошмаром. Макроси й фабричні класи можуть спростити код, але принципово ситуацію не змінюють.
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋点
APE(kEvent4001);
}
2. Візуальне відстеження
За візуального відстеження точки збору налаштовують через візуальний інструмент. Клієнт автоматично розбирає конфігурацію й надсилає події, забезпечуючи так зване «відстеження без слідів». Один із характерних прикладів із відкритим кодом — Mixpanel.
3. Автоматичне відстеження
Автоматичне відстеження, яке також називають відстеженням усіх подій, насправді не означає відсутності інструментування. Воно збирає й надсилає всі події, залишаючи серверу фільтрування корисних даних.
Для подальшої оптимізації сервер може надсилати файл конфігурації, щоб клієнт сам фільтрував події.
Заради KPI я знову винайшов колесо на основі цієї ідеї та створив WKTrackingData.
Тут я занотовую деякі проблеми й ідеї, що виникли під час реалізації WKTrackingData.
Ідея реалізації проста. Код організовано так:

Тут 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 або залиште коментар нижче!