GoogleAnalytics在电商场景下的实践

GA提供了多个维度的数据分析,覆盖了电商场景下所有的统计需求;

行为

屏幕

一级屏幕

二级屏幕

名词解释

  • 二级屏幕:在一级屏幕基础上细分的二级屏幕;
  • 屏幕名称:屏幕统计埋点,key的名字;
  • 屏幕浏览量:屏幕pv,等于当前屏幕所有二级屏幕pv总和;
  • 唯一屏幕浏览量:屏幕浏览会话数量(默认30分钟),等于当前屏幕所有二级屏幕会话数量的总和;

埋点位置

Android
  • Fragment/Activity onStart ;
  • 如果是ViewPager+Fragment结构,需要在切换Tab(点击/滑动)时添加统计;
iOS
  • ViewController viewWillAppear;
  • 如果是UIScrollView+ViewController结构,需要在切换Tab(点击/滑动)时添加统计;
  • 从后台进入前台,需要添加统计;

埋点代码

Android
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static Tracker getTracker(){
Tracker t = GoogleAnalytics.getInstance(ProxyFactory.getContext()).newTracker(trackingId);
// 跨平台共享会话的唯一标识
final String cid = t.get("&cid");
GAHelper.cid = cid;
return t;
}
/**
* 带二级维度的屏幕统计
* @param screenName 屏幕名
* @param dimensionVaue 二级维度
*/
protected static void trackCustomDimensionPage(String screenName, String dimensionVaue){
final Tracker t = getTracker();
t.setScreenName(screenName);
// Send the custom dimension value with a screen view.
// Note that the value only needs to be sent once.
Map<String, String> build = new HitBuilders.ScreenViewBuilder()
.setCustomDimension(1, dimensionVaue)
.build();
if (BuildInfo.DEBUG) {
LogUtils.d(screenName + "==========>" + dimensionVaue + ":" + build);
}
t.send(build);
// Clear the screen name field when we're done.
t.setScreenName(null);
if (BuildInfo.DEBUG){
sendHitsNow();
}
}
/**
* 立即发送ga数据
*/
private static void sendHitsNow(){
GoogleAnalytics.getInstance(ProxyFactory.getContext()).dispatchLocalHits();
}
iOS
1
2
3
4
5
6
7
8
9
public static func trackCustomDimensionPage(screenName: String, dimensionVaue: String){
guard let tracker = getTracker() else {return}
// 设置一级屏幕
tracker.set(kGAIScreenName, value: screenName)
// 设置二级屏幕
tracker.set(GAIFields.customDimension(for: 1), value: dimensionVaue)
guard let builder = GAIDictionaryBuilder.createScreenView().set(dimensionVaue, forKey: GAIFields.customDimension(for: 1)) else {return}
tracker.send(builder.build() as [NSObject : AnyObject])
}

常见问题

一级屏幕浏览量 != 二级屏幕浏览量 (iOS)
  • 埋点代码错误,tracker和builder都必须设置二级屏幕;

    1
    2
    tracker.set(GAIFields.customDimension(for: 1), value: dimensionVaue)//必须添加,否则会造成屏幕
    guard let builder = GAIDictionaryBuilder.createScreenView().set(dimensionVaue, forKey: GAIFields.customDimension(for: 1)) else {return}
屏幕浏览量大于实际浏览量 (iOS & Android)
  • 产品列表展示统计造成的影响, 产品列表展示统计,会将当前一级屏幕及二级屏幕数据上传,解决方法是,强制将一级屏幕和二级屏幕参数值设置为空字符串;
  • Android解决方案

    1
    2
    3
    4
    // 必须添加以下参数,否则产品列表不显示
    builder.set("&t", "screenview");
    builder.set("&cd", "");
    builder.set("&cd1", "");
  • iOS解决方案

    1
    2
    3
    // 特别注意:商品列表数据,必须强制设置屏幕及二级屏幕为空字符串,否则造成屏幕统计数量过多
    tracker.set(kGAIScreenName, value: "")
    tracker.set(GAIFields.customDimension(for: 1), value: "")

行为流

  • 行为流,是根据屏幕统计数据分析,得出用户在App内所有操作行为的展现,可以查看每个交互行为的转化;

应用速度

使用场景

App启动时间的统计分析
  • App启动时间,特别是冷启动时间对启动率和留存率的影响还是蛮大的;
  • 可以将App启动时间做二级维度的拆分,冷启动时间、热启动时间、CPU数量、内存大小、系统版本等,为启动时间的优化提供数据支持;

API执行时间统计分析

  • 服务端的API执行时间统计,是从接收到请求到处理完响应的过程,无法准确反应实际场景下用户的等待时间;在App端额外添加统计作为辅助;
  • 可以对整个请求时间做二级维度的拆分,请求时间、响应时间、数据解析时间,做优化时有数据支持更有针对性;

其它业务场景下的统计分析

  • 为版本迭代的效果提供数据依据,例如,订单确认页的改版是为了提高用户下单的效率,除了观察下单数量的变化外,在订单确认页到提交订单实际停留时间埋点更直接有效;
  • 一些实际业务场景统计
    • 订单确认页到提交订单实际停留时间;
    • 购物流程耗时;
    • 支付流程耗时;
    • 注册流程耗时
    • 分享流程耗时;

转化-电子商务

产品列表业绩

名词解释

  • 在产品列表中获得的浏览次数:列表请求次数 x 列表中所有商品的总和;
  • 在产品列表中获得的点击次数:点击进入详情页的次数;
  • 产品结账次数:进入自定义的checkout流程的次数;目前是进入订单确认页的次数;
  • 唯一身份购买次数:商品实际支付成功的次数;例如:一个订单包含多件单品,单品也可能来自不同的产品列表,支付成功后该数据会体现在不同的产品列表中;
  • 产品收入:实际支付成功的金额;

产品列表展示

埋点位置
  • 所有产品列表获取到网络数据,将新获取到的所有产品添加到统计;
    • 首次进入页面获取数据;
    • 请求数据失败重试;
    • 下拉刷新;
    • 加载更多;
埋点代码
  • Android

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 跟踪产品列表
    * @param impressionList
    * @param statProducts
    */
    protected static void trackProductList(String impressionList, List<StatProduct> statProducts){
    if (StringUtils.isNotEmpty(impressionList) && statProducts != null && statProducts.size() > 0){
    final HitBuilders.ScreenViewBuilder builder = new HitBuilders.ScreenViewBuilder();
    int productCount = 0;
    for (StatProduct statProduct : statProducts) {
    final Product product = toProduct(statProduct);
    builder.addImpression(product, impressionList);
    productCount ++;
    }
    if (LogUtils.DEBUG){
    LogUtils.d("跟踪产品列表==========>" + impressionList + ":" + productCount);
    }
    // 必须添加以下参数,否则产品列表不显示
    builder.set("&t", "screenview");
    builder.set("&cd", "");
    builder.set("&cd1", "");
    final Tracker t = getTracker();
    t.send(builder.build());
    if (BuildInfo.DEBUG){
    sendHitsNow();
    }
    }
    }
  • iOS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public static func trackerItemProductList(className: String, impressionList: String, itemInfos: [ItemInfo]) {
    guard let builder = GAIDictionaryBuilder.createScreenView() else {return}
    var productCount = 0
    for item in itemInfos {
    if let title = item.title, let price = item.price, let id = item.itemId {
    productCount = productCount + 1
    addProductImpressionToBuilder(id: String(id), name: title, price: price
    , impressionList: impressionList, builder: builder)
    }
    }
    sendProductList(className, impressionList: impressionList, builder: builder, productCount: productCount)
    }
    private static func addProductImpressionToBuilder(id: String, name: String, price: Double
    , impressionList: String, builder: GAIDictionaryBuilder){
    let product = GAIEcommerceProduct()
    let customId = id + "-\(name)"
    product.setId(customId)
    product.setName(name)
    product.setBrand("xxx")
    product.setPrice(price as NSNumber!)
    builder.addProductImpression(product, impressionList: impressionList, impressionSource: nil)
    }
    private static func sendProductList(_ className: String, impressionList: String, builder: GAIDictionaryBuilder, productCount: Int){
    guard let tracker = getTracker() else {return}
    // 特别注意:商品列表数据,必须强制设置屏幕及二级屏幕为空字符串,否则造成屏幕统计数量过多
    tracker.set(kGAIScreenName, value: "")
    tracker.set(GAIFields.customDimension(for: 1), value: "")
    tracker.send(builder.build() as [NSObject : AnyObject])
    zzlog(" Google Analytics className --> \(className) 展示列表 -->|\(impressionList)| --> productCount:\(productCount)")
    }

产品点击

埋点位置
  • 跳转到商品详情页点击事件;
埋点代码
  • Android

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /**
    * 跟踪产品点击
    * @param impressionList
    * @param statProduct
    */
    protected static void trackProductClick(String impressionList, StatProduct statProduct){
    if (StringUtils.isNotEmpty(impressionList) && statProduct != null){
    final Product product = toProduct(statProduct);
    final Tracker t = getTracker();
    if (LogUtils.DEBUG){
    LogUtils.d("跟踪产品点击==========>" + impressionList + ":" + statProduct.getName());
    }
    // detail
    final ProductAction detailAction = new ProductAction(ProductAction.ACTION_DETAIL)
    .setProductActionList(impressionList);
    final HitBuilders.ScreenViewBuilder detailBuilder = new HitBuilders.ScreenViewBuilder()
    .addProduct(product)
    .setProductAction(detailAction);
    // 必须添加以下参数,否则产品列表不显示
    detailBuilder.set("&t", "screenview");
    detailBuilder.set("&cd", "");
    detailBuilder.set("&cd1", "");
    t.send(detailBuilder.build());
    // click
    final ProductAction clickAction = new ProductAction(ProductAction.ACTION_CLICK)
    .setProductActionList(impressionList);
    final HitBuilders.ScreenViewBuilder clickBuilder = new HitBuilders.ScreenViewBuilder()
    .addProduct(product)
    .setProductAction(clickAction);
    // 必须添加以下参数,否则产品列表不显示
    clickBuilder.set("&t", "screenview");
    clickBuilder.set("&cd", "");
    clickBuilder.set("&cd1", "");
    t.send(clickBuilder.build());
    if (BuildInfo.DEBUG){
    sendHitsNow();
    }
    }
    }
  • iOS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    public static func trackerProductClick(className: String, id: String, name: String, price: Double, impressionList: String){
    guard let tracker = getTracker() else {return}
    var product = GAIEcommerceProduct()
    let customId = id + "-\(name)"
    product.setId(customId)
    product.setName(name)
    product.setBrand("xxx")
    product.setPrice(price as NSNumber!)
    guard let builder = GAIDictionaryBuilder.createScreenView() else {return}
    builder.addProductImpression(product, impressionList: impressionList, impressionSource: nil)
    product = GAIEcommerceProduct()
    product.setId(customId)
    product.setName(name)
    product.setBrand("xxx")
    product.setPrice(price as NSNumber!)
    let action = GAIEcommerceProductAction()
    action.setAction(kGAIPADetail)
    action.setProductActionList(impressionList)
    builder.setProductAction(action)
    builder.add(product)
    //tracker.set(kGAIScreenName, value: className)
    let detailBuilderDict = builder.build() as [NSObject : AnyObject]
    tracker.send(detailBuilderDict)
    zzlog("detailBuilderDict:\(detailBuilderDict)")
    guard let clickBuilder = GAIDictionaryBuilder.createEvent(withCategory: "Ecommerce", action: kGAIPAClick, label: nil, value: nil) else {return}
    let clickAction = GAIEcommerceProductAction()
    clickAction.setAction(kGAIPAClick)
    clickBuilder.add(product)
    clickAction.setProductActionList(impressionList)
    clickBuilder.setProductAction(clickAction)
    let clickBuilderDict = clickBuilder.build() as [NSObject : AnyObject]
    tracker.send(clickBuilderDict)
    zzlog("clickBuilderDict:\(clickBuilderDict)")
    zzlog("Google Analytics \(name) 在 |\(impressionList)| 列表中被点击了")
    }

产品添加到购物车

埋点位置
  • 添加商品到购物车成功;
埋点代码
  • Android
  • iOS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public static func trackerAddItemProductToCartList(className: String, quantity: Int, itemInfo: ItemInfo) {
    guard let tracker = getTracker() else {return}
    if let title = itemInfo.title, let price = itemInfo.price, let itemId = itemInfo.itemId {
    let product = GAIEcommerceProduct()
    let customId = String(itemId) + "-\(title)"
    product.setId(customId)
    product.setName(title)
    product.setBrand("xxx")
    product.setPrice(price as NSNumber!)
    product.setQuantity(quantity as NSNumber!)
    let action = GAIEcommerceProductAction()
    action.setAction(kGAIPAAdd)
    guard let builder = GAIDictionaryBuilder.createEvent(withCategory: "Ecommerce", action: kGAIPAAdd, label: nil, value: nil) else {return}
    builder.setProductAction(action)
    builder.add(product)
    //tracker.set(kGAIScreenName, value: className)
    tracker.send(builder.build() as [NSObject : AnyObject])
    zzlog(" Google Analytics 添加单品到购物车 --> |\(builder.build() as [NSObject : AnyObject])|")
    }
    GAIHelper.sendHitsNow()
    }

结账行为

可以查看结账流程每一步的转化率;


名词解释

  • 带来交易的会话:实际支付成功的会话数(默认30分钟视为一个会话);

自定义Checkout流程

埋点位置
  • 订单确认页页面初始化位置;
  • 点击确认订单按钮后,提交订单成功;
  • 支付按钮点击事件;
埋点代码
  • Android
  • iOS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public static func trackCheckoutStep(orderSn: String, orderPrice: Double, items: [Item]){
    guard let tracker = getTracker() else {return}
    guard let builder = GAIDictionaryBuilder.createEvent(withCategory: "Ecommerce", action: "Checkout", label: nil, value: nil) else {return}
    for item in items{
    if item.isSuite ?? false{
    if item.children != nil{
    for subItem in item.children{
    builder.add(createProduct(item: subItem))
    }
    }
    }else{
    builder.add(createProduct(item: item))
    }
    }
    // action
    let action = GAIEcommerceProductAction()
    action.setAction(kGAIPACheckout)
    // 根据checkout流程设置步骤数,目前为:1、2、3
    action.setCheckoutStep(1)
    builder.setProductAction(action)
    tracker.send(builder.build() as [NSObject : AnyObject])
    GAIHelper.sendHitsNow()
    }

Purchase

埋点位置
  • App支付成功页面或服务端支付成功代码处;
埋点代码
  • Android
  • iOS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public static func onPurchaseCompleted(transactionId: String, revenue: Double, items: [Item]) {
    guard let tracker = getTracker() else {return}
    guard let builder = GAIDictionaryBuilder.createEvent(withCategory: "Ecommerce", action: kGAIPAPurchase, label: nil, value: nil) else {return}
    for item in items{
    if item.isSuite ?? false{
    if item.children != nil{
    for subItem in item.children{
    builder.add(createProduct(item: subItem))
    }
    }
    }else{
    builder.add(createProduct(item: item))
    }
    }
    // action
    let action = GAIEcommerceProductAction()
    action.setAction(kGAIPAPurchase)
    action.setTransactionId(transactionId)
    action.setRevenue(revenue as NSNumber!)
    builder.setProductAction(action)
    tracker.send(builder.build() as [NSObject : AnyObject])
    GAIHelper.sendHitsNow()
    zzlog(" Google Analytics 电子商务 支付完成:\(builder.build() as [NSObject : AnyObject])")
    }

购物行为


名词解释

  • 已结账的会话:进入自定义checkout流程的会话数;
  • 带来交易的会话:实际支付成功的会话数;

销售业绩

销售业绩,显示的是实际支付成功的订单;

订单列表

订单详情

产品业绩

产品业绩,显示的是实际支付成功的商品;

常见问题

产品列表业绩,无产品列表数据(Android)

1
2
3
4
// 必须添加以下参数,否则产品列表不显示
builder.set("&t", "screenview");
builder.set("&cd", "");
builder.set("&cd1", "");

产品列表业绩,无产品点击数据(Android)

1
2
3
4
5
// 必须添加以下参数,否则产品列表不显示点击
clickBuilder.set("&t", "screenview");
clickBuilder.set("&cd", "");
clickBuilder.set("&cd1", "");
t.send(clickBuilder.build());

产品列表业绩,出现大量noset

App端统计问题
  • 根本原因是产品被添加到购物车或产品结账前必须执行了点击统计;
服务端统计问题
  • 服务端在支付成功的统计中,将”t”对应的值写成了”pageview”,必须为”event”

    1
    put("t", "event");//匹配的类型(订单支付成功就填这个) 必填

产品列表业绩,产品结账次数错误

  • 产品结账次数,是进入checkout流程第一步的次数;如果有可能直接进入checkout流程第二步就会造成此数据不显示,应该考虑重构checkout流程;

结账行为,统计数据与实际数据差距较大

  • GA默认120秒定时发送缓存的统计数据,如果用户走完购买流程后直接退出App可能造成数据统计不全;
  • 解决方案

    • 可将checkout流程、添加到购物车、Purchase放在服务端埋点;
    • App在checkout流程、添加到购物车、Purchase埋点时,主动调度GA发送数据;

      1
      2
      // 主动调度数据匹配
      GAI.sharedInstance().dispatch()
    • App进入后台,主动调度GA发送数据;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      func applicationDidEnterBackground(_ application: UIApplication) {
      sendHitsInBackground()
      }
      func applicationWillEnterForeground(_ application: UIApplication) {
      GAI.sharedInstance().dispatchInterval = 120
      }
      private var dispatchHandler:((_ result: GAIDispatchResult ) -> ())?
      private func sendHitsInBackground() {
      var taskExpired = false
      let taskId = UIApplication.shared.beginBackgroundTask {
      taskExpired = true
      }
      if (taskId == UIBackgroundTaskInvalid) {
      return
      }
      self.dispatchHandler = {[weak self] (result) in
      if (result == GAIDispatchResult.good && !taskExpired) {
      GAI.sharedInstance().dispatch(completionHandler: self?.dispatchHandler)
      }else{
      UIApplication.shared.endBackgroundTask(taskId)
      }
      }
      GAI.sharedInstance().dispatch(completionHandler: self.dispatchHandler)
      }

接入问题

怎么区分开发/生产环境

编译期区分生产/测试环境

  • Android可以在BuildConfig中,根据编译环境动态配置GA AppKey;
  • iOS可以通过DEBUG宏动态配置GA AppKey;

运行期区分生产/测试环境

  • 日常测试,下载生产环境的App,通过更改DNS连接到测试服务器测试,可能会造成统计数据发送到生产环境;
  • 将GA AppKey在服务端根据环境动态下发到App;

查看GA日志

Android

1
2
3
4
5
6
7
// Google Analytics(分析)将使用 Android Log 系统通过 GAv4 代码将日志记录到 logcat。
// 默认情况下只启用 ERROR、WARN 和 INFO 级别。
// 要启用 DEBUG 级别,请在您的设备或模拟器上运行以下 adb 命令:
adb shell setprop log.tag.GAv4 DEBUG
// 要只查看 logcat 中的 Google Analytics(分析)消息,请使用以下命令:
adb logcat -v time -s GAv4

iOS

Android接入后Crash

java.lang.NoSuchMethodError: No static method zzb(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
         in class Lcom/google/android/gms/common/internal/zzaa;
         or its super classes (declaration of 'com.google.android.gms.common.internal.zzaa'
         appears in /data/data/com.zaozuo.android/files/instant-run/dex/slice-com.google.android.gms-play-services-basement-9.2.0_838f224be8ca39a2f48a2f75d5178b0e700ad7c6-classes.dex)
         at com.google.firebase.provider.FirebaseInitProvider.zza(Unknown Source)
         at com.google.firebase.provider.FirebaseInitProvider.attachInfo(Unknown Source)
         at android.app.ActivityThread.installProvider(ActivityThread.java:5595)
         at android.app.ActivityThread.installContentProviders(ActivityThread.java:5168)
         at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5108)
         at android.app.ActivityThread.-wrap2(ActivityThread.java)
         at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1633)
         at android.os.Handler.dispatchMessage(Handler.java:111)
         at android.os.Looper.loop(Looper.java:207)
         at android.app.ActivityThread.main(ActivityThread.java:5896)
         at java.lang.reflect.Method.invoke(Native Method)
         at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
         at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:679)
参考资料:
0%