Отслеживание событий на стороне клиента в общих чертах делится на три категории:

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

В этой статье в основном рассматриваются идеи и процесс реализации автоматического отслеживания.

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 или оставьте комментарий ниже!