การติดตามเหตุการณ์ฝั่งไคลเอนต์แบ่งกว้าง ๆ ได้สามประเภท:
- การติดตามด้วยโค้ด
- การติดตามแบบมองเห็น
- การติดตามอัตโนมัติ
บทความนี้จะแนะนำแนวคิดและกระบวนการใช้งานเบื้องหลัง การติดตามอัตโนมัติ เป็นหลัก
1. การติดตามด้วยโค้ด
การติดตามด้วยโค้ด หมายถึงการเรียกอินเทอร์เฟซ ณ ทุกจุดที่ต้องการติดตาม แล้วอัปโหลดข้อมูลเหตุการณ์โดยตรง บริการวิเคราะห์ของบุคคลที่สามส่วนใหญ่มี api สำหรับติดตามด้วยโค้ดซึ่งใช้งานสะดวก
แต่วิธีนี้ทำให้เกิดปัญหา: โค้ดติดตามกระจัดกระจายอยู่ทั่วตรรกะธุรกิจและผูกกันแน่น เมื่อหน้าจอหรือขั้นตอนธุรกิจเปลี่ยน ก็ไม่ชัดว่าจะทำอย่างไรกับเหตุการณ์เดิมหรือต้องมีเหตุการณ์ใหม่หรือไม่ เมื่อจำนวนเหตุการณ์เพิ่ม การดูแลโค้ดติดตามที่กระจัดกระจายกลายเป็นฝันร้าย มาโครและคลาสโรงงานช่วยให้โค้ดง่ายขึ้นได้ แต่ไม่เปลี่ยนปัญหาพื้นฐาน
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 埋点
APE(kEvent4001);
}
2. การติดตามแบบมองเห็น
การติดตามแบบมองเห็นตั้งค่าจุดเก็บข้อมูลผ่านเครื่องมือแบบภาพ ไคลเอนต์จะแปลการตั้งค่าและรายงานเหตุการณ์โดยอัตโนมัติ ทำให้เกิดสิ่งที่เรียกว่า “การติดตามแบบไร้ร่องรอย” ตัวอย่างโอเพนซอร์สที่เป็นตัวแทนคือ Mixpanel นั่นเอง
3. การติดตามอัตโนมัติ
การติดตามอัตโนมัติหรือการติดตามทุกเหตุการณ์ไม่ได้หมายความว่าไม่มีการฝังการวัดจริง ๆ แต่เก็บและรายงานเหตุการณ์ทั้งหมด แล้วปล่อยให้เซิร์ฟเวอร์กรองข้อมูลที่มีประโยชน์
เพื่อปรับปรุงต่อ เซิร์ฟเวอร์ส่งไฟล์ตั้งค่าให้ไคลเอนต์กรองเหตุการณ์เองได้
เพื่อ KPI ผมจึงสร้างวงล้อใหม่จากแนวคิดนี้และทำ WKTrackingData ขึ้นมา
ที่นี่ผมบันทึกปัญหาและแนวคิดบางส่วนจากการทำ WKTrackingData ไว้
แนวคิดการใช้งานเรียบง่าย โค้ดจัดเป็นดังนี้:

ในส่วนนี้ 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::

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

หาก 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 หรือแสดงความคิดเห็นด้านล่าง!