Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money. The author of this article: Cjinhuo, reprint is prohibited without authorization.

The profile

Open source front-end monitoring SDK: Mitojs, interested partners can go to check ~(SDK online Demo)

Coming to the body, this paper is divided into four parts

  • background
  • Principle of front-end monitoring
  • At the end

background

This article is about monitoring the implementation of code, that is, the implementation of the code inside the plugin

Principle of front-end monitoring

Monitor native events. If addEventListener is not supported, then override the native function to take the input parameter and return the original function.

replaceOld

We need to rewrite a lot of native functions, and define a public function up front to reduce redundant code

/** * overrides a property on the object **@export
 * @param {IAnyObject} Source Specifies the object to be overridden@param {string} Name Specifies the key * of the object to be overridden@param {(... args: any[]) => any} Replacement takes the original function as an argument and executes and overwrites the original function *@param {boolean} [isForced=false] Whether to force overwrite (this property may not originally exist) */
export function replaceOld(source: IAnyObject, name: string, replacement: (... args: any[]) => any, isForced =false) :void {
  if (source === undefined) return
  if (name in source || isForced) {
    const original = source[name]
    const wrapped = replacement(original)
    if (typeof wrapped === 'function') {
      source[name] = wrapped
    }
  }
}
Copy the code

fetch

All request third-party libraries are secondary encapsulated based on XHR and FETCH. You only need to rewrite these two events to get the information of all interface requests. For example, to rewrite the fetch code operation:

replaceOld(_global, BrowserEventTypes.FETCH, (originalFetch: voidFun) = > {
  return function (url: string, config: Partial<Request> = {}) :void {
    const sTime = getTimestamp()
    const method = (config && config.method) || 'GET'
    // Collect the basic information of fetch
    const httpCollect: HttpCollectedType = {
      request: {
        httpType: HttpTypes.FETCH,
        url,
        method,
        data: config && config.body
      },
      time: sTime,
      response: {}}return originalFetch.apply(_global, [url, config]).then(
      (res: Response) = > {
        // The object needs to be cloned, otherwise it will be marked as already used
        const resClone = res.clone()
        const eTime = getTimestamp()
        httpCollect.elapsedTime = eTime - sTime
        httpCollect.response.status = resClone.status
        resClone.text().then((data) = > {
          // Collect the response body
          httpCollect.response.data = data
          The notify function is used to notify the subscription center
          notify(BrowserEventTypes.FETCH, httpCollect)
        })
        return res
      },
      (err: Error) = > {
        const eTime = getTimestamp()
        httpCollect.elapsedTime = eTime - sTime
        httpCollect.response.status = 0
        The notify function is used to notify the subscription center
        notify(BrowserEventTypes.FETCH, httpCollect)
        throw err
      }
    )
  }
})
Copy the code

Interface cross domain, timeout: The two happens, interface returns the response of the body and response headers are empty inside, the status is equal to zero, so it is difficult to distinguish between the two, but under normal circumstances, the general project of the request are complex, so the formal request to the request of the option for first, if it is cross domain basic tens of milliseconds will return to, This can be used as a threshold to determine cross-domain and timeout problems (if the interface does not exist, it will be judged as cross-domain interface)

The above code is the basic operation of rewriting FETCH. After collecting data, one step of data processing can be done. The data will be discussed below. Similarly, the following table can be overwritten in the same way, rewrite the process to get the input parameter and collect the data you want, specific code implementation click the link below

  1. console
  2. xhr
  3. Onpopstate, pushState, replaceState

onerror

Onerror can be listened for via addEventListener, a callback that is triggered when a resource error or code error occurs

/** * Add event listener **@export
 * @param {{ addEventListener: Function }} target Target object *@param {TotalEventName} EventName specifies the eventName * on the target object@param {Function} Handler callback function *@param {(boolean | unknown)} [opitons=false] useCapture Defaults to false */
function on(
  target: { addEventListener: Function },
  eventName: TotalEventName,
  handler: Function,
  opitons: boolean | unknown = false
) :void {
  target.addEventListener(eventName, handler, opitons)
}
on(
  _global,
  'error'.function (e: ErrorEvent) {
    The notify function is used to notify the subscription center
    notify(BrowserEventTypes.ERROR, e)
  },
  true
)
Copy the code

Similarly, the following monitoring methods are the same:

  1. click
  2. hashchange
  3. unhandlerejecttion

Vue2 and Vue3 errors

Vue provides a function errorHandler for developers to catch frame-level errors, so simply override this method and take the input parameter

const originErrorHandle = Vue.config.errorHandler
Vue.config.errorHandler = function (err: Error, vm: ViewModel, info: string) :void {
  const data: ReportDataType = {
    type: ErrorTypes.VUE,
    message: `${err.message}(${info}) `.level: Severity.Normal,
    url: getUrlWithEnv(),
    name: err.name,
    stack: err.stack || [],
    time: getTimestamp()
  }
  notify(BaseEventTypes.VUE, { data, vm })
  const hasConsole = typeof console! = ='undefined'
  // Vue source code will determine vue.config. silent, true will not print on the console, false will print
  if(hasConsole && ! Vue.config.silent) { silentConsoleScope(() = > {
      console.error('Error in ' + info + '"' + err.toString() + '"', vm)
      console.error(err)
    })
  }
  returnoriginErrorHandle? .(err, vm, info) }Copy the code

Of course, Vue2 and Vue3 get data format is different, the specific processing logic can be click here

React render error catch

React16.13 provides the componentDidCatch hook function to call for error messages, so we can create a new class ErrorBoundary that inherits React and then declare the componentDidCatch hook function to get error messages

interface ErrorBoundaryProps { fallback? : ReactNode onError? :(error: Error, componentStack: string) = > void} interface ErrorBoundaryState { hasError? : boolean }class ErrorBoundaryWrapped extends PureComponent<ErrorBoundaryProps.ErrorBoundaryState> {
  readonly state: ErrorBoundaryState
  constructor(props: any) {
    super(props)
    this.state = {
      hasError: false}}componentDidCatch(error: Error, { componentStack }: ErrorInfo) {
    // Error and componentStack are the error messages we need
    const { onError } = this.props
    constreactError = extractErrorStack(error, Severity.Normal) reactError.type = ErrorTypes.REACT onError? .(error, componentStack)this.setState({
      hasError: true})}render() {
    return (this.state.hasError ? this.props.fallback : this.props.children) ?? null}}Copy the code

Then throw out the components, the concrete code implementation

Note: If a code error occurs in react but not in the render function, it will be globalonerrorTo capture

The plug-in

Implementation about this, the specific code can go inside the warehouse, on a front-end monitoring: monitoring the SDK touch hands called – architecture (open source) in the article have the concept about plug-in, plug-in is used for standard layer code and one thought, in the specified area designated function of the code, it will greatly improve readability and can be iterative

export interface BasePluginType<T extends EventTypes = EventTypes, C extends BaseClientType = BaseClientType> {
  // Event enumeration
  name: T
  // Monitors events and notifies the subscription center with notify
  monitor: (this: C, notify: (eventName: T, data: any) => void) = > void
  // Trigger data in monitor and pass it to the current function to get the data for format conversion.transform? :(this: C, collectedData: any) = > any
  // Get the transformed data to breadcrumb, report, etcconsumer? :(this: C, transformedData: any) = > void
}
Copy the code

Let’s take a look at a simple and complete example (see here for the code) :

const domPlugin: BasePluginType<BrowserEventTypes, BrowserClient> = {
  name: BrowserEventTypes.DOM,
  // Listen on events
  monitor(notify) {
    if(! ('document' in _global)) return
    // Add a global click event
    on(
      _global.document,
      'click'.function () {
        notify(BrowserEventTypes.DOM, {
          category: 'click'.data: this})},true)},// Convert data
  transform(collectedData: DomCollectedType) {
    /** * Returns the tag * containing the id, class, and innerTextde strings@param Target HTML node */
    function htmlElementAsString(target: HTMLElement) :string {
      const tagName = target.tagName.toLowerCase()
      letclassNames = target.classList.value classNames = classNames ! = =' ' ? ` class="${classNames}"` : ' '
      const id = target.id ? ` id="${target.id}"` : ' '
      const innerText = target.innerText
      return ` <${tagName}${id}${classNames ! = =' ' ? classNames : ' '}>${innerText}</${tagName}> `
    }
    
    const htmlString = htmlElementAsString(collectedData.data.activeElement as HTMLElement)
    return htmlString
  },
  // Consume converted data
  consumer(transformedData: string) {
    // The transformed data is added to the breadcrumb in the user behavior stack
    addBreadcrumbInBrowser.call(this, transformedData, BrowserBreadcrumbTypes.CLICK)
  }
}
Copy the code

The use of plug-in

Once you’ve defined the plug-ins, you need to use them when browserClient initializes (click here for the code):

  const browserClient = new BrowserClient(options)
  const browserPlugins = [
    fetchPlugin,
    xhrPlugin,
    domPlugin,
    errorPlugin,
    hashRoutePlugin,
    historyRoutePlugin,
    consolePlugin,
    unhandlerejectionPlugin
  ]
  browserClient.use([...browserPlugins, ...plugins])
Copy the code

browserClient.use

BrowserClient is inherited from BaseClient, which has a use method that builds code specific to the hooks of the plug-in

  /** * references plugins **@param {BasePluginType<E>[]} plugins
   * @memberof BaseClient* /
  use(plugins: BasePluginType<E>[]) {
    if (this.options.disabled) return
    // Create a publish subscription instance
    const subscrib = new Subscrib<E>()
    plugins.forEach((item) = > {
      if (!this.isPluginEnable(item.name)) return
      // Call monitor in the plug-in and pass in the publishing function
      item.monitor.call(this, subscrib.notify.bind(subscrib))
      const wrapperTranform = (. args: any[]) = > {
        // Execute transform first
        constres = item.transform? .apply(this, args)
        // Take the data returned by the transform and pass it initem.consumer? .call(this, res)
        // Add logic here if you need to add new hooks
      }
      // Subscribe to the name in the plug-in and pass in the callback function
      subscrib.watch(item.name, wrapperTranform)
    })
  }
Copy the code

Plug-in running process

Then the overall process is roughly as follows:

At the end

🤔 summary

The principle of monitoring is nothing more than rewriting or adding event listeners, as is the case with applet monitoring.

The next article “Monitoring SDK hand touch Teach- micro channel small program” will talk about how to achieve event burying and error monitoring in micro channel small program, please look forward to ~

🧐 open source

Monitor SDKmitojs documents. At present, some people are using MitoJS to do their own monitoring platform or buried point related business. If you are interested, you may wish to come and have a look at 😘

📞 connect & push in

Byte front a large number of recruitment, push can help modify the resume and real-time query interview progress, welcome to hit resume to my email :[email protected]

If you are interested in byte front-end, error monitoring, burying points, also directly contact me on wechat :cjinhuo

Have A Good Day!!!