[dry goods] to realize a no buried point and visual buried point sdk

Preface

In this paper, based on the practice of our own project, we abstract and sort out a sdk for the realization principle of no buried point and visual buried point. At the same time, I also looked up a lot of related materials and found that in fact, they are similar in the principle of no buried point.

sdk only introduces and implements the non buried point of click event, and other users' behavior is similar.
sdk github address https://github.com/mfaying/we...

No burying point

The non buried point is actually the full buried point. As long as the sdk is embedded, the data can be collected automatically. Because no additional burying point code is needed, it can also be called no burying point.

Demonstration

First of all, let's take a look at the demonstration effect of sdk Experience URL (https://www.readingblog.cn/#/...

The parent page (embedded point management page) embeds an iframe, pointing to a sub page (embedded point page of the sdk). The sdk can automatically calculate the unique identification of the click element (named "domPath" here), as well as the size, location and other relevant information of the element, and send the data to the back-end. At the same time, this data will also be sent to the buried point management page across domains, and the management page will do visual buried point work based on these data. In the figure, the management page can get the information of the element (including size, location, domPath, etc.).

How to use

sdk is very simple to use
First, introduce the sdk code into the head tag

<script src="https://www.readingblog.cn/web-log-sdk-1.0.0.min.js"></script>

Then, initialize the sdk. During initialization, you can pass in some custom parameters. After initialization, the sdk is already working in your page. Isn't it convenient!

new WebLogger.AutoLogger({
  debug: true,
});

Here is a simple demo page, which is opened in the browser. Click at will. You can see the automatically printed buried point data in the console every time you click.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>web-log-sdk</title>
  <script src="https://www.readingblog.cn/web-log-sdk-1.0.0.min.js"></script>
  <script>
    new WebLogger.AutoLogger({
      debug: true,
    });
  </script>
</head>
<body>
  <div>
    1
    <div id='1'>
      2
      <div id="1">3</div>
      <div>4</div>
    </div>
  </div>
  <div>5</div>
</body>
</html>

Principle of no buried point

Actually, it listens for click events on document.body. So all the click operations on the page will send the embedded point data.

_autoClickCollection = () => {
  event.on(doc.body, 'click', this._autoClickHandle);
}

There is a problem here. Although the click operation can trigger the sending of embedded data, we must ensure that the data sent is valuable.
The key point here is that we need to know which element in the page triggers the user's click operation. Because it's automatic embedding, we have to think about a way to tag page elements. Although the element has class, nodeName and other identifications, it is impossible for the entire page to locate a single element. Although the id of the element is unique according to the specification, only individual elements will be marked with the id attribute.
So we think of a way, because the dom structure of the whole html is like a tree, for any element (node), we first find its parent node, and then find its parent node, so that we can go all the way back to HTML (root node element), thus forming a path, which we take as the unique identification of the element. Of course, if the "domPath" is reversed, it is arranged by "from parent to child", for example, HTML > body > (APP). In this way, we can only select the clicked element through document.querySelector.
The specific implementation is as follows:

const _getLocalNamePath = (elm) => {
  const domPath = [];
  let preCount = 0;
  for (let sib = elm.previousSibling; sib; sib = sib.previousSibling) {
    if (sib.localName == elm.localName) preCount ++;
  }
  if (preCount === 0) {
    domPath.unshift(elm.localName);
  } else {
    domPath.unshift(`${elm.localName}:nth-of-type(${preCount + 1})`);
  }
  return domPath;
}

const getDomPath = (elm) => {
  try {
    const allNodes = document.getElementsByTagName('*');
    let domPath = [];
    for (; elm && elm.nodeType == 1; elm = elm.parentNode) {
      if (elm.hasAttribute('id')) {
        let uniqueIdCount = 0
        for (var n = 0; n < allNodes.length; n++) {
          if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++;
          if (uniqueIdCount > 1) break;
        }
        if (uniqueIdCount == 1) {
          domPath.unshift(`#${elm.getAttribute('id')}`);
        } else {
          domPath.unshift(..._getLocalNamePath(elm));
        }
      } else {
        domPath.unshift(..._getLocalNamePath(elm));
      }
    }
    return domPath.length ? domPath.join('>') : null
  } catch (err) {
    console.log(err)
    return null;
  }
}

export default getDomPath;

We also do some processing in the code, for example, when there are multiple siblings with the same localName, common examples are

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

We distinguish by: nth of type selector.

If there is an id attribute, in order to ensure that the id is unique (the specification requires that it must be unique, but developers may also inadvertently assign duplicate id attributes), we have checked it. If it is unique, we use the id as the tag, which can improve the efficiency of the selector.

After determining the unique identification of the element, the next thing is very simple. We just need to get the required embedded data and send it to the back end.

For example, getting element location information

const getBoundingClientRect = (elm) => {
  const rect = elm.getBoundingClientRect();
  const width = rect.width || rect.right - rect.left;
  const height = rect.height || rect.bottom - rect.top;
  return {
    width,
    height,
    left: rect.left,
    top: rect.top,
  };
}

export default getBoundingClientRect;

Get platform information

import { ua } from '../common/bom';
import platform from 'platform';

const getPlatform = () => {
  const platformInfo = {};

  platformInfo.os = `${platform.os.family} ${platform.os.version}` || '';
  platformInfo.bn = platform.name || '';
  platformInfo.bv = platform.version || '';
  platformInfo.bl = platform.layout || '';
  platformInfo.bd = platform.description || '';

  const wechatInfo = ua.match(/MicroMessenger\/([\d\.]+)/i);
  const wechatNetType = ua.match(/NetType\/([\w\.]+)/i);
  if (wechatInfo) {
    platformInfo.mmv = wechatInfo[1] || '';
  }
  if (wechatNetType) {
    platformInfo.net = wechatNetType[1] || '';
  }

  return platformInfo;
}

export default getPlatform;

The current url, reference url, title, event trigger time and other information can be added. This is a buried data sent by my sdk

{
    "eventData": {
        "et": "click",
        "ed": "auto_click",
        "text": "Reference resources: Elasticsear...icsearch 2.x Edition",
        "nodeName": "p",
        "domPath": "html>body>#app>section>section>main>div:nth-of-type(5)>div>p>p",
        "offsetX": "0.768987",
        "offsetY": "0.333333",
        "pageX": 263,
        "pageY": 167,
        "scrollX": 0,
        "scrollY": 0,
        "left": 20,
        "top": 153,
        "width": 316,
        "height": 42,
        "rUrl": "http://localhost:8080/",
        "docTitle": "blog",
        "cUrl": "http://localhost:8080/#/blog/article/74",
        "t": 1573987603156
    },
    "optParams": {},
    "platform": {
        "os": "Android 6.0",
        "bn": "Chrome Mobile",
        "bv": "77.0.3865.120",
        "bl": "Blink",
        "bd": "Chrome Mobile 77.0.3865.120 on Google Nexus 5 (Android 6.0)"
    },
    "appID": "",
    "sdk": {
        "type": "js",
        "version": "1.0.0"
    }
}

Realization of visual circle selection of buried points

In general, iframe is used to embed the embedded page. At this time, the child page is the embedded page (introduced by iframe), and the parent page is the management page. Since the src attribute of iframe supports cross domain loading of resources, any embedded page can be embedded.

But in order to realize the circle selection function, the communication between the embedded point page and the management page must be realized, because the management page does not know the embedded point information. And because the embedded page is cross domain, the management page can not operate the embedded page at all.

Here we need sdk to implement a communication mechanism. We use the general cross domain communication scheme postMessage.
Add a postmsgops field to the configuration item of the sdk to configure the postMessage parameter. The default value of postmsgops is an empty array, that is, it allows the embedded page to send data to multiple sources, and its default configuration will not send data through postMessage.
The configuration example of the postmsgops field is as follows:

new AutoLogger({
  debug: true,
  postMsgOpts: [{
    targetWindow: window.parent,
    targetOrigin,
  }, {
    targetWindow: window,
    targetOrigin: curOrigin,
  }],
});

In this way, the embedded point data to be sent will also call the postMessage api to send a copy.

postMsgOpts.forEach((opt) => {
  const { targetWindow, targetOrigin } = opt;
  targetWindow.postMessage({ logData: JSON.stringify(logData) }, targetOrigin)
});

Let's go back to the analysis and demonstration of how to realize the visual buried point. First, iframe of the management page loads the embedded page. Because the embedded page introduces sdk, clicking any element in the page will send a copy of the embedded data to the management page through postMessage. The data here includes the size and location of the element, domPath, and so on. As long as the management page listens to the "message" event, it can get the data from the sub page (embedded point page). In order to be interactive and friendly, the selected elements in iframe can be circled according to these information management pages. Of course, as long as the management page gets the embedded point data, it can interact with the user using the management page on this basis, do some independent configuration and pass the additional information and the information of the selected elements to the back-end, so that the back-end can process the selected elements and realize the visual embedded point.

Configuration item

Finally, I will introduce the configuration items of my sdk. First, I will refer to the default configuration

import getPlatform from '../../utils/getPlatform';

const platform = getPlatform();

export default {
  appID: '',
  // Whether to automatically collect click events
  autoClick: true,
  debug: false,
  logUrl: '',
  sdk: {
    // type
    type: 'js',
    // Edition
    version: SDK_VERSION,
  },
  // Platform parameters
  platform,
  optParams: {},
  postMsgOpts: [],
};

appID you can register an appID during initialization, so the relevant embedded points will be marked with this mark, which is equivalent to a layer of app dimension management for the embedded point data.
autoClick is true by default. When it is turned on, click events will be collected automatically (that is, click No buried point). Of course, you can realize the functions of page login, logout and browse time. At the same time, you can add switch control in the configuration, so that users can selectively enable these functions.
debug is not enabled by default. When it is enabled, the embedded data will be printed to the console for debugging.
logUrl the back-end address to receive the log
Some notes on the information of SDK itself
Platform will automatically get some platform parameters by default. You can also override it by configuring this field
Optparames custom data

Tags: Javascript SDK Attribute github Android

Posted on Sun, 17 Nov 2019 12:54:15 -0500 by alohatofu