android mixpanel可视化埋点框架解析

Mixpanel唯一的开源的商业可视化埋点实现,提供可视化埋点以及代码埋点方式。

mixpanel官网

mixpanel android集成指南

mixpanel框架开源地址

首先在Mixpanel官网注册,然后创建一个项目,有一个Token,在初始化的时候需要用到。

接下里我们分析一下源码:

入口MixpanelAPI,提供初始化方法

 mMixpanel = MixpanelAPI.getInstance(this, projectToken);

Mixpanel支持代码埋点和可视化埋点,先看一下代码埋点:

1 代码埋点

MixpanelAPI提供track方法埋点

/**
     * Track an event.
     *
     * <p>Every call to track eventually results in a data point sent to Mixpanel. These data points
     * are what are measured, counted, and broken down to create your Mixpanel reports. Events
     * have a string name, and an optional set of name/value pairs that describe the properties of
     * that event.
     *
     * @param eventName The name of the event to send
     * @param properties A JSONObject containing the key value pairs of the properties to include in this event.
     *                   Pass null if no extra properties exist.
     */
    // DO NOT DOCUMENT, but track() must be thread safe since it is used to track events in
    // notifications from the UI thread, which might not be our MixpanelAPI "home" thread.
    // This MAY CHANGE IN FUTURE RELEASES, so minimize code that assumes thread safety
    // (and perhaps document that code here).
    public void track(String eventName, JSONObject properties) {
        if (hasOptedOutTracking()) return;
        track(eventName, properties, false);
    }

    /**
     * Equivalent to {@link #track(String, JSONObject)} with a null argument for properties.
     * Consider adding properties to your tracking to get the best insights and experience from Mixpanel.
     * @param eventName the name of the event to send
     */
    public void track(String eventName) {
        if (hasOptedOutTracking()) return;
        track(eventName, null);
    }

例如:

        // Now we send an event to Mixpanel. We want to send a new
        // "App Resumed" event every time we are resumed, and
        // we want to send a current value of "hour of the day" for every event.
        // As usual,all of the user's super properties will be appended onto this event.
        try {
            final JSONObject properties = new JSONObject();
            properties.put("hour of the day", hourOfTheDay);
            mMixpanel.track("App Resumed", properties);
        } catch (final JSONException e) {
            throw new RuntimeException("Could not encode hour of the day in JSON");
        } 

主要是在 activiyt 的 onPause 之后上传数据,同时也提供了方法让你立即上传时间:

public void flush() {
    mMessages.postToServer(new AnalyticsMessages.FlushDescription(mToken));
}

2可视化埋点

讲到可视化埋点,那必须讲到服务器和客户端的通信,服务端下发消息客户端在哪里接收?

Mixpanel可视化埋点通信是基于Socket,并且可以找到WebSocketImpl这个类的decode方法,那么这个方法在哪里调用的呢,其实是在WebSocketClient的run方法中,读取到服务器的流然后decode解析,然后调用decodeFrames方法,里面调用

try {
	wsl.onWebsocketMessage( this, Charsetfunctions.stringUtf8( f.getPayloadData() ) );
	} catch ( RuntimeException e ) {
		wsl.onWebsocketError( this, e );
	}

最终实现在EditorConnection类

 @Override
        public void onMessage(String message) {
            MPLog.v(LOGTAG, "Received message from editor:\n" + message);
            try {
                final JSONObject messageJson = new JSONObject(message);
                final String type = messageJson.getString("type");
                if (type.equals("device_info_request")) {//发送设备信息
                    mService.sendDeviceInfo();
                } else if (type.equals("snapshot_request")) {//发送截图和控件坐标
                    mService.sendSnapshot(messageJson);
                } else if (type.equals("change_request")) {//改变上报事件
                    mService.performEdit(messageJson);
                } else if (type.equals("event_binding_request")) {//后台选中按钮下发绑定事件
                    mService.bindEvents(messageJson);
                } else if (type.equals("clear_request")) {//清除请求
                    mService.clearEdits(messageJson);
                } else if (type.equals("tweak_request")) {//上报事件
                    mService.setTweaks(messageJson);
                }
            } catch (final JSONException e) {
                MPLog.e(LOGTAG, "Bad JSON received:" + message, e);
            }
        }

根据后台发送type来区分是要干什么。

主要讲一下snapshot_request(发送截图和控件坐标)和event_binding_request(后台选中按钮下发绑定事件)这两种类型。

1 snapshot_request(发送截图和控件坐标)

    首先mService.sendSnapshot(messageJson);实现方法在ViewCrawler类,这个类比较重要,mService其实是ViewCrawler的一个内部内Editor

 private class Editor implements EditorConnection.Editor {

        @Override
        public void sendSnapshot(JSONObject message) {
            final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_STATE_FOR_EDITING);
            msg.obj = message;
            mMessageThreadHandler.sendMessage(msg);
        }
    .....
}
ViewCrawler里面有一个ViewCrawlerHandler,顾名思义这其实是继承自Handler,
这里发送了一个ViewCrawler.MESSAGE_SEND_STATE_FOR_EDITING消息,那么对应的我们在handleMessage方法应该要处理这个消息
@Override
        public void handleMessage(Message msg) {
            mStartLock.lock();
            try {

                final int what = msg.what;
                switch (what) {
                    /*case MESSAGE_INITIALIZE_CHANGES:
                        loadKnownChanges();
                        break;*/
                    case MESSAGE_CONNECT_TO_EDITOR:
                        connectToEditor();
                        break;
                    case MESSAGE_SEND_DEVICE_INFO:
                        sendDeviceInfo();
                        break;
                    case MESSAGE_SEND_STATE_FOR_EDITING:
                        sendSnapshot((JSONObject) msg.obj);
                        break;
                    case MESSAGE_SEND_EVENT_TRACKED:
                        sendReportTrackToEditor((String) msg.obj);
                        break;
                    case MESSAGE_SEND_LAYOUT_ERROR:
                        sendLayoutError((ViewVisitor.LayoutErrorMessage) msg.obj);
                        break;
                    case MESSAGE_VARIANTS_RECEIVED:
                        handleVariantsReceived((JSONArray) msg.obj);
                        break;
                    case MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED:
                        handleEditorChangeReceived((JSONObject) msg.obj);
                        break;
                    case MESSAGE_EVENT_BINDINGS_RECEIVED:
                        handleEventBindingsReceived((JSONArray) msg.obj);
                        break;
                    case MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED:
                        handleEditorBindingsReceived((JSONObject) msg.obj);
                        break;
                    case MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED:
                        handleEditorBindingsCleared((JSONObject) msg.obj);
                        break;
                    case MESSAGE_HANDLE_EDITOR_TWEAKS_RECEIVED:
                        handleEditorTweaksReceived((JSONObject) msg.obj);
                        break;
                    case MESSAGE_HANDLE_EDITOR_CLOSED:
                        handleEditorClosed();
                        break;
                    case MESSAGE_PERSIST_VARIANTS_RECEIVED:
                        persistVariants((JSONArray) msg.obj);
                        break;
                }
            } finally {
                mStartLock.unlock();
            }
        }
当然要走到这里肯定要先连接到后台,其实就connectToEditor()方法(这个不详细介绍)。
回到刚刚我们需要接受到后台下发的消息之后上传我们当前界面的截图和控件的详细信息给到后台,也就是消息处理的sendSnapshot((JSONObject) msg.obj);跟进去看会拼接一个json
 try {
                writer.write("{");
                writer.write("\"type\": \"snapshot\",");
                writer.write("\"payload\": {");
                {
                    writer.write("\"activities\":");
                    writer.flush();
                    mSnapshot.snapshots(mEditState, out);
                }

                final long snapshotTime = System.currentTimeMillis() - startSnapshot;
                writer.write(",\"snapshot_time_millis\": ");
                writer.write(Long.toString(snapshotTime));

                writer.write("}"); // } payload
                writer.write("}"); // } whole message
            } catch (final IOException e) {
                MPLog.e(LOGTAG, "Can't write snapshot request to server", e);
            } finally {
                try {
                    writer.close();
                } catch (final IOException e) {
                    MPLog.e(LOGTAG, "Can't close writer.", e);
                }
            }

  重要的是在mSnapshot.snapshots(mEditState, out)方法实现在ViewSnapshot类的snapshots方法,这个方法实现了对当前屏幕的截屏已经遍历当前界面rootView的遍历获取各个控件的左边等一些详细的信息。

 /**
     * Take a snapshot of each activity in liveActivities. The given UIThreadSet will be accessed
     * on the main UI thread, and should contain a set with elements for every activity to be
     * snapshotted. Given stream out will be written on the calling thread.
     */
    public void snapshots(UIThreadSet<Activity> liveActivities, OutputStream out) throws IOException {
        mRootViewFinder.findInActivities(liveActivities);
        final FutureTask<List<RootViewInfo>> infoFuture = new FutureTask<List<RootViewInfo>>(mRootViewFinder);
        mMainThreadHandler.post(infoFuture);

        final OutputStreamWriter writer = new OutputStreamWriter(out);
        List<RootViewInfo> infoList = Collections.<RootViewInfo>emptyList();
        writer.write("[");

        try {
            infoList = infoFuture.get(1, TimeUnit.SECONDS);
        } catch (final InterruptedException e) {
            MPLog.d(LOGTAG, "Screenshot interrupted, no screenshot will be sent.", e);
        } catch (final TimeoutException e) {
            MPLog.i(LOGTAG, "Screenshot took more than 1 second to be scheduled and executed. No screenshot will be sent.", e);
        } catch (final ExecutionException e) {
            MPLog.e(LOGTAG, "Exception thrown during screenshot attempt", e);
        }

        final int infoCount = infoList.size();
        for (int i = 0; i < infoCount; i++) {
            if (i > 0) {
                writer.write(",");
            }
            final RootViewInfo info = infoList.get(i);
            writer.write("{");
            writer.write("\"activity\":");
            writer.write(JSONObject.quote(info.activityName));
            writer.write(",");         
            writer.write("\"scale\":");
            writer.write(String.format("%s", info.scale));
            writer.write(",");
            writer.write("\"serialized_objects\":");
            {
                final JsonWriter j = new JsonWriter(writer);
                j.beginObject();
                j.name("rootObject").value(info.rootView.hashCode());
                j.name("objects");
                snapshotViewHierarchy(j, info.rootView);
                j.endObject();
                j.flush();
            }
            writer.write(",");
            writer.write("\"screenshot\":");
            writer.flush();
            info.screenshot.writeBitmapJSON(Bitmap.CompressFormat.PNG, 100, out);
            writer.write("}");
        }

        writer.write("]");
        writer.flush();
    }

我们先看怎么截屏:

执行了一个Future,Future传入了一个从activity中得到的mRootViewFinder,mMainThreadHandler.post(infoFuture)

执行Callable的call()方法,实现为
 

        @Override
        public List<RootViewInfo> call() throws Exception {
            mRootViews.clear();

            final Set<Activity> liveActivities = mLiveActivities.getAll();

            for (final Activity a : liveActivities) {
                final String activityName = a.getClass().getCanonicalName();
                final View rootView = a.getWindow().getDecorView().getRootView();
                        
            a.getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
                final RootViewInfo info = new RootViewInfo(activityName, rootView);
                mRootViews.add(info);
            }

            final int viewCount = mRootViews.size();
            for (int i = 0; i < viewCount; i++) {
                final RootViewInfo info = mRootViews.get(i);
                takeScreenshot(info);
            }

            return mRootViews;
        }

takeScreenshot(info)截取当前屏幕照片,这个方法实际是根据View反射拿到createSnapshot方法,获取当前屏幕的截图

private void takeScreenshot(final RootViewInfo info) {
            final View rootView = info.rootView;
            Bitmap rawBitmap = null;

            try {
                final Method createSnapshot = View.class.getDeclaredMethod("createSnapshot", Bitmap.Config.class, Integer.TYPE, Boolean.TYPE);
                createSnapshot.setAccessible(true);
                rawBitmap = (Bitmap) createSnapshot.invoke(rootView, Bitmap.Config.RGB_565, Color.WHITE, false);
            } catch (final NoSuchMethodException e) {
                MPLog.v(LOGTAG, "Can't call createSnapshot, will use drawCache", e);
            } catch (final IllegalArgumentException e) {
                MPLog.d(LOGTAG, "Can't call createSnapshot with arguments", e);
            } catch (final InvocationTargetException e) {
                MPLog.e(LOGTAG, "Exception when calling createSnapshot", e);
            } catch (final IllegalAccessException e) {
                MPLog.e(LOGTAG, "Can't access createSnapshot, using drawCache", e);
            } catch (final ClassCastException e) {
                MPLog.e(LOGTAG, "createSnapshot didn't return a bitmap?", e);
            }

            Boolean originalCacheState = null;
            try {
                if (null == rawBitmap) {
                    originalCacheState = rootView.isDrawingCacheEnabled();
                    rootView.setDrawingCacheEnabled(true);
                    rootView.buildDrawingCache(true);
                    rawBitmap = rootView.getDrawingCache();
                }
            } catch (final RuntimeException e) {
                MPLog.v(LOGTAG, "Can't take a bitmap snapshot of view " + rootView + ", skipping for now.", e);
            }

            float scale = 1.0f;
            if (null != rawBitmap) {
                final int rawDensity = rawBitmap.getDensity();

                if (rawDensity != Bitmap.DENSITY_NONE) {
                    scale = ((float) mClientDensity) / rawDensity;
                }

                final int rawWidth = rawBitmap.getWidth();
                final int rawHeight = rawBitmap.getHeight();
                final int destWidth = (int) ((rawBitmap.getWidth() * scale) + 0.5);
                final int destHeight = (int) ((rawBitmap.getHeight() * scale) + 0.5);

                if (rawWidth > 0 && rawHeight > 0 && destWidth > 0 && destHeight > 0) {
                    mCachedBitmap.recreate(destWidth, destHeight, mClientDensity, rawBitmap);
                }
            }

            if (null != originalCacheState && !originalCacheState) {
                rootView.setDrawingCacheEnabled(false);
            }
            info.scale = scale;
            info.screenshot = mCachedBitmap;
        }

如果createSnapshot方法拿到的是null的话还会根据view的getDrawingCache去拿到view绘制的一个缓存图像。

并且根据当前手机屏幕的分辨率来缩放一次。保证跟手机的分辨率一致。再上传给服务器,到这里屏幕截图就完成了。

接下来就是遍历rootView,并获取子view的详细信息了,

void snapshotViewHierarchy(JsonWriter j, View rootView)
        throws IOException {
        j.beginArray();
        snapshotView(j, rootView);
        j.endArray();
    }

snapshotView(j, rootView)先把当前rootview信息添加到json中,然后

        if (view instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) view;
            final int childCount = group.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = group.getChildAt(i);
                // child can be null when views are getting disposed.
                if (null != child) {
                    j.value(child.hashCode());
                }
            }
        }
        j.endArray();
        j.endObject();

        if (view instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) view;
            final int childCount = group.getChildCount();
            for (int i = 0; i < childCount; i++) {
                final View child = group.getChildAt(i);
                // child can be null when views are getting disposed.
                if (null != child) {
                    snapshotView(j, child);
                }
            }
        }

然后遍历调用snapshotView将所有子view的信息都添加到json中。

最终给后台的json格式如下:

{
    "type": "snapshot", 
    "payload": {
        "activities": [
            {
                "activity": "com.pingan.pamagicpaneldemo.MainActivity", 
                "pageid": "SYZX", 
                "pagename": "SYZX", 
                "scale": 0.33333334, 
                "serialized_objects": {
                    "rootObject": 65838376, 
                    "objects": [
                        {
                            "hashCode": 65838376, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1920, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "com.android.internal.policy.DecorView", 
                                "android.widget.FrameLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.ColorDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 1920
                                }, 
                                "color": -328966
                            }, 
                            "subviews": [
                                252438848, 
                                10748025
                            ]
                        }, 
                        {
                            "hashCode": 252438848, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1920, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.widget.LinearLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                107370860, 
                                240855861
                            ]
                        }, 
                        {
                            "hashCode": 107370860, 
                            "id": 16909322, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 0, 
                            "height": 0, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 8, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.view.ViewStub", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 8, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 240855861, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 60, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1860, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.widget.FrameLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                169189464
                            ]
                        }, 
                        {
                            "hashCode": 169189464, 
                            "id": 2131230770, 
                            "mp_id_name": "decor_content_parent", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1860, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.ActionBarOverlayLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                14666390, 
                                84354583
                            ]
                        }, 
                        {
                            "hashCode": 14666390, 
                            "id": 16908290, 
                            "mp_id_name": "android:content", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 168, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1692, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.ContentFrameLayout", 
                                "android.widget.FrameLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                155356141
                            ]
                        }, 
                        {
                            "hashCode": 155356141, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 1692, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.widget.LinearLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                239467042, 
                                239996083, 
                                76292720, 
                                80280041, 
                                149788270, 
                                135446799, 
                                124319132
                            ]
                        }, 
                        {
                            "hashCode": 239467042, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 225, 
                            "height": 57, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatTextView", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "text": "Hello World!", 
                            "textColor": -1979711488, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 239996083, 
                            "id": 2131230745, 
                            "mp_id_name": "btn1", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 57, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击1", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 76292720, 
                            "id": 2131230746, 
                            "mp_id_name": "btn2", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 201, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击2", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 80280041, 
                            "id": 2131230747, 
                            "mp_id_name": "btn3", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 345, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击3", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 149788270, 
                            "id": 2131230748, 
                            "mp_id_name": "btn4", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 489, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击4", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 135446799, 
                            "id": 2131230749, 
                            "mp_id_name": "btn5", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 633, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击5", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 124319132, 
                            "id": 2131230750, 
                            "mp_id_name": "btn6", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 777, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 144, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.AppCompatButton", 
                                "android.widget.Button", 
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": true, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.RippleDrawable", 
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 144
                                }
                            }, 
                            "text": "点击6", 
                            "textColor": -570425344, 
                            "fontSize": 42, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 84354583, 
                            "id": 2131230723, 
                            "mp_id_name": "action_bar_container", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 168, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.ActionBarContainer", 
                                "android.widget.FrameLayout", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.support.v7.widget.ActionBarBackgroundDrawableV21", 
                                    "android.support.v7.widget.ActionBarBackgroundDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 168
                                }
                            }, 
                            "subviews": [
                                146670918, 
                                145212167
                            ]
                        }, 
                        {
                            "hashCode": 146670918, 
                            "id": 2131230721, 
                            "mp_id_name": "action_bar", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 168, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.Toolbar", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [
                                74140253, 
                                54865874
                            ]
                        }, 
                        {
                            "hashCode": 74140253, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 43, 
                            "left": 48, 
                            "width": 100, 
                            "height": 81, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.widget.TextView", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": true, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "text": "app", 
                            "textColor": -1, 
                            "fontSize": 60, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 54865874, 
                            "id": -1, 
                            "mp_id_name": null, 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 1080, 
                            "width": 0, 
                            "height": 168, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.ActionMenuView", 
                                "android.support.v7.widget.LinearLayoutCompat", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 145212167, 
                            "id": 2131230728, 
                            "mp_id_name": "action_context_bar", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 0, 
                            "height": 0, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 8, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.support.v7.widget.ActionBarContextView", 
                                "android.support.v7.widget.AbsActionBarView", 
                                "android.view.ViewGroup", 
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 8, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.LayerDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 0, 
                                    "top": 0, 
                                    "bottom": 0
                                }
                            }, 
                            "subviews": [ ]
                        }, 
                        {
                            "hashCode": 10748025, 
                            "id": 16908335, 
                            "mp_id_name": "android:statusBarBackground", 
                            "contentDescription": null, 
                            "tag": null, 
                            "top": 0, 
                            "left": 0, 
                            "width": 1080, 
                            "height": 60, 
                            "scrollX": 0, 
                            "scrollY": 0, 
                            "visibility": 0, 
                            "translationX": 0, 
                            "translationY": 0, 
                            "classes": [
                                "android.view.View"
                            ], 
                            "importantForAccessibility": false, 
                            "clickable": false, 
                            "alpha": 1, 
                            "hidden": 0, 
                            "background": {
                                "classes": [
                                    "android.graphics.drawable.ColorDrawable", 
                                    "android.graphics.drawable.Drawable"
                                ], 
                                "dimensions": {
                                    "left": 0, 
                                    "right": 1080, 
                                    "top": 0, 
                                    "bottom": 60
                                }, 
                                "color": -13615201
                            }, 
                            "subviews": [ ]
                        }
                    ]
                }, 
                "screenshot": "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mHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQ9u9j1NyNowBRZQAAAABJRU5ErkJg"
            }
        ], 
        "snapshot_time_millis": 172
    }
}

 

event_binding_request(后台选中按钮下发绑定事件)

 后台埋点下发事件最终也到handleMessage中处理对应的消息为:

 case MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED:
      handleEditorBindingsReceived((JSONObject) msg.obj);
      break;

在处理消息方法中调用applyVariantsAndEventBindings()方法,大致我们可以推断是在这个方法里面对下发的控件进行拦截,然后插入自己的监听事件。进去发现

{
                if (mEditorEventBindings.size() == 0 && mOriginalEventBindings.size() == 0) {
                    for (MPPair<String, JSONObject> changeInfo : mPersistentEventBindings) {
                        try {
                            final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
                            newVisitors.add(new MPPair<String, ViewVisitor>(changeInfo.first, visitor));
                        } catch (final EditProtocol.InapplicableInstructionsException e) {
                            MPLog.i(LOGTAG, e.getMessage());
                        } catch (final EditProtocol.BadInstructionsException e) {
                            MPLog.e(LOGTAG, "Bad persistent event binding cannot be applied.", e);
                        }
                    }
                }
            }

            {
                for (MPPair<String, JSONObject> changeInfo : mEditorEventBindings.values()) {
                    try {
                        final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
                        newVisitors.add(new MPPair<String, ViewVisitor>(changeInfo.first, visitor));
                    } catch (final EditProtocol.InapplicableInstructionsException e) {
                        MPLog.i(LOGTAG, e.getMessage());
                    } catch (final EditProtocol.BadInstructionsException e) {
                        MPLog.e(LOGTAG, "Bad editor event binding cannot be applied.", e);
                    }
                }
            }

            final Map<String, List<ViewVisitor>> editMap = new HashMap<String, List<ViewVisitor>>();
            final int totalEdits = newVisitors.size();
            for (int i = 0; i < totalEdits; i++) {
                final MPPair<String, ViewVisitor> next = newVisitors.get(i);
                final List<ViewVisitor> mapElement;
                if (editMap.containsKey(next.first)) {
                    mapElement = editMap.get(next.first);
                } else {
                    mapElement = new ArrayList<ViewVisitor>();
                    editMap.put(next.first, mapElement);
                }
                mapElement.add(next.second);
            }

            mEditState.setEdits(editMap);

将下发的事件转化为ViewVisitor对象实现了一个Accumulator接口,这个接口实现方法就是反射拿到view的getAccessibilityDelegate然后替换成自己的AccessibilityDelegate,从而在用户触发控件的时候使用自己的AccessibilityDelegate获取信息上报给服务器,最终实现埋点的效果(谷歌提供的 Accessibility 相关 api ,具体可以详细的了解一下)。

接着看怎么实现的,最重要的是mEditState.setEdits(editMap)方法,我们再进去看一下,

 /**
     * Sets the entire set of edits to be applied to the application.
     *
     * Edits are represented by ViewVisitors, batched in a map by the String name of the activity
     * they should be applied to. Edits to apply to all views should be in a list associated with
     * the key {@code null} (Not the string "null", the actual null value!)
     *
     * The given edits will completely replace any existing edits.
     *
     * setEdits can be called from any thread, although the changes will occur (eventually) on the
     * UI thread of the application, and may not appear immediately.
     *
     * @param newEdits A Map from activity name to a list of edits to apply
     */
    // Must be thread-safe
    public void setEdits(Map<String, List<ViewVisitor>> newEdits) {
        // Delete images that are no longer needed

        synchronized (mCurrentEdits) {
            for (final EditBinding stale : mCurrentEdits) {
                stale.kill();
            }
            mCurrentEdits.clear();
        }

        synchronized(mIntendedEdits) {
            mIntendedEdits.clear();
            mIntendedEdits.putAll(newEdits);
        }

        applyEditsOnUiThread();
    }

遍历并调用stale.kill()方法,里面执行了一个handle.post(this)方法,然后我们找到EditState的run方法

@Override
        public void run() {
            if (!mAlive) {
                return;
            }

            final View viewRoot = mViewRoot.get();
            if (null == viewRoot || mDying) {
                cleanUp();
                return;
            }
            // ELSE View is alive and we are alive

            mEdit.visit(viewRoot);
            mHandler.removeCallbacks(this);
            mHandler.postDelayed(this, 1000);
        }

里面调用mEdit.visit(viewRoot),最终调用Pathfinder类的findTargetsInMatchedView方法里面就会实现上面说到的接口Accumulator的accumulate方法


        if (remainingPath.isEmpty()) {
            // Nothing left to match- we're found!
            accumulator.accumulate(alreadyMatched);
            return;
        }

那么accumulate里面又是怎么实现的呢,接下去看

@Override
        public void accumulate(View found) {
            final View.AccessibilityDelegate realDelegate = getOldDelegate(found);//先获取到view的AccessibilityDelegate
            if (realDelegate instanceof TrackingAccessibilityDelegate) {
                final TrackingAccessibilityDelegate currentTracker = (TrackingAccessibilityDelegate) realDelegate;
                if (currentTracker.willFireEvent(getEventName())) {
                    return; // Don't double track
                }
            }

            // We aren't already in the tracking call chain of the view
            final TrackingAccessibilityDelegate newDelegate = new TrackingAccessibilityDelegate(realDelegate);
            found.setAccessibilityDelegate(newDelegate);//替换成自己的AccessibilityDelegate
            mWatching.put(found, newDelegate);
        }

        @Override
        protected String name() {
            return getEventName() + " event when (" + mEventType + ")";
        }
        //反射拿到view的AccessibilityDelegate
        private View.AccessibilityDelegate getOldDelegate(View v) {
            View.AccessibilityDelegate ret = null;
            try {
                Class<?> klass = v.getClass();
                Method m = klass.getMethod("getAccessibilityDelegate");
                ret = (View.AccessibilityDelegate) m.invoke(v);
            } catch (NoSuchMethodException e) {
                // In this case, we just overwrite the original.
            } catch (IllegalAccessException e) {
                // In this case, we just overwrite the original.
            } catch (InvocationTargetException e) {
                MPLog.w(LOGTAG, "getAccessibilityDelegate threw an exception when called.", e);
            }

            return ret;
        }

        private class TrackingAccessibilityDelegate extends View.AccessibilityDelegate {
            public TrackingAccessibilityDelegate(View.AccessibilityDelegate realDelegate) {
                mRealDelegate = realDelegate;
            }

            public View.AccessibilityDelegate getRealDelegate() {
                return mRealDelegate;
            }

            public boolean willFireEvent(final String eventName) {
                if (getEventName() == eventName) {
                    return true;
                } else if (mRealDelegate instanceof TrackingAccessibilityDelegate) {
                    return ((TrackingAccessibilityDelegate) mRealDelegate).willFireEvent(eventName);
                } else {
                    return false;
                }
            }

            public void removeFromDelegateChain(final TrackingAccessibilityDelegate other) {
                if (mRealDelegate == other) {
                    mRealDelegate = other.getRealDelegate();
                } else if (mRealDelegate instanceof TrackingAccessibilityDelegate) {
                    final TrackingAccessibilityDelegate child = (TrackingAccessibilityDelegate) mRealDelegate;
                    child.removeFromDelegateChain(other);
                } else {
                    // We can't see any further down the chain, just return.
                }
            }

            @Override
            public void sendAccessibilityEvent(View host, int eventType) {
                if (eventType == mEventType) {
                    fireEvent(host);//事件上传
                }

                if (null != mRealDelegate) {
                    mRealDelegate.sendAccessibilityEvent(host, eventType);
                }
            }

            private View.AccessibilityDelegate mRealDelegate;
        }
fireEvent处理我们的事件,里面实现了event的OnEventListener的OnEvent方法,最终实现在
DynamicEventTracker类
@Override
    public void OnEvent(View v, String eventName, boolean debounce) {
        // Will be called on the UI thread
        final long moment = System.currentTimeMillis();
        final JSONObject properties = new JSONObject();
        try {
            final String text = textPropertyFromView(v);
            properties.put("$text", text);
            properties.put("$from_binding", true);

            // We may call track much later, but we'll be tracking something
            // that happened right at moment.
            properties.put("time", moment / 1000);
        } catch (JSONException e) {
            MPLog.e(LOGTAG, "Can't format properties from view due to JSON issue", e);
        }

        if (debounce) {
            final Signature eventSignature = new Signature(v, eventName);
            final UnsentEvent event = new UnsentEvent(eventName, properties, moment);

            // No scheduling mTask without holding a lock on mDebouncedEvents,
            // so that we don't have a rogue thread spinning away when no events
            // are coming in.
            synchronized (mDebouncedEvents) {
                final boolean needsRestart = mDebouncedEvents.isEmpty();
                mDebouncedEvents.put(eventSignature, event);
                if (needsRestart) {
                    mHandler.postDelayed(mTask, DEBOUNCE_TIME_MILLIS);
                }
            }
        } else {
            mMixpanel.track(eventName, properties);//实现埋点
        }
    }

我们看到最终的埋点 mMixpanel.track(eventName, properties)

<think>嗯,用户现在想了解可视化埋点的技术实现和工具推荐。根据之前的对话,用户已经询问过后端埋点的实现方法和最佳实践,现在转向可视化埋点,可能是在考虑如何在不依赖开发团队的情况下进行埋点管理,或者希望降低埋点成本。用户可能属于产品经理、业务分析师或非技术背景的角色,需要一种更直观的方式来设置埋点。 首先,我需要回顾之前提供的引用内容。引用[1]提到可视化埋点属于前端全埋点,通过可视化工具让非技术人员管理事件,但存在自定义不足的问题。引用[2]说明可视化埋点通常在Web端使用截屏方案,Mixpanel是例子之一。引用[4]则比较了代码埋点可视化埋点和全埋点的区别,推荐代码埋点用于高准确性需求,但可视化适合快速部署。 接下来,用户的问题明确需要技术实现和工具推荐,所以应分两部分回答。技术实现方面,需要涵盖事件配置、元素追踪、数据采集、动态加载等步骤,可能结合AOP或DOM事件监听。工具推荐部分,需要列出主流工具如Mixpanel、GrowingIO、神策,并比较其优缺点,同时提到开源方案如Sentry。 需要注意的是用户之前的问题涉及后端埋点,这里要强调可视化埋点的局限性,比如无法覆盖后端或异步操作,可能需要结合其他埋点方式。此外,引用中提到的第三方工具和实现方案需要准确对应,例如Mixpanel的截屏方案,以及动态加载配置避免发版。 还需要考虑用户可能关心的数据关联问题,比如如何将可视化埋点的前端数据与后端埋点结合,但用户当前问题可能不需要深入这部分,不过简要提及可以增强回答的全面性。最后,确保回答结构清晰,技术实现分步骤,工具推荐分商业和开源,优缺点明确,帮助用户做决策。</think>### 可视化埋点的技术实现与工具推荐 可视化埋点通过**可视化界面配置埋点事件**,无需开发人员手动编码,主要服务于非技术人员(如产品经理、运营人员)。其核心技术实现分为以下步骤: --- #### 一、技术实现原理 1. **事件配置与元素绑定** - **元素识别**:通过可视化工具(如浏览器插件或SDK)对页面元素(如按钮、表单)进行截图或DOM结构解析,生成唯一标识(如`XPath`或`CSS选择器`)。 - **事件定义**:用户直接在可视化界面选择需要追踪的元素,并定义事件类型(如点击、曝光)。 2. **动态埋点注入** - **配置下发**:埋点配置信息(如元素路径、事件类型)通过服务端动态下发到客户端,无需更新代码。 - **事件监听**:客户端SDK通过`DOM事件监听`或`AOP(面向切面编程)`自动捕获用户行为。 ```javascript // 示例:动态绑定点击事件(伪代码) document.querySelectorAll('[data-track="button"]').forEach(element => { element.addEventListener('click', (event) => { sendTrackData('button_click', { element: event.target.id }); }); }); ``` 3. **数据采集与上报** - **数据封装**:采集事件关联的上下文信息(如用户ID、页面URL、设备信息)。 - **异步上报**:通过`Web Beacon`或`HTTP请求`将数据发送到日志服务器,避免阻塞页面性能[^1]。 --- #### 二、主流工具推荐 | 工具类型 | 代表工具 | 适用场景 | 特点与局限性 | |----------------|------------------------|----------------------------------|-----------------------------------------------------------------------------| | **商业SaaS** | **Mixpanel** | 用户行为分析、A/B测试 | 提供可视化埋点配置界面,但跨平台埋点(如后端服务)需结合代码埋点[^2]。 | | **国内方案** | **GrowingIO** | 电商、金融行业精细化运营 | 支持无埋点+可视化埋点混合模式,但对动态页面(如SPA)需额外适配[^3]。 | | **开源方案** | **Sentry (性能监控)** | 错误追踪与性能监控 | 可与可视化埋点工具(如Matomo)集成,但需自行开发配置界面[^4]。 | | **全链路方案** | **神策数据** | 复杂业务场景(如游戏、IoT) | 支持前后端埋点数据关联,但需较高的定制化成本[^5]。 | --- #### 三、关键挑战与解决方案 1. **动态元素追踪** - **问题**:SPA(单页应用)或异步加载内容导致元素无法静态绑定。 - **方案**:使用`MutationObserver`监听DOM变化,动态更新埋点配置。 2. **数据准确性验证** - **问题**:可视化埋点可能遗漏异步事件或跨页行为。 - **方案**:结合**全埋点(无痕埋点)**兜底采集,并通过数据抽样校验[^6]。 3. **跨平台一致性** - **问题**:Web、App、小程序等平台埋点规则不一致。 - **方案**:采用统一埋点SDK(如`Firebase`)或自定义抽象层(如JSON配置中心)。 --- #### 四、选型建议 - **轻量级需求**:优先使用SaaS工具(如Mixpanel),快速搭建埋点体系。 - **高定制化需求**:选择开源方案(如Matomo)或自研SDK,结合动态配置下发。 - **混合场景**:采用**可视化埋点+代码埋点**组合模式,覆盖前后端全链路行为[^7]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值