preface

The purpose of this series of articles is to show you how to build a front-end monitoring system from scratch.

The project is open source

Project Address:

  • Github.com/bombayjs/bo… (web sdk)
  • Github.com/bombayjs/bo… (Server side, used to provide API)
  • Github.com/bombayjs/bo… (Background management system, visual data, etc.)

Your support is what keeps us moving forward.

Like please start!!!!!!

Like please start!!!!!!

Like please start!!!!!!

This article is the first part of the series, the design and development of Web Probe SDK, focusing on the functions and implementation of THE SDK.

function

  • Report the pv uv
  • Capture the error
  • Reporting Performance performance
  • Report user tracks
  • Single page support
  • hack ajax fetch
  • Report the loaded resources
  • hack console
  • hack onpopstate
  • Expose the global variable __bb
  • Bury the sum AVg MSG API

Catch exceptions

Window. onError exception handling

window.onerror = function (msg, url, row, col, error) {
    console.log({
        msg,  url,  row, col, error
    })
    return true;
};

Copy the code

Note:

  • The window. Onerror function returns true only when the exception is not raised

  • Window. onError cannot catch resource exception errors because network request exceptions do not event bubble

So instead of window.onError, we use window.addeventListener (‘error’,callback).

window.addEventListener('error', (msg, url, row, col, error) => {
    console.log(
        msg, url, row, col, error
    );
    return true;
}, true);

Copy the code

How do you distinguish between a caught exception and a resource error by instanceof? A caught exception instanceof is an ErrorEvent and a resource error instanceof is an Event

You can refer to the following code

export function handleErr(error): void {
  switch (error.type) {
    case 'error':
        error instanceof ErrorEvent ? reportCaughtError(error)  : reportResourceError(error)
      break;
    case 'unhandledrejection':
      reportPromiseError(error)
    break;
    // case 'httpError':
    //     reportHttpError(error)
    //   break;
  }
  setGlobalHealth('error')}Copy the code

Abnormal promise

Promise exceptions cannot be caught with onError or try-catch. You can listen for unhandledrejection events

window.addEventListener("unhandledrejection".function(e){
    e.preventDefault()
    console.log(e.reason);
    return true;
});

Copy the code

The iframe abnormal

If an iframe exception is a Script error, we ignore it and do not report it

Script error. Solution: www.alibabacloud.com/help/zh/doc…

Page performance

Window.performance allows us to calculate key performance indicators (KPIs) by obtaining the time spent in each of the following phases.

Tips: through the window. The navigator. Connection. The bandwidth we can estimate the bandwidth

User behavior

For the time being, the user behavior here is just the click event and console

Listen for click events

window.addEventListener('click', handleClick, true); // handleClick event definitionexport function handleClick(event) {
  var target;
  try {
    target = event.target
  } catch (u) {
    target = "<unknown>"
  }
  if (0 !== target.length) {
    var behavior: clickBehavior = {
      type: 'ui.click',
      data: {
        message: function (e) {
          if(! e || 1 ! == e.nodeType)return "";
          for (var t = e || null, n = [], r = 0, a = 0,i = ">".length, o = ""; t && r++ < 5 &&! ("html" === (o = normalTarget(t)) || r > 1 && a + n.length * i + o.length >= 80);) 
          n.push(o), a += o.length, t = t.parentNode;
          return n.reverse().join(">")}(target),}} // Empty information is not reportedif(! behavior.data.message)return
    let commonMsg = getCommonMsg()
    letmsg: behaviorMsg = { ... commonMsg, ... { t:'behavior',
        behavior,
      }
    }
    report(msg)
  }
}

Copy the code

The final data format is as follows

{
"type": "ui.click"."data": {
"message": "div#mescroll.mescroll.mescroll-bar > div.index__search-content___1Q2eh"}}Copy the code

Rewrite the console

To listen on console, we have to override the window.console method

// hack console // config.behavior. console"debug"."info"."warn"."log"."error"]
export function hackConsole() {
  if (window && window.console) {
    for (var e = Config.behavior.console, n = 0; e.length; n++) {
      var r = e[n];
      var action = window.console[r]
      if(! window.console[r])return;
        (function (r, action) {
          window.console[r] = function() {
            var i = Array.prototype.slice.apply(arguments)
            var s: consoleBehavior = {
              type: "console",
              data: {
                level: r,
                message: JSON.stringify(i),
              }
            };
            handleBehavior(s)
            action && action.apply(null, i)
          }
        })(r, action)
    }
  }
}
Copy the code

Single page support

At present, many monitors do not support a single page, to support a single page we must know the single page jump principle. Currently, there are two methods: hash and History

hash

Hash is easy, just listen for hashchange

on('hashchange', handleHashchange)
Copy the code

history

History relies on the HTML5 History API and server configuration. Rely mainly on history.pushState and history.replacestate

Next, we want the browser to send the same event HistoryStatechanged when executing both methods, which requires overwriting both methods

/** * hack pushState replaceState * send historyStatechange event * @export
 * @param {('pushState' | 'replaceState')} e
 */
export function hackState(e: 'pushState' | 'replaceState') {
  var t = history[e]
  "function" == typeof t && (history[e] = function(n, i, s) { ! window['__bb_onpopstate_'] && hackOnpopstate(); Var c = 1 === arguments.length? [arguments[0]] : Array.apply(null, arguments), u = location.href, f = t.apply(history, c);
    if(! s ||"string"! = typeof s)return f;
    if (s === u) return f;
    try {
        var l = u.split("#"),
            h = s.split("#"), p = parseUrl(l[0]), d = parseUrl(h[0]), g = l[1] && l[1].replace(/^\/? (. *) /,"The $1"), v = h[1] && h[1].replace(/^\/? (. *) /,"The $1"); p ! == d ? dispatchCustomEvent("historystatechanged", d) : g ! == v && dispatchCustomEvent("historystatechanged", v)
    } catch (m) {
      warn("[retcode] error in " + e + ":" + m)
    }
    return f
  }, history[e].toString = fnToString(e))
}
Copy the code

Then just listen on HistoryStatechanged

on('historystatechanged', handleHistorystatechange)
Copy the code

Tips: Window.CustomEvent API is used here

Report the resource

Resources refer to the external resources of web pages, such as pictures, JS, CSS and so on

The principle is through the performance. GetEntriesByType (” resource “) obtain the resources page is loaded

export function handleResource() {
  var performance = window.performance
  if(! performance ||"object"! = typeof performance ||"function"! = typeof performance.getEntriesByType)return null;
  let commonMsg = getCommonMsg()
  letmsg: ResourceMsg = { ... commonMsg, ... { dom: 0, load: 0, t:'res',
      res: ' ',
    }
  }
  var i = performance.timing || {},
      o = performance.getEntriesByType("resource") | | [];if ("function" == typeof window.PerformanceNavigationTiming) {
    var s = performance.getEntriesByType("navigation") [0]; s && (i = s) } each({ dom: [10, 8], load: [14, 1] },function (e, t) {
      var r = i[TIMING_KEYS[e[1]]],
          o = i[TIMING_KEYS[e[0]]];
      if(r > 0 && o > 0) { var s = Math.round(o - r); S >= 0 && s < 36e5 && (MSG [t] = s)}}) o = o.filter(item => {var include = getConfig(item => {var include = getConfig(item => {var include = getConfig(item => {var include = getConfig(item)}}))'ignore').ignoreApis.findIndex(ignoreApi => item.name.indexOf(ignoreApi) > -1)
    return include > -1 ? false : true
  })
  msg.res = JSON.stringify(o)
  report(msg)
}
Copy the code

Listening API

In this case, ajax or FETCH will be rewritten to automatically report the success or failure of the interface call. Of course, __bb.api() will also be supported for manual reporting if the network request is not made by either of these methods

Rewrite the ajax

// If the return is too long, it will be truncated to a maximum of 1000 charactersfunction hackAjax() {
  if ("function" == typeof window.XMLHttpRequest) {
    var begin = 0,
        url =' ',
        page = ' '
        ;
    var __oXMLHttpRequest_ = window.XMLHttpRequest
    window['__oXMLHttpRequest_'] = __oXMLHttpRequest_
    window.XMLHttpRequest = function(t) {
      var xhr = new __oXMLHttpRequest_(t)
      if(! xhr.addEventListener)return xhr
      var open = xhr.open,
        send = xhr.send
      xhr.open = function(method: string, url? : string) { var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments); url = url page = parseUrl(url) open.apply(xhr,a) } xhr.send =function() {
        begin = Date.now()
        var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments);
        send.apply(xhr,a)
      }
      xhr.onreadystatechange = function() {
        if (page && 4=== xhr.readyState) {
          var time = Date.now() - begin
          if (xhr.status >= 200 && xhr.status <= 299) {
            var status = xhr.status || 200
            if ("function" == typeof xhr.getResponseHeader) {
              var r = xhr.getResponseHeader("Content-Type");
              if(r && ! /(text)|(json)/.test(r))return} handleApi(page, ! 0, time, status, xhr.responseText.substr(0,Config.maxLength) ||' ', begin)
          } else{ handleApi(page, ! 1, time, status ||'FAILED', xhr.responseText.substr(0,Config.maxLength) || ' ', begin)
          }
        }
      }
      return xhr
    }
  }
}
Copy the code

Rewrite the fetch

function hackFetch() {if ("function" == typeof window.fetch) {
    var __oFetch_ = window.fetch
    window['__oFetch_'] = __oFetch_
    window.fetch = function(t, o) {
      var a = 1 === arguments.length ? [arguments[0]] : Array.apply(null, arguments);
      var begin = Date.now(),
          url = (t && "string"! = typeof t ? t.url : t) ||"",
          page = parseUrl((url as string));
      if(! page)return __oFetch_.apply(window, a)
      return __oFetch_.apply(window, a).then(function (e) {
        var response = e.clone(),
            headers = response.headers;
        if (headers && 'function' === typeof headers.get) {
          var ct = headers.get('content-type')
          if(ct && ! /(text)|(json)/.test(ct))return e
        }
        var time = Date.now() - begin;
          response.text().then(function(res) {
            if(response.ok) { handleApi(page, ! 0, time, status, res. Substr (0100) | |' ', begin)
            } else{ handleApi(page, ! 1, time, status, res. Substr (0100) | |' ', begin)
            }
          })
        return e
      })
    }
  }
}
Copy the code

Manual buried point

Supports multiple manual reporting methods such as SUM avG API MSG

More resources

Github.com/abc-club/fr…