Hệ thống tracking active, retention rate - Phần 1
White

Minh Hoang TO viết ngày 09/07/2016

Bài viết này trình bày một giải pháp build-from-scratch cho bài toán khá phổ biến sau

Tôi có một ứng dụng A, tôi đang liên kết với các đối tác {C_1, C_2, ..., C_n} để phát triển user cho sản phẩm. Vậy làm thế nào để tôi có thể xác định được độ hiệu quả cũng như chất lượng của tập user mỗi đối tác mang lại?

Giải pháp

Về cơ bản, để xác định được độ hiệu quả cũng như chất lượng người dùng của đối tác C_k thì ta cần các thông số về số lượng user mà C_k mang về, số lượng active user và tỉ lệ retention rate. Để có các thông số này thì ta sẽ hướng đến xây dựng hệ thống cho phép:

  • Phân kênh mỗi user U theo đối tác C_k đã mang lại user U
  • Ghi nhận các user action đi kèm với dữ liệu phân kênh
  • Trích xuất dữ liệu thống kê dựa trên user action data ghi nhận được

Trong phần ghi nhận user action, ta sẽ dùng ElasticSearch do indexing engine này cung cấp query language rất mạnh, cho phép trích xuất dễ dàng các dữ liệu cho bất kỳ advanced dashboard nào. Chi tiết về việc cài đặt, sử dụng ElasticSearch nằm ngoài khuôn khổ của bài viết này và xin được dành cho bạn đọc tự tìm hiểu.

Phân kênh user

Giả dụ người dùng U cài đặt ứng dụng A thông qua link tải được cung cấp cho đối tác C_k, ta sẽ cần lưu trữ thông tin về C_k trên preferences của ứng dụng A trên thiết bị của người dùng U.

Trên nền tảng Android, khi người dùng cài đặt ứng dụng từ Google Play thì ứng dụng CHPlay trên thiết bị sẽ broadcast sự kiện INSTALL_REFERRER với param referrer trong phần query string của link tải ứng dụng. Dựa trên tính năng INSTALL_REFERRER, ta có thể phân kênh user theo mô hình sau:

public class ChannelConfigReceiver extends BroadcastReceiver{

    @Override
    public void onReceive(final Context ctx, Intent intent) {
        String ref = intent.getStringExtra("referrer");
        //Lấy thông tin về đối tác trong 'ref' và lưu 
        // giá trị vào param 'channel_id' trong SharedPreferences
    }
}

       <receiver
            android:name="ChannelConfigReceiver"
            android:exported="true">
            <intent-filter>
              <action android:name="com.android.vending.INSTALL_REFERRER" />
            </intent-filter>
        </receiver>

Việc phân kênh trên iOS phức tạp hơn khá nhiều do nền tảng này không hỗ trợ sẵn cơ chế tương tự như INSTALL_REFERRER . Với các thiết bị iOS có cài ứng dụng Facebook thì ta vẫn có giải pháp work around như mô tả trong link sau

https://developers.facebook.com/docs/applinks/ios

Ghi nhận user action

Để tracking được active user, retention rate thì ta cần API cho phép ghi nhận các action sau:

  • FIRST_RUN - Khi người dùng mở ứng dụng lần đầu tiên. Endpoint này sẽ trả về first_run timestamp cho phép phía client lưu lại giá trị này trong suốt thời gian sống của ứng dụng
  • START - Khi người dùng mở ứng dụng. Endpoint này sẽ trả về giá trị của session_id cho phép phía client lưu lại giá trị này trong suốt thời gian tồn tại của session
  • STOP - Khi người dùng thoát ứng dụng, crash

Để có được các lát cắt khác nhau cho dữ liệu thống kê trên dashboard thì ta có thể gửi kèm các dữ liệu sau cho mỗi API call từ phía mobile client

  • device_id
  • channel_id
  • first_run
  • session_id
  • product_id
  • product_version
  • os_brand
  • os_model
  • country_name
  • country_code

Đoạn code dưới đây (dùng Flask + ElasticSearch client) minh hoạ cơ chế xử lý đầu server.

from flask import Blueprint, request, jsonify
from elasticsearch import Elasticsearch

from app import app

import time
import logging

__author__ = 'hoang281283@gmail.com'

logger = logging.getLogger("api.device_tracking")

es = Elasticsearch(app.config.get('ELASTIC_SEARCH_HOSTS', [{'host': 'localhost', 'port': 9200}]))
index_name = app.config.get('TRACKING_ACTION_INDEX', 'tracking_action')
doc_type = "user_action"

mod = Blueprint('device_tracking', __name__)


@mod.route('/<product_id>/tracking_action', methods=['POST'])
def track_action(product_id):
    try:
        env = request.environ
        remote_ip = env.get('HTTP_X_REAL_IP', None)
        if not remote_ip:
            remote_ip = env.get("HTTP_X_FORWARD_FOR", None)

        country_name = "UNKNOWN"
        country_code = "UNKNOWN"
        try:
            location = get_location(remote_ip)
            country_name = location["country_name"]
            country_code = location["country_code"]
        except Exception as e:
            pass

        json = request.get_json(force=True)
        action = json.get('action', '')

        device_id = json.get('device_id', '')
        session_id = json.get('session_id', '')
        channel_id = json.get('channel_id', '')
        sub_channel_id = json.get('sub_channel_id', '')
        product_version = json.get('product_version', '')
        os_brand = json.get('os_brand', '')
        os_version = json.get('os_version', '')

        tmp = int(time.time() * 1000)
        first_run = json.get('first_run', tmp)

        doc = {
            'created': int(time.time() * 1000),
            'first_run': first_run,
            'product_id': product_id,
            'device_id': device_id,
            'session_id': session_id,
            'channel_id': channel_id,
            'action': action,
            'country_name': country_name,
            'country_code': country_code,
            'product_version': product_version,
            'os_brand': os_brand,
            'os_version': os_version
        }

        if action == 'FIRST_RUN':
            session_id = generate_uuid()
            first_run = tmp
            doc['first_run'] = first_run

            inf = "Tracking action product_id=%s, device_id=%s, first_run=%s, session_id=%s, channel_id=%s, " \
                  "action=%s" % \
                  (product_id, device_id, first_run, session_id, channel_id, action)

            logger.info(inf)

            doc['session_id'] = session_id
            es.index(index=index_name, doc_type=doc_type, body=doc)

            return jsonify({"product_id": product_id, "session_id": session_id, "first_run": first_run, "action": "FIRST_RUN"})

        elif action == 'START':
            session_id = generate_uuid()
            inf = "Tracking action product_id=%s, device_id=%s, first_run=%s, session_id=%s, channel_id=%s, " \
                  "action=%s" % \
                  (product_id, device_id, first_run, session_id, channel_id, action)

            logger.info(inf)

            doc['session_id'] = session_id
            es.index(index=index_name, doc_type=doc_type, body=doc)

            return jsonify({"product_id": product_id, "session_id": session_id, "first_run": first_run, "action": "START"})

        else:
            inf = "Tracking action product_id=%s, device_id=%s, first_run=%s, session_id=%s, channel_id=%s, " \
                  "action=%s" % \
                  (product_id, device_id, first_run, session_id, channel_id, action)

            logger.info(inf)

            es.index(index=index_name, doc_type=doc_type, body=doc)

            return jsonify({"product_id": product_id, "session_id": session_id, "first_run": first_run, "action": action})
    except Exception as e:
        return jsonify({"status": "error"})

Phần gửi request đầu client thì có thể xây dựng SDK nội bộ làm việc với API trên, hoặc đơn giản hơn trực tiếp gửi HTTP request thông qua HTTP client (như Retrofit) hoặc HttpUrlConnection.

Trích xuất dữ liệu thống kê

Với dữ liệu tracking được ghi nhận vào Elastic Search thì ta có thể lấy dữ liệu thống kê về active user cho kênh C_k trong một khoảng thời gian cho trước với query như sau

  q = {
            "query": {
                "filtered": {
                    "filter": {
                        "bool": {
                            "must": [
                                {
                                    "terms": {
                                        "action": ["FIRST_RUN", "START"]
                                    }
                                },
                               {
                                   "term":{"channel_id": C_k}
                               },
                                {
                                    "range": {
                                        "created": {
                                            "gte": start_time_epoch,
                                            "lte": end_time_epoch
                                        }
                                    }
                                }
                            ]
                        }

                    }
                }
            },

            "aggs": {
                "unique_device": {
                    "cardinality": {
                        "field": "device_id"
                    }
                }
            }
        }

--- Hết phần 1---

Phần tiếp theo sẽ trình bày một số queries phức tạp hơn (vẫn dựa trên dữ liệu lưu theo mô hình trên) cho phép thống kê sâu hơn active, retention rate. Ví dụ như cho phép trích xuất ra dữ liệu hiển thị retention rate theo hình tam giác ngược như minh hoạ dưới đây

alt text

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Minh Hoang TO

4 bài viết.
5 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
6 0
Trước hết xin nhắc lại mục đích của bài viết này là đưa ra giải pháp buildfromscratch cho bài toán sau: Tôi có một ứng dụng A, tôi đang liên kết v...
Minh Hoang TO viết hơn 2 năm trước
6 0
White
6 1
Bài viết này trình bày cách thiết lập cơ chế build CI thông qua Jenkins cho các Android project (libraries hoặc applications) được quản lý bằng (Li...
Minh Hoang TO viết hơn 2 năm trước
6 1
White
1 0
Bài viết này trình bày cách xử lý khá đơn giản để đăng nhập vào tài khoản quản trị của TeamCity khi bạn vô tình quên password. Thay vì tìm hiểu cơ...
Minh Hoang TO viết 2 năm trước
1 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
4 bài viết.
5 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!