Clientseitige Ereigniserfassung lässt sich grob in drei Kategorien einteilen:

  • Codebasierte Erfassung
  • Visuelle Erfassung
  • Automatische Erfassung

Dieser Artikel stellt vor allem die Ideen und den Umsetzungsprozess hinter der automatischen Erfassung vor.

1. Codebasierte Erfassung

Codebasierte Erfassung bedeutet, an jeder zu erfassenden Stelle eine Schnittstelle aufzurufen und Ereignisdaten direkt hochzuladen. Die meisten Analysedienste von Drittanbietern stellen dafür eine bequeme api bereit.

Dadurch entsteht jedoch ein Problem: Der Erfassungscode ist über die gesamte Geschäftslogik verteilt und eng mit ihr gekoppelt. Wenn sich Ansichten oder Abläufe ändern, ist unklar, was mit alten Ereignissen geschehen oder ob neue benötigt werden. Mit wachsender Ereigniszahl wird die Pflege des verstreuten Codes zum Albtraum. Makros und Factory-Klassen können ihn vereinfachen, ändern daran aber grundsätzlich nichts.

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

2. Visuelle Erfassung

Bei der visuellen Erfassung werden Sammelpunkte über ein visuelles Werkzeug konfiguriert. Der Client wertet die Konfiguration automatisch aus und meldet Ereignisse – das sogenannte „spurenlose Tracking“. Eine repräsentative quelloffene Lösung ist Mixpanel.

3. Automatische Erfassung

Automatische Erfassung, auch vollständige Ereigniserfassung genannt, bedeutet nicht wirklich, dass es keine Instrumentierung gibt. Sie sammelt und meldet sämtliche Ereignisse; der Server filtert anschließend die nützlichen Daten heraus.

Als weitere Optimierung kann der Server eine Konfigurationsdatei senden, damit der Client die Ereignisse selbst filtert.

Für die KPIs erfand ich auf Grundlage dieser Idee das Rad neu und entwickelte WKTrackingData.

Hier halte ich einige Probleme und Gedanken fest, die bei der Umsetzung von WKTrackingData auftraten.

Die Grundidee ist einfach. Der Code ist wie folgt aufgebaut:

Ursprüngliche WKTrackingData-Architektur: Manager, Ereignispfade und AOP-Module

Hier übernimmt WKTrackingDataManager die gesamte Datenverwaltung und die Konfiguration der Ereigniserfassung.

Ursprüngliches Anwendungsbeispiel für die Konfiguration der Ereigniserfassung

WKTrackingDataViewPathHelper erzeugt event_path-Werte.

Das Modul AOP ist für die Erfassung sämtlicher Ereignisse zuständig.

Eine ausführliche Anleitung findest du auf GitHub.

Wie lässt sich ein Ereignis eindeutig identifizieren?

Dabei stellen sich zwei Fragen: Wie stellen wir ein Ereignis dar, und wie gewährleisten wir seine Eindeutigkeit?

Darstellung eines Ereignisses

Ereignisse werden mit event_path dargestellt. Für den responder, der ein Ereignis auslöst, besteht die Kernidee darin, seiner Responder-Kette zu folgen und deren path aufzubauen.

Der resultierende Responder-path sieht so aus:

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
Ein Ereignis eindeutig identifizieren

In manchen Geschäftsszenarien steht derselbe button oder ein anderes Steuerelement je nach veränderten Eigenschaften für unterschiedliche fachliche Ereignisse.

So kann ein button auf dem Startbildschirm im abgemeldeten Zustand Zum Anmelden tippen anzeigen und nach der Anmeldung bei noch ausstehender Identitätsprüfung Identität bestätigen.

Bei demselben button hat sich der Ansichtsbaum nicht verändert; der erzeugte Responder-path bleibt daher gleich.

Ähnliche Fälle sind der Ein-/Aus-Zustand von UISwitch, die Eigenschaft indexSelect von UISegmentedControl, UIStepper sowie Interaktionen von UITableView oder UICollectionView mit einer cell.

Um dies abzudecken, hängt WKTrackingData beim Aufbau von event_path ausgewählte Eigenschaften des Steuerelements an. Der resultierende event_path sieht dann so aus:

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
Fachspezifische Erweiterungen

Betrachten wir ein weiteres Szenario: ein banner-Karussell auf dem Startbildschirm.

Für jede banner-Position liefert der Server ein Bild und eine Ziel-url; auch die Positionen sind konfigurierbar.

Damit hat das banner an jedem index kein festes Ziel. Das Ereignis an Position 0 lässt sich nicht allein aus event_path bestimmen.

Zur eindeutigen Identifizierung müssen fachspezifische Parameter wie die url angehängt werden.

WKTrackingData unterstützt ebenfalls Erweiterungen um fachliche Parameter, sodass der Aufrufer eigene Parameter anhängen kann:

Ursprüngliches Codebeispiel zur Erweiterung von Ereignissen um fachliche Parameter

Steuerelemente, die nicht erfasst werden sollen, lassen sich mit wk_ignoreTracking ausschließen:

self.slider.wk_ignoreTracking = YES;

Weitere Probleme

Zur Erfassung verschiedener Ereignisse verfolgt WKTrackingData einen aspektorientierten Ansatz und verwendet die runtime, um Methoden direkt auszutauschen.

Für die Erfassung von UIAlertView, UIActionSheet, UITableView und UICollectionView müssen allerdings deren delegate-Methoden ausgetauscht werden.

  • Hat die delegate class die Methode bereits implementiert, wird sie einfach ausgetauscht.

  • Hat die delegate class sie nicht implementiert und soll das Ereignis dennoch erfasst werden, muss die Implementierung der entsprechenden delegate method manuell hinzugefügt werden.

- (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_));
        }
    }
}

Die Implementierung findest du hier

In der Praxis löste bei UITableView und UICollectionView jedoch selbst das manuelle Hinzufügen einer implementation für tableView:didSelectRowAtIndexPath: keinen Aufruf aus, wenn deren delegate-Objekte die Methode nicht implementiert hatten.

Sehen wir uns zunächst den normalen Aufruf-Stack von tableView:didSelectRowAtIndexPath: an:

Ursprünglicher Debugger-Aufruf-Stack für die Auswahl einer Tabellenzeile

Dabei ruft UITableView nacheinander diese vier Methoden auf, um auf das Antippen einer cell zu reagieren:

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

Die Reihenfolge zeigt, dass UITableView zunächst seine eigene Methode _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: aufruft und erst danach die Methode tableView:didSelectRowAtIndexPath: der delegate class.

Setze einen Haltepunkt auf _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: und untersuche den Assemblercode:

Ursprüngliche Assembleransicht mit der Verzweigung zur Delegate-Methode

Implementiert die delegate class bereits tableView:didSelectRowAtIndexPath:, funktioniert alles, und der Aufruf von tableView:didSelectRowAtIndexPath: wird abgeschlossen.

Wenn die delegate class dagegen tableView:didSelectRowAtIndexPath: nicht implementiert, führt selbst das manuelle Hinzufügen von tableView:didSelectRowAtIndexPath: mit einer implementation nicht dazu, dass versucht wird, auf tableView:didSelectRowAtIndexPath: zu reagieren.

Folgt man dem Assemblercode, zeigt sich ein direkter jump zu einer anderen Anweisung.

WKTrackingData wählt hier einen anderen Weg und tauscht direkt [UITableView _userSelectRowAtPendingSelectionIndexPath:] aus (bei UICollectionView auf ähnliche Weise). Dadurch können UITableView und UICollectionView Interaktionen auch dann erfassen, wenn das delegate-Objekt die zugehörige Ereignismethode der cell nicht implementiert.

4. Schlussgedanken

Mehrere event_path-Werte für dasselbe Ereignis

Bei mehreren veröffentlichten Versionen mit kleinen Unterschieden in den Ansichten kann dasselbe Ereignis mehrere event_path-Werte besitzen.

In der Praxis erfordert diese Zuordnung die Zusammenarbeit mit den Kollegen auf der Serverseite. Sie kann im Backend konfigurierbar gemacht werden.

Ereignisanalyse optimieren

Im Abschnitt Fachspezifische Erweiterungen ging es um das Hinzufügen fachlicher Parameter. WKTrackingData setzt dies mit Kategorien um.

In der Praxis verschwendete die vollständige Ereigniserfassung sehr viel Bandbreite. Viele dieser Ereignisse waren unnötig [(ŏ_ŏ)].

Als weitere Optimierung kann der Server eine Konfigurationsdatei mit allen Ereignissen senden, die er gemeldet haben möchte. Nur ein zur Konfiguration passender event_path wird hochgeladen.

Geschäftsparameter können an diesem Punkt auch ohne WKTrackingData abgerufen werden: Verwende stattdessen direkt kvc.

Zum Beispiel:

key_path = "viewController.banner.url"

Wenn du bis hierher gelesen hast, öffne WKTrackingData und gib dem Projekt einen Stern. Bei Fragen kannst du auf GitHub ein Issue eröffnen oder unten einen Kommentar hinterlassen!