当前位置:首页 > 分类69 > 正文

iOS可视化埋点方案

摘要: iOS可视化埋点方案iOS可视化埋点方案前言随着公司业务的发展,数据的重要性日益体现出来。数据埋点的准确和全面性显得尤为重要。通...
iOS可视化埋点方案

iOS可视化埋点方案

前言
随着公司业务的发展,数据的重要性日益体现出来。 数据埋点的准确和全面性显得尤为重要。通过精准和详细的数据,后面的分析才有意义。随着业务的不断变化,动态化埋点也越来越重要。
三大埋点方式为了解决这些问题, 很多公司都提出自己的解决方案, 各中解决方案中,大体分为以下三种:
1、代码埋点由开发人员在触发事件的具体方法里,植入多行代码把需要上传的参数上报至服务端。
2、可视化埋点根据标识来识别每一个事件, 针对指定的事件进行取参埋点。而事件的标识与参数信息都写在配置表中,通过动态下发配置表来实现埋点统计。
3、无埋点无埋点,也称为“无痕埋点”,无埋点还有另一种叫法:全埋点。前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。通过定期上传记录文件,配合文件解析,解析出来我们想要的数据, 并生成可视化报告供专业人员分析 ,因此称为“无埋点”统计。
无埋点方案目前已经有神策埋点实现,另外考虑到“无埋点”的方案成本较高,并且后期解析也比较复杂,加上view_path的不确定性,所以本文重点介绍“可视化埋点”的简单实现方式。
可视化埋点
可视化埋点并非完全抛弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点。理论上可视化的埋点也应该封装在埋点的SDK层,但是由于历史原因,智联的埋点SDK只封装了缓存数据和上报数据这一层,所以我们可以在客户端层面,增加一层可视化埋点SDK。
大体架构如下:
WX20181212-111410@2x.png从业务架构上来看,可视化埋点主要对页面Out、In(PV)、按钮等事件点击(Action)、列表的滑动、点击等(List)、手势动作(Gesture)进行埋点,就能覆盖90%以上统计事件。
要解决的问题
代码埋点可以解决所有的自定义埋点,深入程度也是最高的,但是他有天然劣势,就是每出现一个新的页面,新的需求,都需要开发人员植入多行代码把需要上传的参数上报至服务端,开发成本高,效率低下,经常出现业务开发需求一星期,埋点埋两星期的情况。
埋点需要解决的问题有:
1、重复埋点问题如何才能动态埋点,不需要每次需求都要特意去埋一次点,特别是那些页面的进出、停留时长等的埋点,重复的埋点徒增开发时间。
2、pageid(pagecode)不同而且无规律问题虽然可视化埋点可以利用Hook原理,来解决这个问题,但由于统计的要求,每个页面都自带不同的pageid或者pagecode,这样一来无法利用父类的方式去一次性埋点,因为即使通过继承的方式,也无法做到每个子类都有不同的pageid。即使利用Hook原理,去Hook每一个页面的Appear和DisAppear方法,也无法对这些不同的页面注入不同的Pagecode,这样唯一标识又构成了瓶颈。
3、动态埋点问题即使进行了代码埋点,每个版本都进行埋点,但是却无法对已经上线的版本进行埋点,假如上线后有些埋点忘记埋了,就只能等到下个版本才能进行埋点的添加,是否有办法做到动态的下发配置来对线上版本增加埋点,而不需要发版呢?
4、页面OI先后顺序问题代码埋点的方式,如果想知道C页面是从A页面进来的,还是从B页面还是先A再B最后在进入C的,就得对每个页面进行一个传值,而且这样做还有一个弊端就是可能不知道用户这个C页面可能是这样的:A->B->D->B->A->B->C,普通的代码埋点只能知道是B到C,却不了解进入C之前其实有很多前进后退页面的操作,这样对数据分析可能就会有偏差,那是否有办法做到自动记录页面进出的方案呢?
解决方案
唯一标识的组成方式主要是又 target + action 来确定, 即任何一个事件都存在一个target与action。 在此引入AOP编程,AOP(Aspect-Oriented-Programming)即面向切面编程的思想,基于 Runtime 的 Method Swizzling能力,来 hook 相应的方法,从而在hook方法中进行统一的埋点处理。例如所有的按钮被点击时,都会触发UIApplication的sendAction方法,我们hook这个方法,即可拦截所有按钮的点击事件。
WX20181212-143303@2x.png但是刚才问题2提到,只是单纯的Hook,无法解决PageCode、ActionCode如果埋入的问题,即:“事件唯一标识符“如何埋入的问题。所以在这里,我们要利用一份配置表来管理这个“事件唯一标识符“。这里主要分为两个部分 :
事件的锁定事件的锁定主要是靠 “事件唯一标识符”来锁定,而事件的唯一标识是由我们写入配置表中的。这里分为两种,本地配置表和线上下载的配置表。
埋点数据的上报。埋点数据的数据又分为两种类型: 固定数据与可变的业务数据, 而固定数据我们可以直接写到配置表中,通过唯一标识来获取。而对于业务数据,我是这么理解的:数据是有持有者的,例如我们Controller的一个属性值,又或者数据再Model的某一个层级。这么的话我们就可以通过KVC的的方式来递归获取该属性的值来取到业务数据。
整体解决方案由于业务中的事件场景是多样的,以iOS为例,在此我以UIControl(Button、Switch、TextField等都属于Control), UITablview(CollectionView与TableView基本相同,Android里对应的则是ListView),UITapGesture,UIViewController的PV统计为例,介绍一下具体思路。
UIViewController PV统计页面的统计较为简单,利用Method Swizzing hook 系统的viewDidLoad, 直接通过页面名称即可锁定页面的展示代码如下:
+ (void)load{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        SEL originalAppearSelector = @selector(viewWillAppear:);        SEL swizzingAppearSelector = @selector(analysis_viewWillAppear:);        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalAppearSelector swizzingSel:swizzingAppearSelector];                SEL originalDisappearSelector = @selector(viewWillDisappear:);        SEL swizzingDisappearSelector = @selector(analysis_viewWillDisappear:);        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDisappearSelector swizzingSel:swizzingDisappearSelector];                SEL originalDidLoadSelector = @selector(viewDidLoad);        SEL swizzingDidLoadSelector = @selector(analysis_viewDidLoad);        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSelector swizzingSel:swizzingDidLoadSelector];    });}
Hook系统ViewDidLoad的方法大致如下:
- (void)analysis_viewDidLoad{    [self analysis_viewDidLoad];        self.view.backgroundColor = [UIColor whiteColor];        //从配置表中取参数的过程 1 固定参数  2 业务参数(此处参数被target持有)    NSString *identifier = [NSString stringWithFormat:@"%@", [self class]];//    NSLog(@"identifier:%@",identifier);    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];    if (dic) {        NSString *pageid = dic[@"userDefined"][@"pageid"];        NSString *pagename = dic[@"userDefined"][@"pagename"];        NSDictionary *pagePara = dic[@"pagePara"];                __block NSMutableDictionary *uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {                        id value = [ZPMCapturePropertyTool captureVarforInstance:self withPara:obj];            if (value && key) {                [uploadDic setObject:value forKey:key];            }        }];                NSLog(@"\n 事件唯一标识为:%@ \n  pageid === %@,\n  pagename === %@,\n  pagepara === %@ \n", [self class], pageid, pagename, uploadDic);    }}
UIControl 点击统计主要通过hook sendAction:to:forEvent: 来实现, 其唯一标识符我们用 targetname/selector/tag来标记,具体代码如下:
+ (void)load{    static dispatch_once_t onceToken;    dispatch_once(&onceToken, ^{        SEL originalSelector = @selector(sendAction:to:forEvent:);        SEL swizzingSelector = @selector(analysis_sendAction:to:forEvent:);        [ZPMMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];    });}- (void)analysis_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{    [self analysis_sendAction:action to:target forEvent:event];        NSString *identifier = [NSString stringWithFormat:@"%@/%@", [target class], NSStringFromSelector(action)];    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"ACTION"] objectForKey:identifier];    if (dic) {                NSString *eventid = dic[@"userDefined"][@"eventid"];        NSString *targetname = dic[@"userDefined"][@"target"];        NSString *pageid = dic[@"userDefined"][@"pageid"];        NSString *pagename = dic[@"userDefined"][@"pagename"];        NSDictionary *pagePara = dic[@"pagePara"];        __block NSMutableDictionary *uploadDic = [NSMutableDictionary dictionaryWithCapacity:0];        [pagePara enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL *_Nonnull stop) {                        id value = [ZPMCapturePropertyTool captureVarforInstance:target withPara:obj];            if (value && key) {                [uploadDic setObject:value forKey:key];            }        }];                NSLog(@"\n event id === %@,\n  target === %@, \n  pageid === %@,\n  pagename === %@,\n pagepara === %@ \n", eventid, targetname, pageid, pagename, uploadDic);    }}
TableView (CollectionView) 的点击统计tablview的唯一标识, 我们使用 delegate.class/tableview.class/tableview.tag的组合来唯一锁定。主要是通过hook setDelegate 方法, 在设置代理的时候再去交互 didSelect(点击)、scrollViewDidScroll(滑动列表)方法来实现。具体代码先不上了。
gesture方式添加的的点击统计gesture的事件,是通过 hook initWithTarget:action:方法来实现的, 事件的唯一标识依然是target.class/actionname来锁定的。
从上面的代码可以看出,这个pageid和eventid都不是写死的,而且从一个字典里面获取值,如dic[@"userDefined"][@"pageid"],那么这个value从哪获取呢,这里就要用到配置表了。
配置表结构
配置表是一个json数据。 针对不同的场景 (UIControl , 页面PV, Tabeview, Gesture)都做了区分, 用不同的key区别。 对于 "固定参数" , 我们之间写到配置表中,而对于业务参数, 我们之间写清楚参数在业务内的名字, 以及上传时的 keyName, 参数的持有者。 通过Runtime + KVC来取值。 配置表可以是这个样子:(仅供参考)
{    "ACTION": {        "ThirdViewController/jumpSecond:": {            "userDefined": {                "eventid": "201803074|93",                "target": "",                "pageid": "234",                "pagename": "button点击,跳转至下一个页面"            },            "pagePara": {                "testKey9": {                    "propertyName": "testPara",                    "propertyPath":"",                    "containIn": "0"                }            }        },                "SecondViewController/back": {            "userDefined": {                "eventid": "201803074|965",                "target": "second",                "pageid": "235",                "pagename": "button点击,返回"            },            "pagePara": {                "testKey9": {                    "propertyName": "testPara",                    "propertyPath":"",                    "containIn": "0"                }            }        }    },        "PAGEPV": {        "HomeViewController": {            "userDefined": {                "pageid": "3156",                "pagename": "首页展示了"            },            "pagePara": {                "testKey10": {                    "propertyName": "testPara",                    "propertyPath":"",                    "containIn": "0"                }            }        },        "SecondViewController": {            "userDefined": {                "pageid": "4687",                "pagename": "SecondView页面展示"            },            "pagePara": {                "testKey0": {                    "propertyName": "age",                    "propertyPath":"",                    "containIn": "0"                },                "testKey1": {                    "propertyName": "content",                    "propertyPath":"",                    "containIn": "0"                },                "testKey2": {                    "propertyName": "propertyDic",                    "propertyPath":"",                    "containIn": "0"                },                "testKey3": {                    "propertyName": "items",                    "propertyPath":"",                    "containIn": "0"                }            }        }    },    "TABLEVIEW": {        "ViewController/UITableView/0":{            "userDefined": {                "eventid": "201803074|93",                "target": "",                "pageid": "238",                "pagename": "tableview 被点击"            },            "pagePara": {                "user_grade": {                    "propertyName": "grade",                    "propertyPath":"",                    "containIn": "1"                }            }        }    },        "GESTURE": {        "ViewController/gesture1clicked:":{            "userDefined": {                "eventid": "201803074|93",                "target": "",                "pageid": "手势1对应的id",                "pagename": "手势1对应的page name"            },            "pagePara": {                "testKey1": {                    "propertyName": "testPara",                    "propertyPath":"",                    "containIn": "0"                }                            }        },        "ViewController/gesture2clicked:":{            "userDefined": {                "eventid": "201803074|93",                "target": "",                "pageid": "手势2对应的id",                "pagename": "手势2对应的page name"            },            "pagePara": {                "testKey2": {                    "propertyName": "testPara",                    "propertyPath":"",                    "containIn": "0"                }                            }        },                "SecondViewController/gesture3clicked:":{            "userDefined": {                "eventid": "201803074|98",                "target": "",                "pageid": "gesture3clicked",                "pagename": "手势3对应的page name"            },            "pagePara": {                "user_age": {                                  }                            }        }    }}
json最外层有四个Key, 分别为 ACTION PAGEPV TABLEVIEW GESTURE, 分别对应 UIControl的点击,页面PV,tableview cell点击, Gesture 单击事件的参数。 每个key对应的value为json格式,Json中的keys, 即为唯一标识符。
标识符下的json有两个key :userDefine指的固定数据,即直接取值进行上报。而pagePara为业务参数。pagePara对应的value也是一个json, json的keys,即上报的keys,value内的json包含三个参数:propertyName为属性名字,containIn 参数只有0 ,1 两种情况,用来区分类似Tableview的Cell里面按钮的持有对象,看是要统计cell的点击事件,还是cell里面的控件的点击事件。propertyPath是属性路径,有些不同的层级有相同的属性名字,比如self.age 和 self.person.age,如果把propertyPath的值设为 person/age,取值的时候就会按照指定路径进行取值。
从配置表来看,所有的hook事件都不再是一个写死的id,而是从配置表里面拿的数据,解决了问题2提到的,由于不同的pageid导致无法Hook的瓶颈。这样的配置表有2个好处,一是可以自由配置想统计的页面,二是可以动态下发,只要配置正确,即使不发版也可以拿到线上版本的数据。
效果如下:
2018-12-12 15:04:18.373103+0800 ZPMStatisticsDemo[1435:199292]  事件唯一标识为:SecondViewController   pageid === 4687,  pagename === SecondView页面展示,  pagepara === {    testKey0 = 30;    testKey1 = "Hello World";    testKey2 =     {        key = 1;    };    testKey3 =     (        a,        b,        2    );} 
有了这个配置表,页面的In、Out,就可以通过Hook页面的ViewAppear和ViewDisAppear来拦截埋点了,减少了大量重复埋点的时间。但是我们还有一个问题,页面进出先后顺序问题。
页面进出顺序
试想一个场景,我在JD页点击投递简历按钮,可视化埋点虽然记录了点击事件,我们做分析的时候,确无法得知用户点击这个投递按钮是通过什么方式进来的,是从搜索结果页进入,还是首页推荐,还是推送进入的。如果要知道这样的进出先后,就需要进行页面的传递。如果有一个方案,自动记录页面的进出堆栈顺序,那么这个问题就迎刃而解了。
解决方案:通过Hook viewWillAppear:方法来实现,具体代码如下:
- (void)analysis_viewWillAppear:(BOOL)animated{    [self analysis_viewWillAppear:animated];        NSString *identifier = [NSString stringWithFormat:@"%@", [self class]];    NSDictionary *dic = [[[ZPMDataContainer sharedInstance].data objectForKey:@"PAGEPV"] objectForKey:identifier];    NSString *pageInfo = [NSString stringWithFormat:@"%@, %@",[self getCurrentTimes], [self class]];    if (dic) {        NSString *pageid = dic[@"userDefined"][@"pageid"];        pageInfo = [pageInfo stringByAppendingFormat:@" ,%@",pageid];    }    [[ZPM_IO_Queue sharedInstance].queueArray addObject:pageInfo];        // 把页面出现顺序保存起来    if ([ZPM_IO_Queue sharedInstance].queueArray.count > 10) { // 这里只存10个页面队列        [[ZPM_IO_Queue sharedInstance].queueArray removeObjectAtIndex:0];    }    NSLog(@"queueArray:%@",[ZPM_IO_Queue sharedInstance].queueArray);}
打印了一下日志:
2018-12-12 15:04:36.813738+0800 ZPMStatisticsDemo[1435:199292] queueArray:(    "2018-12-12 15:04:12, UINavigationController",    "2018-12-12 15:04:12, HomeViewController ,3156",    "2018-12-12 15:04:18, SecondViewController ,4687",    "2018-12-12 15:04:32, ThirdViewController",    "2018-12-12 15:04:36, SecondViewController ,4687")
从日志里可以看出,进出队列包含时间、类名、和pageid,(UINavigationController代表的是这些页面都是Navigation类型的,而ThirdViewController没有pageid是因为配置表里没有配)。这样每个点击事件的来源就一目了然了。
动态获取自定义上报事件
试想这样一个场景,即使解决了动态Pageid的问题,如果统计需求,要在不同页面拿不同的参数,比如简历页我要获取简历id、简历编号、个人信息,JD页我又要页面数据、各个点击事件。每个页面要拿的参数都是不一样的,那么岂不是即使解决了pageid不同的瓶颈,最后还是落得要手动代码埋点的下场,因为每个页面的上报的参数都是不一样的。那这种情况下该如何解决呢?
一般来说有2种解决方案
第一种是代码埋点,对于高度自定义的上报就是得用代码来埋点,因为即使是像神策这样的全埋点策略,也无法做到所有地方的精确埋点。
这里主要是介绍第二种方案,取参埋点法。简单介绍一下什么是取参埋点,取参埋点其实就是利用Runtime,获取一个类所有的property属性,即成员变量,比如搜索结果页的列表数据,是存放在一个叫listData的数组里的,那么通过Hook机制,动态拿到listData,就可以拿到里面的数据进行上传操作。
这样只需要通过配置表,添加自己想获取的属性数据,就能上报这样的数据(前提是这个页面有这样的成员变量,局部变量的数据只能手动埋点了)。
取参埋点的部分代码:
+ (BOOL)getVariableWithClass:(Class) myClass varName:(NSString *)name{    unsigned int outCount, i;    Ivar *ivars = class_copyIvarList(myClass, &outCount);    for (i = 0; i < outCount; i++) {        Ivar property = ivars[i];        NSString *keyName = [NSString stringWithCString:ivar_getName(property) encoding:NSUTF8StringEncoding];        keyName = [keyName stringByReplacingOccurrencesOfString:@"_" withString:@""];        if ([keyName isEqualToString:name]) {            return YES;        }    }    return NO;}+ (id)captureVarforInstance:(id)instance varName:(NSString *)varName{    unsigned int count;    objc_property_t *properties = class_copyPropertyList([instance class], &count);        // 检测是否存在这个属性    BOOL exit = [ZPMCapturePropertyTool getVariableWithClass:[instance class] varName:varName];        id value = nil;    if (exit) {        value = [instance valueForKey:varName];    }        if (!value) {        NSMutableArray *varNameArray = [NSMutableArray arrayWithCapacity:0];        for (int i = 0; i < count; i++) {            objc_property_t property = properties[i];            NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];            NSArray *splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""];            if (splitPropertyAttributes.count < 2) {                continue;            }            NSString *className = [splitPropertyAttributes objectAtIndex:1];            Class cls = NSClassFromString(className);            NSBundle *bundle2 = [NSBundle bundleForClass:cls];            if (bundle2 == [NSBundle mainBundle]) {                //                NSLog(@"自定义的类----- %@", className);                const char *name = property_getName(property);                NSString *varname = [[NSString alloc] initWithCString:name encoding:NSUTF8StringEncoding];                [varNameArray addObject:varname];            } else {                //                NSLog(@"系统的类");            }        }                for (NSString *name in varNameArray) {            id newValue = [instance valueForKey:name];            if (newValue) {                value = [newValue valueForKey:varName];                if (value) {                    return value;                }else{                    value = [[self class] captureVarforInstance:newValue varName:varName];                }            }        }    }    return value;}
总结
以上讨论的方案主要是解决了提出的4个问题,尽量可以减少代码的侵入性,以及以后的维护成本。同时可以动态更新埋点数据,而不需要通过发版的方式解决。但是以上方案也只是涵盖了大部分场景, 并非所有场景都适用,具体大家可以根据业务情况来决定使用范围。如果有更好的方案获取提议,欢迎来骚扰。

人工智能时代:如何写一份高质量的埋点文档

在产品规划的过程中,产品经理的工作往往需要使用数据来进行辅助,例如如何利用用户的使用数据来为后续的产品迭代提供依据,如何向上级领导汇报产品成果,如何做精细化的运营活动,这些都需要通过埋点文档获取的数据来实现。
一、埋点文档的定义、分类以及数据平台1. 什么是埋点文档举个简单的例子,如何才能知道自已一个月的收入与支出情况呢?有两种做法,第一种是每个月初的时候看一下自己还有多少存款,然后到下个月月初的时候再看一下有多少存款,两者相减就是一个月的开销情况。
但如果你想知道钱到底花在哪了,哪里该花哪里该省,你就得有一个账本,定义好一些类别,例如吃饭,住房,交通,服装等等,然后分门别类的把收支记录下来,这样才能有针对性的对收支进行调整。
埋点文档,就是一个定义好的产品账本,它记录的是产品的收支情况,例如哪些功能被哪些用户使用了多少次,哪些页面用户流失率高,哪些内容被哪类用户喜欢。
2. 埋点文档的分类从埋点的分类来看,埋点分为“前端埋点”和“后端埋点”两种,前者是记录用户在客户端的使用数据,包括但不限于网页,APP,PC客户端等等,而后端埋点主要是记录程序接口的调用情况,例如接口访问次数,接口返回各个状态次数的统计等等。
产品经理通常说的埋点文档指的是前端埋点文档,前端埋点的优势是可以事无巨细的统计到用户的行为数据,但前端埋点为了性能考虑,并不会实时上报数据,所以注定了前端埋点的数据在时效性和准确性上不会做到100%精准无误。
如果希望统计的数据是实时且精准的,则往往采用接口将数据在指定的触发条件下上报至服务器,这时候就需要后端埋点来进行统计了。
3. 前端埋点的三种方法目前常见的前端埋点分为三种方法,分别是使用代码埋点,可视化埋点以及无埋点,我们一个一个来看一下:
(1)代码埋点
代码埋点是指在程序中加入用户统计数据的代码,当指定的触发行为发生的时候,就统计用户的使用数据,例如:想要统计某个活动页的运营效果,则触发行为是用户点击活动页的入口,并且同时上报用户本身的数据例如用户ID等等。
代码埋点的好处是可以根据使用者的需要任意的选择在什么时候发送什么数据,并且可以自定义丰富的数据属性。
而劣势则是对于产品经理对业务的理解程度和用户的理解程度要求较高,需要知道什么数据需要被收集;另外一个劣势是每一次加入埋点代码都会带来相对应的工作量,每一次更新埋点代码会引起新旧版本的不兼容问题,因为总是有一些用户不会更新到最新的版本(当然,如果你的产品可以确保每次用户使用的都是最新版本可以忽视这个问题)。
(2)可视化埋点
可视化埋点一般由第三方数据平台提供,可以通过非常直观的形式管理数据追踪点,通过圈选页面元素来实现数据的收集,例如下图是国内数据平台TalkingData的可视化埋点方案——灵动分析。
灵动分析
可视化埋点的优势在于每次更新埋点的时候并不需要等待程序更新,而是把数据统计的代码在应用程序启动的时候通过网络更新配置,解决了产品临时想要加入或修改埋点的需求。
而可视化埋点的劣势,是不能灵活的自定义事件,只能使用平台提供的一些通用性事件,例如点击次数,如果产品希望收集到用户在文本框中输入的内容,或者产品需要做大数据分析或人工智能推荐系统,可视化埋点方案是无法支持这样的数据收集能力的。
(3)无埋点
无埋点方案又叫全埋点方案,是尽可能的收集所有控件的操作数据,然后通过界面配置的方式添加一些需要统计的数据。
这种方案的好处是可以解决数据的回溯问题,即使之前的版本没有对某一个控件做精确的埋点,那么全埋点方案也一样会收集这个控件的数据,当后续有数据分析需要的时候就可以调出数据来查看了,
当然,无埋点的劣势跟可视化埋点一样,不能灵活的自定义事件,仅能用户分析用户在产品中的交互行为,且因为所有的事件都在收集,有时候会产生大量不必要的数据,给服务器带来很多负载。
综上所述,在三种埋点方案中,能最全面满足产品需要的还是代码埋点方案,所以本文重点介绍代码埋点的文档撰写。
二、如何写埋点文档1. 选择数据平台绝大多数公司的前端埋点会使用第三方数据平台来进行,极少数的大公司会有自己开发的数据平台,不想自己开发但又想确保数据安全的公司会选择购买数据平台进行私有化部署。
知名的第三方数据平台有国外的Google Analytics、Mixpanel,国内的有百度统计、友盟、Talkingdata、诸葛IO,神策数据,还有专注于游戏领域的dataeye等等。
通常来讲埋点平台选择一家即可,但因为前端埋点非实时性和精确度不高的特点,也会有公司在产品中同时使用两个埋点平台,用两个平台收集的数据来做相互的印证,提高数据的准确性,不过这种做法会带来额外的工作量,建议谨慎选择。
2. 查看平台技术文档选择好平台之后,第二步就是要查看平台提供的技术接入文档,不同的平台对于数据上报可能会有不同的限制,同时字段的命名也可能有一定的差异,所以需要通过查看平台提供的技术文档来了解这些信息。
下面我们就用Talkingdata的文档来举例,首先点击官网中的文档中心。
进入文档中心后我们会看到有很多的产品服务,包括APP、游戏,广告等等。我们点击第一个App Analytics的集成文档查看APP数据统计的技术文档。
进到集成文档之后,首先看到左边画红框并且标注1的部分,这里是各个不同客户端的文档,对应的是不同的开发语言,这个我们就不用一个一个去看了,因为平台给不同客户端提供的数据统计功能都是一样的,只是各个客户端编程语言不一样所以需要这么多,我们就以第一个iOS平台为例来讲。
然后我们看到右边红框标注2的部分,这部分是在教开发人员怎么把数据统计的功能快速集成到自己的app里面,我们如果是做一款从0到1的产品,要做数据统计,只要告诉开发同事,我们要使用哪个平台,然后他就会自己找到这个技术文档来看,我们不用去操心这部分的事情。
右边红框标注3的部分,是一些基础的统计功能,比如说:新增用户数,活跃用户数,7日留存,版本升级情况,这些数据只要你的产品完成了标注2的那些事情之后,数据平台就会帮你自动统计好。等产品上线之后直接到这个数据平台来看数据就好了。
重点是红框标注4的部分,高级功能中的自定义事件,我们的埋点文档主要也就是为这个功能服务的。而灵动事件就是之前提到的可视化埋点,这里我们省略不做说明。
埋点技术文档
三、埋点文档实例通过查看埋点技术文档,我们可以知道Talkingdata使用EventID来记录自定义事件的名称,使用Label来记录自定义事件下的多个追踪项,所以EventID+Label就组成了一个具体的事件名称。
在实际工作中,因各家公司使用的数据平台不同,所以埋点文档并没有形成一个统一的规范。我习惯于将EventID用于记录某一个页面,以Label记录该页面下的某个事件,以此达到对用户行为进行归类的目的。
我们用“人人都是产品经理”APP的首页来做一个说明,看一下:
人人都是产品经理
1. EventID在第一列的EventID,我们定义页面的ID号和名字,这里我采用的是英文字母+两位数字的方式,可以通过英文字母区分不同的业务模块,数字区分不同的页面。
2. Label第二列Label字段,定义的是用户在这个页面上的使用行为,因为Label是从属于前面EventID的,所以在功能点编号上也要继承前面的编号,例如:阅读页的EventID是A01,那么阅读页下面的事件就是A01加上两位数的编号,这样可以很方便的查看事件的从属页面,特别是当有同一个事件在多个页面重复被调用的时候。如果两位数的编号不够的话可以再增加位数。
另外一个需要注意的点是,如果某个点击事件是进入到了EventID的子页面,那么这个事件Label的编号就自动变为这个EventID的编号,这样可以很好的体现上级页面与下级页面的从属关系。 但如果一个页面可以由多个Label事件进入,那么就不用这么处理,而是直接使用一个公共的编号就可以了。
例如做一个电商系统的埋点,商品的详情页可以从A01-banner,从A02-广告,从A03-商品列表,从A04-搜索,从A05-收藏等等入口进入,在不同的页面中这些入口的Label编号肯定是不一样,但商品详情页的EventID是唯一的。
3. 上报时机第三列上报时机字段,需要说明这个埋点事件根据什么条件来触发,通常来讲分为显示时触发和操作时触发两种,前者看的是曝光量,后者看的是转化量,当有了全量的数据之后就可以用来构建曝光-转化的漏斗模型了。
对于平台之间差异化较小或没有差异的产品,例如iOS和安卓如果功能页面交互都一致,那么可以共用同一份埋点文档,但如果产品分布的平台较多,互相之间差异较大,例如既有手机APP又有PC客户端还有网页版,那么最好分不同平台来撰写不同的埋点文档。
4. 上线时间第四列上线时间,这个字段是说明该埋点是什么时候上线的,有些团队会用版本号来说明上线时间,但我认为版本号有几个弊端:
一是如果产品不同平台的版本号码不一致,会导致混乱;二是版本号无法体现埋点的生效时间,需要通过历史的产品文档查找到对应的功能才能知道,不够直观,所以我这边选择使用上线时间来作为埋点的生效时间。5. 优先级第五列是优先级,因为代码埋点需要开发人员花费时间来进行代码的编写,所以与功能需求池一样,需要标注埋点的优先级,以便开发人员根据优先级来评估工作量。我这里使用的是腾讯对于需求优先级的排列习惯,以P0为优先级最高。
最后一列是备注列,通常用来做一些备注的说明,例如某个埋点事件可以不再统计了,就可以写在备注说明里面。
四、自定义事件如果仅仅只是按照这样撰写埋点文档,只能统计到事件发生的次数。而代码埋点的核心是自定义事件的属性,也就是在上报事件的时候,同时上报这个事件定义的属性。
还是拿人人都是产品经理的APP来举例,首页上有很多文章,所以会有一个点击查看文章详情的事件,但是要想统计到谁在什么时间点击了一篇什么文章,以便分析用户的喜好和使用习惯,就需要通过定义数据字典来给自定义事件添加用户是谁,点击事件,点击的文章ID这三个属性。
1. 什么是数据字典数据字典是用来定义自定义事件属性的文档,通常和埋点文档放在一起通常有以下几个字段:
数据字典
KEY:Key是数据字典的核心内容,表示的是属性的名称,例如如果要记录用户的ID,那么就需要定义一个名为User_ID的key,如果是记录文章的标题,则需要定义一个名为Title的key。注释:对于key字段的解释,用来说明key值代表的是什么,便于后续的查询。数据类型:数据类型分为名义数据、等级数据和连续数据三种,这三个数据类型的定义是基本的统计学知识,本文略去不表,标注数据的类型有助于后续的数据分析工作,Value:Value是Key对应的值,有一些Key对应的是不确定的值,例如User_ID,有多少个用户就有多少个值,所以Value可以为空。但有一些Key的Value是限制在一定范围内的,所以需要事先对Value的可选择值作出定义,例如如果想统计一篇文章是否读完,可以定义一个Is_Read_Off的Key,那么对应的value值只有两个,是或否。全局字段Global:在数据统计的过程中,有一些key值是需要所有的事件都要进行统计的,典型的例如用户的ID,为了节省时间,可以将这些key值定义为全局字段,这样就可以不用在每个事件当中重复填写了。2. 如何给key命名在给数据字典的key命名的时候,建议可以使用程序员给字段变量取名的常用方法,主要有两种:
(1)驼峰命名法
驼峰命名法是最常用的命名方法之一,第一个单词以小写字母开始;从第二个单词开始以后的每个单词的首字母都采用大写字母,例如:userName,这种驼峰命名法又叫小驼峰法。而大驼峰法,则是把第一个单词的首字母也大写,例如:UserName。
(2)下划线命名法
而下划线命名法就顾名思义,是在多个单词之间使用下划线来进行分割,例如如果定义用户名为UserName,那么用下划线命名法则会写为User_Name。
我个人倾向于大驼峰+下划线的写法,当然,并没有强制的要求说字段命名一定要这么写,甚至写拼音也可以(就是显得有点low)。这两种命名方法是一种约定俗成的规则,只是如果你这么写的话,负责埋点的开发GG会觉得你很专业。
3. 将自定义事件的属性添加至事件中基于这份数据字典,我们就可以给自定义事件添加属性了,在原有的埋点文档上添加一列Key/Value字段,然后把要添加的属性加入到事件对应的行就可以了。
添加Key/Vlaue字段
如果要统计的属性很多,可以使用分号或者换一行来描述,同时也可以在每一行后面写上这个属性是用来统计什么内容的,方便负责埋点的开发同事了解属性的内容。
五、埋点文档注意事项1. 埋点文档只可增加,不可修改和删除埋点文档不同于产品经理的其他文档,像PRD文档一般都是只写本次迭代的内容,但埋点文档需要自始至终都在原有的基础上进行填写,且不能对原有的埋点进行修改或删除。
为什么呢?举个例子:
假设我们现在有一个编号A01的功能点,对应的事件是点击了某一篇文章,对应的版本号是1.0版本,到了1.1版本的时候,我把原来A01编号的功能点从点击了某一篇文章改了一下,变成了点击搜索按钮。
那么问题就出现了,还没有升级到1.1版本的用户,也就是那些1.0版本的用户,他们点击文章的时候依然会使用A01的编号来上传数据,而更新到1.1版本的用户,点击搜索按钮的时候,也在用A01编号来上传,这就会导致A01这个编号同时记录了两个版本不同行为的数据,导致数据不准确。
因为产品无法保证每个使用者都在使用同一版本,所以埋点文档不可以修改,也不可以删除,因为即使从埋点文档中删除了,已经上线了的统计代码是不会删除的。删除某个埋点文档可能会导致这个事件依然在上报,但后续的产品经理却不知道这是个什么事件了。
如果要对埋点文档进行删减,只能在备注中标明,该埋点已于xx时间废弃。
2. 事件必须独立为了确保埋点的准确性,需要让不同的事件之间相互独立,例如APP页面中的返回事件,要统计该页面的蹦失率(Bounce Rate)就需要统计有多少人点了返回按钮,但是每个页面可能都有返回按钮,如果只把Lable写成“返回”则很有可能会与其他页面的返回相互混淆,造成数据结果不正确,这个问题我们已经通过给每个EventID和Label加上唯一编码解决了。
另外一个注意点之前也提到过,就是通用的页面事件需保持唯一的编号,而不是用多个编号去统计同一个事件,造成数据的分散。如果有一个通用的页面可以通过不同的入口进入,那么可以在这个页面的事件中加入一个From_page的属性,来记录是从哪个入口进入到这个通用页面中来的。
3. 数据字典不重复在一个大型的团队中可能会有多个产品经理一起维护一份埋点文档,为了确保每个事件属性的含义保持一致,所以数据字典中的每一个key也都是唯一的,如果自己需要的key已经由其他人定义好了,则可以直接拿过来使用。如果要定义之前没有出现过的key,则只需要在数据字典中添加,然后同步给其他产品经理即可。
4. 注意平台限制不同的埋点平台可能对于事件和属性有上限的限制,例如友盟平台一个APP只能记录500个事件,每个事件只能定义10个属性,而talkingdata的事件是可以无上限记录的,每个事件可以记录50个属性,所以大家在撰写埋点文档的时候,一定要注意自己选择的平台是否对于事件有限制规则,以免出现无法记录的情况出现。
5. 埋点测试埋点代码编写完成后需要对埋点进行测试,这个过程一般是和测试同事一起进行,用来确保埋点的数据上报正确,该统计的属性也都添加成功了。
总结对于产品经理来说,埋点的数据不仅仅是用于分析用户的行为,它更是很多功能的基础,例如有了埋点的数据才可以做产品报表,或者可以通过埋点构建大数据用户画像,用于Ai推荐系统,亦或者是分析渠道的优劣对运营作出指导。
以上就是对埋点文档的一点经验,希望能对你有所帮助,大家有什么看法也欢迎在评论区讨论。
本文由 @黄瀚星 原创发布于人人都是产品经理。未经许可,禁止转载
题图来自Unsplash,基于CC0协议

发表评论