In the era of mobile Internet, user behavior data is very important for every company and enterprise. Important to what extent, the user in this page stay how long, click what button, browse what content, what mobile phone, what network environment, what version of the App and so on need to be clear. Many business results of some big factories are based on the user’s operation behavior after the recommendation of the secondary transformation. On the other hand, it is an auxiliary means to help developers analyze online problems.

So given these demands, how can technicians meet these needs? It leads to a technical point – “buried point”

0x01. Burial means

There are three mainstream schemes for code burial in the industry: code manual burial, visual burial and non-trace burial. Briefly talk about these several burial schemes.

  • Code manual burial point: according to business requirements (operation, product, development perspective) in the need to manually call the burial point interface, upload the burial point data.
  • Visualization of buried point: Through the visualization configuration tool to complete the node collection, the front-end automatically parse the configuration and report the buried point data, so as to realize the visualization of “non-trace buried point”
  • Non-trace burial point: through technical means, complete the statistical upload of user behavior data without difference. Later data analysis and processing through technical means to select appropriate data for statistical analysis.

0x02. Technology selection

1. Manual burying of code

In the case of this scheme, if the burial point is needed, the relevant code of the burial point should be written in the engineering code. Because of the intrusion into the business code and the contamination of the business code, the obvious disadvantages are the high cost of burial and the violation of the single principle.

Example 1: If you need to know the information about when the user clicks the “buy button” (phone model, App version, page path, stay time, action, etc.), then you need to write the buried point statistics code in the button click event. The obvious downside of this is that there are more buried bits of code on top of the previous business logic code. Because the buried point code is scattered, the workload of the buried point is very large, the maintenance cost of the code is high, and the refactoring is very headache.

Example 2: If the App adopts Hybrid architecture, when the first version of the App is released, the key business logic statistics of H5 is defined by the bridge of the key logic defined by Native (for example, H5 has activated the sharing function of Native, then there is a sharing buried point event). If one day a sweep function is added and the sweep burial bridge is not defined, then when the H5 page changes, the Native burial code is not updated, and the business of the changed H5 is not accurately counted.

Advantages: Less product and operation workload, related business scenarios can be restored according to the business mapping table, and fine data does not need a lot of processing and processing

Disadvantages: large development workload, early need and operation, product designated good business identification, so that the product and operation for statistical analysis of data

2. Visualization of burial point

The emergence of visual burying point is to solve the problem that the process of code burying point is complex, the cost is high, and the newly developed page (H5, or the JSON delivered by the server to generate the corresponding page) cannot have the ability of burying point in time

In the “buried point editing mode”, the front end configures and binds the path of key business modules to the front end in a “visual” way, which uniquely identifies the xpath process to the view.

Each time the user operates on a control, an xpath string is generated, and the xpath string (view) is uniquely positioned in the front-end system through the interface. Take iOS as an example, App name, controller name, layer on layer view, serial number of view of the same type: “GoodCell. 21. RetailTableView. GoodsViewController. * baoApp”) to the real business module (” treasure App – mall controller – distribute goods list – a commodities by clicking the “21) the mapping relationship uploaded to the server. What xpath is will be explained later.

Then the operation App generates the corresponding xpath and buried point data (the developer obtains the key data from the server through technical means to plug the UI control on the front end. For the iOS side, the accessibilityIdentifier property of UIView can set the burial point data we get from the server to the server.

Advantages: The amount of data is relatively accurate and the cost of later data analysis is low

Disadvantages: early control unique identification, positioning need additional development; The development cost of visual platform is high; Additional requirements can be difficult to analyze

3. No trace burial point

Technology to indiscriminately record user behavior on the front page. You can obtain PV, UV, IP, Action, and Time information correctly.

Disadvantages: High cost of technical products for developing basic statistical information in the early stage; large amount of data for data analysis in the later stage; high cost of analysis (great pressure on traditional relational database with large amount of data)

Advantages: low workload for developers, comprehensive data, no omissions, on-demand product and operation analysis, support for dynamic page statistical analysis

4. How to choose

Combined with the above advantages and disadvantages, we choose the non-trace burial point + visual burial point combination of technical solutions.

How can I put it? For the key business development, after going live, through the visualization solution (similar to an interface, think of Dreamweaver, you can drag controls on the interface, simple editing can generate corresponding HTML code) click the binding corresponding relationship to the server.

So what is this correspondence? We need to only locate a front-end element, so the idea comes to mind is that whether Native or Web front-end, control or element is a tree hierarchy, DOM tree or UI tree, so we locate this element through technical means. Taking Native iOS as an example, If the product details page of the add to cart button will according to the UI hierarchy generates a unique identifier “addCartButton. GoodsViewController. GoodsView. * BaoApp”. But when the user uses the App, what he uploads is the MD5 of this string to the server.

This is done for two reasons: it’s not good for the server database to store this long string of things; It’s not good to see the plain text when the buried data is hijacked. So MD5 uploads again.

0x03. Just do it

1. Data collection

The implementation scheme is based on the following key indicators:

  • Existing code changes less, try not to invade business code to intercept system events
  • Full amount collected
  • How do I uniquely identify a control element

2. Intercept system events without intruding into business codes

Take iOS for example. We think of AOP (Aspect Oriented Programming). In Objective-C we can use the Runtime feature to hook the corresponding function using Method Swizzling

So to hook all of our classes easily, we can add a Category to NSObject, and we’ll call it NSObject+MethodSwizzling

#pragma mark - public Method + (void)lbp_swizzleMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { class_swizzleInstanceMethod(self, originalSelector, swizzledSelector); } + (void)lbp_swizzleClassMethod:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector :(SEL)swizzledSelector :(SEL)swizzledSelector :(SEL)swizzledSelector; That is, the class method is equivalent to the instance method of the metaclass, so you just need to pass in the metaclass, and the rest of the logic is the same as the interactive instance method. Class class2 = object_getClass(self); class_swizzleInstanceMethod(class2, originalSelector, swizzledSelector); } #pragma mark - private method void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL) { /* Class class = [self class]; OriginalMethod = class_getInstanceMethod(class, originalSelector); SwizzledMethod = class_getInstanceMethod(class, swizzledSelector); // First try adding IMP to SEL, So in case the source SEL doesn't implement IMP BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); If (didAddMethod) {// Added successfully: Source SEL does not implement IMP, Replace the IMP of the source SEL with the IMP of the exchange SEL class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {// If the source SEL has an IMP, method_exchangeImplementations(originalMethod, swizzledMethod); } */ Method originMethod = class_getInstanceMethod(class, originalSEL); Method replaceMethod = class_getInstanceMethod(class, replacementSEL); if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod))) { class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); }else { method_exchangeImplementations(originMethod, replaceMethod); }}Copy the code

3. Collect in full

We think of hook AppDelegate proxy methods, UIViewController lifecycle methods, button click events, gesture events, click-callback methods for various system controls, application state switching, and so on.

action The event
Switching the App status Add a category to the Appdelegate, hook lifecycle
UIViewController lifecycle function Add class, hook lifecycle to UIViewController
UIButton and so on UIButton adds category, hook click event
UICollectionView, UITableView, etc Add category and hook click event in the corresponding Cell
Gesture events UITapGestureRecognizer, UIControl, and UIResponder Corresponding System Events

Taking the opening time of the statistical page and the opening and closing requirements of the statistical page as an example, we hook UIViewController

static char *lbp_viewController_open_time = "lbp_viewController_open_time"; static char *lbp_viewController_close_time = "lbp_viewController_close_time"; @implementation UIViewController (lbpka) // Add dispatch_once to the load method to prevent manually calling the load method. + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @autoreleasepool { [[self class] lbp_swizzleMethod:@selector(viewWillAppear:) swizzledSelector:@selector(lbp_viewWillAppear:)]; [[self class] lbp_swizzleMethod:@selector(viewWillDisappear:) swizzledSelector:@selector(lbp_viewWillDisappear:)]; }}); } #pragma mark - add prop - (void)setOpenTime:(NSDate *)openTime { objc_setAssociatedObject(self,&lbp_viewController_open_time, openTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDate *)getOpenTime{ return objc_getAssociatedObject(self, &lbp_viewController_open_time); } - (void)setCloseTime:(NSDate *)closeTime { objc_setAssociatedObject(self,&lbp_viewController_close_time, closeTime, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSDate *)getCloseTime{ return objc_getAssociatedObject(self, &lbp_viewController_close_time); } - (void)lbp_viewWillAppear:(BOOL)animated { NSString *className = NSStringFromClass([self class]); NSString *refer = [NSString string]; If ([self getPageUrl:className]) {// setOpenTime [self setOpenTime:[NSDate] dateWithTimeIntervalSinceNow:0]]; If (self. NavigationController) {if (self. NavigationController. ViewControllers. Count > = 2) {/ / get the current vc a vc on the stack UIViewController *referVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2]; refer = [self getPageUrl:NSStringFromClass([referVC class])]; } } if (! refer || refer.length == 0) { refer = @"unknown"; } [UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer]; } [self lbp_viewWillAppear:animated]; } - (void)lbp_viewWillDisappear:(BOOL)animated { NSString *className = NSStringFromClass([self class]); if ([self getPageUrl:className]) { [self setCloseTime:[NSDate dateWithTimeIntervalSinceNow:0]]; [UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; } [self lbp_viewWillDisappear:animated]; } #pragma mark - private method - (NSString *)p_calculationTimeSpend { if (! [self getOpenTime] || ! [self getCloseTime]) { return @"unknown"; } NSTimeInterval aTimer = [[self getCloseTime] timeIntervalSinceDate:[self getOpenTime]]; int hour = (int)(aTimer/3600); int minute = (int)(aTimer - hour*3600)/60; int second = aTimer - hour*3600 - minute*60; return [NSString stringWithFormat:@"%d",second]; } @endCopy the code

4. How do I uniquely identify a control element

Xpath is the unique identifier for the mobile side to define the actionable region. Since you want a string to identify manipulable controls in the front-end system, xpath requires two metrics:

  • Uniqueness: No different controls in the same system have the same xpath
  • Stability: The xPaths of the same controls on the same page need to be consistent across versions of the system without changing the structure of the page.

When we think of systems like Naive and H5 pages, we draw and render with a tree structure. So we take all the key points between the current View and the root element of the system (UIViewController, UIView, UIView container (UITableView, UICollectionView, etc.), UIButton… Concatenated to uniquely locate the control element.

For precise positioning of element nodes, see the following figure

Suppose a UIView has three child views in the order of label, Button1 and Button2, then the depth is 0, 1 and 2 in sequence. Suppose the user does something to remove Label1 from the parent view. At this time, UIView only has two child views: Button1 and Button2, and the depth changes to 0 and 1.

You can see that just because one of the subviews changes, the depth of the other subviews changes. Therefore, it is necessary to pay attention to the design. When adding/removing a view, the influence on the depth of the existing view should be reduced as far as possible. The calculation method of node depth is adjusted to adopt the index values of all sub-views of the same type as the current view in its parent view.

Let’s look at the example above again. The original depth of label, Button1, button2 is 0, 0, 1. After the label is removed, the depth of Button1 and Button2 is 0 and 1 respectively. It can be seen that in this example, the removal of label has no impact on the depth of Button1 and Button2, and this adjusted calculation method enhances the anti-interference of xpath to a certain extent.

In addition, the adjusted depth is calculated in a way that depends on the type of the node, so you must put the node name in the viewPath, not just for readability.

When identifying the hierarchy of control elements, you need to know “the index values of all subviews of the same type as the current view in its parent view.” Look at the figure above. Uniqueness is not guaranteed if it is not of the same type.

5. Unique positioning problem of views of the same type

There is a problem, for example, we click on the element is a UITableViewCell, although it can locate to similar to the labeled xxApp. GoodsViewController. GoodsTableView. GoodsCell, Cell has many of the same type, So there’s no way to locate the particular Cell that was clicked by just using this string.

Of course there are solutions.

  • Finds the index of the current element in the parent element of the same type. The child elements of the parent element of the current element are traversed according to the current element. If the same element appears, it is necessary to determine which element of the current element is in the hierarchy

    To iterate over all the child views of the parent view of the current control element, if there are controls of the same type as the current control element, then the position of the current control element in the same type of control element needs to be determined, then can be uniquely located. For example: GoodsCell – 3. GoodsTableView. GoodsViewController. XxApp

    //UIResponder class - (NSString *)lbp_identifierKa {// if (self.xq_identifier_ka == nil) {if ([self isKindOfClass:[UIView class]]) { UIView *view = (id)self; NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath]; NSMutableString *str = [NSMutableString string]; / / special add and subtract Because with SPM but want to add and subtract need to bring TreeNode nsstrings * className = [nsstrings stringWithUTF8String: object_getClassName (view)]; if (! view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) { [str appendString:sameViewTreeNode]; [str appendString:@","]; } while (view.nextResponder) { [str appendFormat:@"%@,", NSStringFromClass(view.class)]; if ([view.class isSubclassOfClass:[UIViewController class]]) { break; } view = (id)view.nextResponder; } self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]]; // self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str]; } // } return self.xq_identifier_ka; } / / UIView classification - (nsstrings *) obtainSameSuperViewSameClassViewTreeIndexPat classStr = {nsstrings * NSStringFromClass ([the self  class]); //UITableView special superView (UITableViewContentView) //UICollectionViewCell shouldUseSuperView = ([classStr isEqualToString:@"UITableViewCellContentView"]) || ([[self.superview class] isKindOfClass:[UITableViewCell class]])|| ([[self.superview class] isKindOfClass:[UICollectionViewCell class]]); if (shouldUseSuperView) { return [self obtainIndexPathByView:self.superview]; }else { return [self obtainIndexPathByView:self]; } } - (NSString *)obtainIndexPathByView:(UIView *)view { NSInteger viewTreeNodeDepth = NSIntegerMin; NSInteger sameViewTreeNodeDepth = NSIntegerMin; NSString *classStr = NSStringFromClass([view class]); NSMutableArray *sameClassArr = [[NSMutableArray alloc]init]; For (NSInteger index =0; NSInteger index =0; index < view.superview.subviews.count; Index + +) {/ / if same type ([classStr isEqualToString: NSStringFromClass ([view. Superview. Subviews class [index]])]) { [sameClassArr addObject:view.superview.subviews[index]]; } if (view == view.superview.subviews[index]) { viewTreeNodeDepth = index; break; For (NSInteger index =0; NSInteger index =0; NSInteger index =0; index < sameClassArr.count; index ++) { if (view == sameClassArr[index]) { sameViewTreeNodeDepth = index; break; } } return [NSString stringWithFormat:@"%ld",sameViewTreeNodeDepth]; }Copy the code

6. Same type of View, but different meaning of clicking. How to uniquely identify?

Question 5 shows that there are several different views on the same interface, and they are of the same type (CycleBannerView, but the data source is different, so when the data source length is greater than 1, it will be cast round. The following will show UIPageControl. If there is one data source, then UIPageControl is not rotated and shown. Case 6 is the same type of View, but the meaning of a click is different depending on what is displayed. The operation needs to know which one the user is clicking on. As shown in the figure below, “Snap up immediately” and “share to earn commission” are the same type of View, but the click has different meanings, so we need to identify them uniquely. The previous method of “viewPath with the same type of view index” method is not unique identification. So I came up with a solution to add a class to NSObject, and add a protocol to the class. Let the view that needs to be reused but needs to be uniquely identified implement the protocol method, because it is the protocol added to the NSObject class, so the view does not need to specify compliance.

Key steps:

  • Add a Category for NSObject. Declare a uniquely identified protocol in a category

  • Take out the unique identifier of the current view (view call protocol method) where the viewPath was generated. Then splice the viewPath that I took out before

//NSObject+UniqueIdentify.h #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class NSObject; @protocol UniqueIdentify<NSObject> @optional - (NSString *)setUniqueIdentifier; @end @interface NSObject (UniqueIdentify)<UniqueIdentify> @end NS_ASSUME_NONNULL_END //NSObject+UniqueIdentify.m #import  "NSObject+UniqueIdentify.h" @implementation NSObject (UniqueIdentify) @endCopy the code
//MallTGoodTagView.h extern NSString * _Nonnull const ImmediateyPurchase; extern NSString * _Nonnull const ShareToAward; M NSString *const ImmediateyPurchase = @" ImmediateyPurchase "; NSString *const ShareToAward = @" ShareToAward "; - (NSString *)setUniqueIdentifier { if (self.tagString) { return self.tagString; } else { return NSStringFromClass([self class]); }}Copy the code
//UIResponder Category generate viewPath - (NSString *)lbp_identifierKa {// if (self.xq_identifier_ka == nil) {if ([self isKindOfClass:[UIView class]]) { UIView *view = (id)self; NSString *sameViewTreeNode = [view obtainSameSuperViewSameClassViewTreeIndexPath]; NSMutableString *str = [NSMutableString string]; / / special add and subtract Because with SPM but want to add and subtract need to bring TreeNode nsstrings * className = [nsstrings stringWithUTF8String: object_getClassName (view)]; if (! view.accessibilityIdentifier || [className isEqualToString:@"lbpButton"]) { [str appendString:sameViewTreeNode]; [str appendString:@","]; } while (view.nextResponder) { if ([view respondsToSelector:@selector(setUniqueIdentifier)]) { NSString *unqiueIdentifier = [view setUniqueIdentifier]; if (unqiueIdentifier) { [str appendFormat:@"%@,", unqiueIdentifier]; } }00 [str appendFormat:@"%@,", NSStringFromClass(view.class)]; if ([view.class isSubclassOfClass:[UIViewController class]]) { break; } view = (id)view.nextResponder; } self.xq_identifier_ka = [self md5String:[NSString stringWithFormat:@"%@",str]]; // self.xq_identifier_ka = [NSString stringWithFormat:@"%@",str]; } // } return self.xq_identifier_ka; }Copy the code

7. How to process the data

A. How do I process service data

Use the accessibilityIdentifier provided by the system. Officially, it is a string that identifies user interface elements

/ *

A string that identifies the user interface element.

default == nil

* /

@property(nullable, nonatomic, copy) NSString *accessibilityIdentifier NS_AVAILABLE_IOS(5_0);

The server delivers a unique IDENTIFIER

Interface that contains the unique identification of the current element. For example, in the UITableView interface to request the interface to get the data, then in the obtained data source will have a field dedicated to store dynamic frequently changing business data.

cell.accessibilityIdentifier = [[[SDGGoodsCategoryServices sharedInstance].categories[indexPath.section] children][indexPath.row].spmContent yy_modelToJSONString];
Copy the code

B. Basic data

In design, there are two POD libraries: TriggerKit (used to hook all events required by the opportunity, page dwell time, page logo, view logo), and Appmonitor (used to provide basic data, buried data maintenance, upload mechanism). So there is a class called UserTrackDataCenter in Appmonitor that provides basic data (system version, operating system, location, network, and so on).

Some methods are exposed to hand over the buried data to Appmonitor to maintain the buried data and to achieve the appropriate “mechanism” to upload the buried data to the server.

+ (void)clickEventUuid:(NSString *)uuid otherParam:(NSDictionary *)otherParam spmContent:(NSDictionary *)spmContent { if  (uuid) { NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:otherParam]; params[SDGStatisticEventtagKey] = @"clickMonitorV1"; NSMutableDictionary *valueDict = [[NSMutableDictionary alloc] initWithDictionary:spmContent]; valueDict[@"xpath"] = uuid? : @ ""; params[SDGStatisticEventtagValue] = valueDict? : @ {}; [[AppMonotior shareInstance] traceEvent:[AMStatisticEvent eventWithInfo:params]]; }}Copy the code

8. Data reporting

Data has been collected through the above methods, so how to timely and efficient upload to the back end, for operation analysis, processing?

During the operation of the App, users will click on a large amount of data, and real-time upload will have a low utilization rate for the network. Therefore, a mechanism should be considered to control the upload of buried data generated by users.

Here’s the idea. An interface is exposed to the outside world for storing the generated data in the data center. The data generated by the user is stored in AppMonitor’s memory, with a threshold of memoryEventMax = 50. If the value reaches the threshold of memoryEventMax, the data is written to the file system and saved as a ZIP file. And then it’s uploaded to the burial point system. If the critical value is not reached but there is some App state switching, then you need to save the data to persistence in time. Next time I open the App, I will read whether there is any unuploaded data from the local persistent place. If there is, I will upload the log information. After the success, I will delete the local log compression package.

The switching policy of App status is as follows:

  • DidFinishLaunchWithOptions: memory log information written to the hard disk
  • Upload didBecomeActive:
  • WillTerimate: Memory logs are written to disks
  • DidEnterBackground: Memory log information is written to the disk

The following code is the App buried point data saved and uploaded

// Write the App log information to memory. - (void)joinEvent:(NSDictionary *)dictionary {if (dictionary) { NSDictionary *tmp = [self createDicWithEvent:dictionary]; if (! s_memoryArray) { s_memoryArray = [NSMutableArray array]; } [s_memoryArray addObject:tmp]; if ([s_memoryArray count] >= s_flushNum) { [self writeEventLogsInFilesCompletion:^{ [self startUploadLogFile]; }]; }}} // synchronized (self) {// synchronized (self) {// synchronized (self) {// synchronized (self) { if (event && event.userInfo) { [self joinEvent:event.userInfo]; }}} // Write the data in memory to a file, Persistent storage - (void) writeEventLogsInFilesCompletion (void (^) (void)) completionBlock {NSArray * TMP = nil; @synchronized (self) { tmp = s_memoryArray; s_memoryArray = nil; } if (tmp) { __weak typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSString *jsonFilePath = [weakSelf createTraceJsonFile]; if ([weakSelf writeArr:tmp toFilePath:jsonFilePath]) { NSString *zipedFilePath = [weakSelf zipJsonFile:jsonFilePath]; if (zipedFilePath) { [AppMonotior clearCacheFile:jsonFilePath]; if (completionBlock) { completionBlock(); }}}}); } // Upload each zip file from the App buried statistics zip folder to the server, - (void)startUploadLogFile {NSArray *fList = [self listFilesAtPath:[self eventJsonPath]]; - (void)startUploadLogFile {NSArray *fList = [self listFilesAtPath:[self eventJsonPath]]; if (! fList || [fList count] == 0) { return; } [fList enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if (![obj hasSuffix:@".zip"]) { return; } NSString *zipedPath = obj; unsigned long long fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:zipedPath error:nil] fileSize]; if (!fileSize || fileSize < 1) { Return;} / / call interface to upload buried point data [self uploadZipFileWithPath: zipedPath completion: ^ (nsstrings * completionResult) {if ([completionResult isEqual:@"OK"]) { [AppMonotior clearCacheFile:zipedPath]; } }]; }]; }Copy the code

The time to use is to call the statistics page to upload data when hook the system event

//UIViewController [UserTrackDataCenter openPage:[self getPageUrl:className] fromPage:refer]; [UserTrackDataCenter leavePage:[self getPageUrl:className] spendTime:[self p_calculationTimeSpend]]; // The page disappearsCopy the code

Summarize the key steps:

  1. Hook system events (UIResponder, UITableView, UICollectionView agent events, UIControl events, UITapGestureRecognizers), hook application, and controller lifecycle. Add additional monitoring code before doing the original logic
  2. To click on the element according to generate the corresponding view tree unique identifier (addCartButton. GoodsView. GoodsViewController) md5 value
  3. After the business development is completed, enter the edit mode of the buried point, bind MD5 with the critical events of the critical pages (key modules of operation and product statistics: App level, business module, key pages, and key operations). Such as addCartButton. GoodsView. GoodsViewController. TbApp corresponding tbApp – mall module – commodity details page – add to cart function.
  4. Store the required data
  5. Design mechanisms to upload data at the right time

An example is given to illustrate a complete reporting process for burial sites

The buried point module is divided into two POD component libraries. TriggerKit is responsible for intercepting system events and obtaining buried point data. Appmonitor is responsible for collecting buried data, local persistence or memory storage, and uploading buried data when the time is right.

  1. Obtain the data through the interface and bind the buried point data to the accessibilityIdentifier property of the corresponding view

  2. Hook the system event, click “Get view”, and get the value of the accessibilityIdentifier attribute

  3. The data is sent to the data center, and the data center processes the data (buried data combined with App basic information, the UserTrackDataCenter object in the figure). Store the data in memory or local, and upload it at the right time