การติดตามเหตุการณ์ฝั่งไคลเอนต์แบ่งกว้าง ๆ ได้สามประเภท:

  • การติดตามด้วยโค้ด
  • การติดตามแบบมองเห็น
  • การติดตามอัตโนมัติ

บทความนี้จะแนะนำแนวคิดและกระบวนการใช้งานเบื้องหลัง การติดตามอัตโนมัติ เป็นหลัก

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 ที่กระตุ้นเหตุการณ์ แนวคิดหลักคือไล่ตามสายโซ่ responder แล้วสร้าง path ของสายโซ่นั้น

ผลลัพธ์คือ path ของ responder ที่มีลักษณะดังนี้:

"event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
ระบุเหตุการณ์ไม่ให้ซ้ำ

ในบางสถานการณ์ทางธุรกิจ button หรือตัวควบคุมเดียวกันแทนเหตุการณ์ธุรกิจต่างกันเมื่อคุณสมบัติเปลี่ยน

ตัวอย่างเช่น button บนหน้าแรกอาจแสดง แตะเพื่อเข้าสู่ระบบ เมื่อยังไม่ลงชื่อเข้าใช้ แล้วแสดง ยืนยันตัวตน หลังเข้าสู่ระบบหากยังยืนยันตัวตนไม่ครบ

สำหรับ button เดียวกัน โครงสร้างมุมมองไม่เปลี่ยน ดังนั้น path ของ responder ที่สร้างขึ้นจึงเหมือนเดิม

กรณีคล้ายกันได้แก่สถานะเปิด/ปิดของ 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 เองก็ยังไม่ถูกเรียก

ขั้นแรก ลองดู call stack ปกติของ tableView:didSelectRowAtIndexPath::

call stack จากตัวดีบักต้นฉบับสำหรับการเลือกแถวในตาราง

ตรงนี้ 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: ก่อน

ตั้ง breakpoint ที่ _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: แล้วตรวจ assembly:

มุมมอง assembly ต้นฉบับที่แสดงแขนงของเมธอด delegate

หาก delegate class ทำ tableView:didSelectRowAtIndexPath: ไว้ ทุกอย่างจะทำงานและการเรียก tableView:didSelectRowAtIndexPath: จะสำเร็จตามปกติ

แต่ถ้า delegate class ไม่ได้ทำ tableView:didSelectRowAtIndexPath: แม้เพิ่ม tableView:didSelectRowAtIndexPath: ด้วย implementation เอง ระบบก็ไม่พยายามตอบสนองต่อ tableView:didSelectRowAtIndexPath: อีกต่อไป

เมื่อตาม assembly จะเห็นว่าทำ jump ตรงไปยังคำสั่งอื่น

WKTrackingData ใช้วิธีอื่น โดยสลับ [UITableView _userSelectRowAtPendingSelectionIndexPath:] โดยตรง และใช้วิธีคล้ายกันกับ UICollectionView ทำให้ UITableView และ UICollectionView ติดตามการโต้ตอบได้แม้ออบเจ็กต์ delegate จะไม่ได้ทำเมธอดเหตุการณ์ cell ที่เกี่ยวข้อง

4. ส่งท้าย

หลายค่า event_path สำหรับเหตุการณ์เดียว

ลองนึกถึงหลายรุ่นที่เผยแพร่แล้วซึ่งหน้าจอแตกต่างกันเล็กน้อย เหตุการณ์เดียวกันอาจมีค่า event_path หลายค่า

ในการใช้งานจริง การจับคู่นี้ต้องอาศัยเพื่อนร่วมงานฝั่งเซิร์ฟเวอร์ และทำให้กำหนดค่าในระบบเบื้องหลังได้

ปรับปรุงการวิเคราะห์เหตุการณ์

หัวข้อ ส่วนขยายเฉพาะธุรกิจ อธิบายการเพิ่มพารามิเตอร์ธุรกิจ WKTrackingData ทำสิ่งนี้ผ่าน categories

ในการใช้งานจริง การติดตามทุกเหตุการณ์สิ้นเปลืองแบนด์วิดท์มาก เหตุการณ์จำนวนมากไม่จำเป็น [(ŏ_ŏ)]

การปรับปรุงต่อคือให้เซิร์ฟเวอร์ส่งไฟล์ตั้งค่าที่ระบุทุกเหตุการณ์ที่ต้องการรับรายงาน ไคลเอนต์จะอัปโหลดเฉพาะ event_path ที่ตรงกับการตั้งค่า

ขั้นนี้ดึงพารามิเตอร์ธุรกิจได้โดยไม่ต้องใช้ WKTrackingData โดยใช้ kvc โดยตรงแทน

ตัวอย่าง:

key_path = "viewController.banner.url"

ถ้าคุณอ่านมาถึงตรงนี้ เปิด WKTrackingData แล้วให้ดาวสักดวง หากมีคำถาม ให้เปิด issue บน GitHub หรือแสดงความคิดเห็นด้านล่าง!