Il tracciamento degli eventi lato client rientra in tre categorie:

  • Tracciamento nel codice
  • Tracciamento visivo
  • Tracciamento automatico

L’articolo presenta idee e realizzazione del tracciamento automatico.

1. Tracciamento nel codice

Il tracciamento nel codice chiama un’interfaccia in ogni punto da monitorare, caricando direttamente i dati. Quasi tutti i servizi di analisi offrono una comoda api.

Ma il codice si disperde nella logica di business e vi resta strettamente accoppiato. Quando cambiano schermate o flussi, non è chiaro come trattare i vecchi eventi o se ne servano di nuovi. Con molti eventi, mantenerlo diventa un incubo. Macro e factory possono semplificarlo, ma non risolvono il problema.

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

2. Tracciamento visivo

Il tracciamento visivo configura i punti tramite uno strumento grafico. Il client interpreta la configurazione e invia gli eventi, realizzando il cosiddetto “tracciamento senza tracce”. Una soluzione open source rappresentativa è Mixpanel.

3. Tracciamento automatico

Il tracciamento automatico, o completo, non significa assenza di strumentazione: raccoglie e invia tutti gli eventi, lasciando al server il filtro dei dati utili.

Come ottimizzazione, il server può inviare una configurazione affinché il client filtri gli eventi.

Per i KPI, reinventai la ruota e creai WKTrackingData.

Qui annoto problemi e idee incontrati realizzando WKTrackingData.

L’idea è semplice. Il codice è organizzato così:

Architettura originale di WKTrackingData: manager, percorsi evento e moduli AOP

Qui WKTrackingDataManager gestisce dati e configurazione del tracciamento.

Esempio originale di configurazione del tracciamento

WKTrackingDataViewPathHelper genera i valori event_path.

Il modulo AOP traccia ogni evento.

Per l’uso dettagliato, visita GitHub.

Come identificare un evento univocamente?

Ci sono due domande: come rappresentarlo e come garantirne l’unicità?

Rappresentare un evento

Gli eventi usano event_path. Per il responder che li attiva, si segue la catena dei responder e se ne costruisce il path.

Il path risultante è:

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
Identificare un evento univocamente

In alcuni scenari lo stesso button o controllo rappresenta eventi diversi al cambiare delle sue proprietà.

Per esempio, un button sulla home può mostrare Tocca per accedere da disconnessi, poi Verifica identità dopo l’accesso se la verifica è incompleta.

Per lo stesso button l’albero delle viste non cambia, quindi il path resta uguale.

Casi simili sono lo stato di UISwitch, la proprietà indexSelect di UISegmentedControl, UIStepper e le interazioni con una cell di UITableView o UICollectionView.

Per gestirli, WKTrackingData costruisce event_path aggiungendo proprietà selezionate del controllo. Il nuovo event_path è:

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

Consideriamo un altro scenario: un carosello banner sulla home.

Per ogni posizione banner, immagine e url di destinazione arrivano dal server; anche le posizioni sono configurabili.

Ogni index del banner non ha quindi una destinazione fissa. L’evento in posizione 0 non si determina dal solo event_path.

Per identificarlo bisogna aggiungere parametri specifici, come l’url.

WKTrackingData supporta questi parametri e permette al chiamante di aggiungerne di propri:

Esempio di codice originale con parametri di business aggiuntivi

I controlli da non tracciare possono essere esclusi con wk_ignoreTracking:

self.slider.wk_ignoreTracking = YES;

Altri problemi

Per eventi diversi, WKTrackingData usa un approccio orientato agli aspetti e la runtime per scambiare direttamente i metodi.

Per UIAlertView, UIActionSheet, UITableView e UICollectionView, però, occorre scambiare i metodi delegate.

  • Se la delegate class implementa già il metodo, basta scambiarlo.

  • Se la delegate class non lo implementa e vogliamo tracciare l’evento, bisogna aggiungere manualmente l’implementazione del delegate method corrispondente.

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

L’implementazione è qui

In pratica, se gli oggetti delegate di UITableView e UICollectionView non implementavano il metodo, aggiungere una implementation a tableView:didSelectRowAtIndexPath: non lo faceva comunque partire.

Vediamo prima il normale stack di chiamata di tableView:didSelectRowAtIndexPath::

Stack originale del debugger per la selezione di una riga

Qui UITableView chiama in sequenza questi quattro metodi per rispondere al tocco di una cell:

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

L’ordine mostra che UITableView invoca prima il proprio _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:, poi il metodo tableView:didSelectRowAtIndexPath: della delegate class.

Imposta un breakpoint su _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: ed esamina l’assembly:

Vista assembly originale con il ramo del metodo delegate

Se la delegate class implementa già tableView:didSelectRowAtIndexPath:, tutto funziona e la chiamata a tableView:didSelectRowAtIndexPath: termina.

Ma se la delegate class non implementa tableView:didSelectRowAtIndexPath:, anche aggiungere manualmente tableView:didSelectRowAtIndexPath: con una implementation non provoca un tentativo di rispondere a tableView:didSelectRowAtIndexPath:.

Seguendo l’assembly si vede un jump diretto a un’altra istruzione.

WKTrackingData scambia direttamente [UITableView _userSelectRowAtPendingSelectionIndexPath:] (e usa un approccio simile per UICollectionView). Così UITableView e UICollectionView tracciano le interazioni anche se il delegate non implementa il metodo della cell.

4. Conclusione

Più event_path per lo stesso evento

Con più versioni pubblicate e schermate leggermente diverse, lo stesso evento può avere più valori event_path.

In pratica questa mappatura richiede la collaborazione lato server e può essere configurata nel backend.

Ottimizzare le analisi

La sezione Estensioni di business descriveva i parametri specifici. WKTrackingData li realizza tramite categorie.

Il tracciamento completo sprecava molta banda: tanti eventi erano inutili [(ŏ_ŏ)].

Il server può inviare una configurazione con tutti gli eventi desiderati. Si caricano solo gli event_path corrispondenti.

I parametri di business possono anche essere ottenuti senza WKTrackingData, usando direttamente kvc.

Per esempio:

key_path = "viewController.banner.url"

Se hai letto fin qui, apri WKTrackingData e lascia una stella. Per domande, apri una issue su GitHub o commenta qui sotto!