מעקב אחר אירועים בצד הלקוח מתחלק באופן כללי לשלוש קטגוריות:

  • מעקב בקוד
  • מעקב חזותי
  • מעקב אוטומטי

המאמר מציג בעיקר את הרעיונות ואת תהליך המימוש של מעקב אוטומטי.

1. מעקב בקוד

מעקב בקוד פירושו קריאה לממשק בכל נקודה שרוצים לעקוב אחריה והעלאה ישירה של נתוני האירוע. רוב שירותי הניתוח החיצוניים מספקים api למעקב בקוד, וזה נוח.

אבל נוצרת בעיה: קוד המעקב מפוזר בהיגיון העסקי וצמוד אליו. כשהמסכים או הזרימות משתנים, לא ברור מה לעשות באירועים ישנים או אם נדרשים חדשים. ככל שמספר האירועים גדל, תחזוקת הקוד המפוזר הופכת לסיוט. מאקרואים ומחלקות מפעל יכולים לפשט את הקוד, אך אינם משנים זאת מיסודו.

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

2. מעקב חזותי

מעקב חזותי מגדיר נקודות איסוף בכלי חזותי. הלקוח מנתח אוטומטית את התצורה ומדווח אירועים, וכך מתקבל ״מעקב ללא סימנים״. פתרון קוד פתוח מייצג הוא Mixpanel.

3. מעקב אוטומטי

מעקב אוטומטי, המכונה גם מעקב מלא, אינו באמת ללא מכשור. הוא אוסף ומדווח את כל האירועים ומשאיר לשרת לסנן את הנתונים השימושיים.

בשיפור נוסף השרת יכול לשלוח קובץ תצורה, כך שהלקוח יסנן את האירועים בעצמו.

למען מדדי הביצוע, המצאתי מחדש את הגלגל סביב הרעיון ובניתי את 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, המאפיין indexSelect של UISegmentedControl, וכן UIStepper ואינטראקציות cell ב-UITableView או UICollectionView.

לשם כך 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 קוראת למתודה tableView:didSelectRowAtIndexPath: של ה-delegate class רק לאחר הפעלת המתודה שלה _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 או השאירו תגובה!