Written by Ali Tao Department F(X) Team – Mou mou

The introduction

Front-end error governance is the basis for ensuring user interaction experience. In the actual front-end development process, we often need to deal with all kinds of abnormal situations, but some errors caused by coding negligence or dependent resource abnormality are not caught, it is also difficult to avoid. This article documents an error-governance process in a real project and combs through some common error-tracking methods and tools.

The problem found

Recently, I took over a Rax project. Through the monitoring tool, I found that the overall error rate of the project was high, and 99% of the error information was “Script error.”, so I tried to solve it.

Error stack fetch

First, the monitoring tool listens for global error and unhandledrejection events to catch unhandled page errors and summarize them into a report. The report content is as follows:

In order to get these js script errors, we need to add crossOrigin attribute to the script tag, CDN also need to add cross-domain header, the core code is as follows:

import { createElement } from 'rax';
import { Root, Style, Script} from 'rax-document';

import css from './css';

function Document() {
  return (
    <html>
      <head>
        <script crossorigin="anonymous" src="//cnd.com/corss-origin.js"></script>
      </head>
      <body id="body">
        {/* root container */}
        <Root />
        <Script crossorigin="anonymous" />
      </body>
    </html>
  );
}
export default Document;
Copy the code

The Script component of Rax implements an HTML Script tag, which is modified in the same way as the HTML Script tag, so that the error stack can be obtained if the JS Script inside the two Script tags fails.

In addition, you need to enable source-map at build time (Rax 1.0.0 +), modify build.json, and add sourceMap configuration:

{
  "sourceMap": "source-map"
}
Copy the code

In this way, the error stack can be matched with the source code, otherwise see the error stack is the source location after pack, can not be analyzed. Then recompile and go live:

$ npm run build
$ ls ./build/web/index.js.map
./build/web/index.js.map
Copy the code

Error stack analysis

Follow up after release:You can see there’s oneTypeError:Cannot set property 'tn' of nullErrors made up 57.4% of all errors and 36% of all errorsScript error.This should be some dynamically introduced script tag not addedcrossOriginCause, first solve the main problem.

Open error stack:Then, we need to use a Source map toolsource-map-cliThis tool can help us map the error stack information after pack to the source code, it is also very easy to use:

$NPM install source-map-cli -g $source-map resolve /path/to/source-map-file.map 1 127855 #Copy the code

Combined with the error stack, we can get the following information:

$ source-map resolve ./index.js.map 1 127855                                                             
Maps to webpack:/ / / node_modules / _rax @ 1.1.4 @ rax/dist/rax. Min. Js: 1:10 078 (this)

!function(){var P={n:1.t:!1.driver:null.rootComponents: {},rootInstances: {}... $source-map resolve./index.js.map1 119884      
Maps to webpack:/ / / node_modules / _rax @ 1.1.4 @ rax/dist/rax. Min. Js: 1:20 92 (t)

!function(){var P={n:1.t:!1.driver:null.rootComponents: {},rootInstances: {}... $source-map resolve./index.js.map1 428416      
Maps to webpack:///src/component/A/index.jsx:100:8 (animation)

        animation({
        ^
          
$ source-map resolve ./index.js.map 1 428729      
Maps to webpack:///src/components/A/index.jsx:137:2 (useEffect)

  useEffect(() = > {
  ^

$ source-map resolve ./index.js.map 1 135896      
Maps to webpack:/ / / node_modules / _appear - [email protected] @ appear - polyfill/lib/intersectionObserverManager js: 73, 6 (target)

      target.setAttribute('data-has-appeared'.'true');
      ^

$ source-map resolve ./index.js.map 1 135396      
Maps to webpack:/ / / node_modules / _appear - [email protected] @ appear - polyfill/lib/intersectionObserverManager js: "six (Windows)

  if ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
                                                                                                          ^
Copy the code

The top two call stacks are in RAx, and since RAX was introduced into the current project through rax.min.js after pack, we can only locate the error location information in rax.min.js, but the third stack is more clear. The code is A callback to the animated component in component A. The core code is as follows:

  animateFun.current = animation(
      {} // Animation configuration,() = > {// return to the original position
      animation({  // Error location reported
        props: [{element: refDoms[currentIndex],
            property: 'transform.translateX'.easing: 'linear'.duration: 10.start: -4.end: 5.delay: 0,}]})// Set the next page
      let index = currentIndex + 1;
      if (index >= data.brandItems.length) {
        index = 0;
      }
      setCurrentIndex(index);
      animateFun.current = null; })Copy the code

Error: Cannot set property ‘tn’ of NULL; error: Cannot set property ‘TN’ of null Trace the error stack of rax.min.js further by using rax.min.js.map in the source directory:

$source-map resolve./node_modules/[email protected]@rax/dist/rax.min.js.map 1 10078 Maps to.. /src/vdom/reactive.js:92:2componentWillMount() {^ $source-map resolve./node_modules/[email protected]@rax/dist/rax.min.js.map 1 2092 Maps to.. /src/hooks.js:26:46 (prevInputs)if(isNull(prevInputs) || inputs.length ! == prevInputs.length) { ^Copy the code

Open the source file:

  1. vdom/reactive.js
componentWillMount() { // Error location reported
  this.__shouldUpdate = true;
}
Copy the code
  1. ../src/hooks.js
function areInputsEqual(inputs, prevInputs) {
  if(isNull(prevInputs) || inputs.length ! == prevInputs.length) {// Error location reported
    return false;
  }

  for (let i = 0; i < inputs.length; i++) {
    if (is(inputs[i], prevInputs[i])) {
      continue;
    }
    return false;
  }
  return true;
}
Copy the code

This error stack message does not look right, does not seem to be in the right place, and estimates the difference between the raX build environment and the current project. Error: Cannot set property ‘tn’ of null rax.min.js Cannot set property ‘tn’ of null

There are altogether 4 results in rax.min.js searching.tn=, which are selected as follows:

t.componentWillUnmount=function(){k(this.willUnmount)},t.u=function(){this[S].tn=!0.this.setState(s)} ! s.un&&e&&yn(n,!0)):(s.tn=!0,e&&yn(n)))}var jn={setState:function(n,t,i){P.t||w(),bn(n,t,i)},forceUpdate:function(n,t)
                                                
unmountComponent(n),this[N] =null),this.in=null.this.tn=! 1,this.v

v.shouldComponentUpdate(i,f,t) :v.R&& (u=! R(e,i)||! R(o,f))),u? (l.tn=!1,r=v.context,v.componentWillUpdate&&v.componentWillUpdate(i,f,t)
Copy the code

It can be found that the code is basically related to the component life cycle or state management, so it should be judged that the problem is caused by calling the VDOM related code after the component life cycle ends.

Validation error

Verify the error stack first, set timeout 5000 ms in the callback function of the animation component and execute it. Then add the test code of manually destroying the component on the page. The operation makes component A be destroyed, and repeat the current problem:

reactive.js:143 Uncaught TypeError: Cannot set property '__isPendingForceUpdate' of null
    at ReactiveComponent._proto.__update (reactive.js:143)
    at setState (hooks.js:96)
    at index.jsx:118
Copy the code
_proto.__update = function __update() {
    this[_constant.INTERNAL].__isPendingForceUpdate = true; // Error code
    this.setState(_types.EMPTY_OBJECT);
  };
Copy the code

This also confirms the first search result of rax.min.js:

t.componentWillUnmount=function(){k(this.willUnmount)},t.u=function(){this[S].tn=!0.this.setState(s)}
Copy the code

The error was then replicated in the component associated with the animation component in the context of the project.

repair

Once the problem is found, it is easier to fix it by introducing the rax-use-mounted state component. This component will mount the current VDOM in a closure and provide an isMounted function to determine whether the VDOM isMounted. Add VDOM mount to all external callback functions before they execute:

    animateFun.current = animation(
      {}, () = > {
      // The asynchronous callback needs to determine whether the DOM is currently mounted
      if (isMounted()) {
        // return to the original position
        animation({
          props: [{element: refDoms[currentIndex],
              property: 'transform.translateX'.easing: 'linear'.duration: 10.start: -4.end: 5.delay: 0,}]})// Set the next page
        let index = currentIndex + 1;
        if (index >= data.brandItems.length) {
          index = 0;
        }
        setCurrentIndex(index);
        animateFun.current = null; }});Copy the code

Release validation

The error rate decreased by 68% after release, in line with expectations.

conclusion

This debug process is a bit complicated, involving source map configuration, source-map-CLI use, and even reading part of the code after pack. Generally speaking, this process still has a lot of room for optimization. For example, error stack analysis is completed automatically by tools. I wouldn’t have to go through all this. The cause of the problem was identified during the troubleshooting process. Since the callback of the animation component is outside the lifecycle, there is a possibility that the DOM may have been unmounted by the time the callback is called. However, in fact, not only the callback of animation components, but also other callback scenes will have similar situations, such as setTimeout. It is recommended that all setTimeout involving DOM modification be replaced with useTimeout. UseTimeout cannot be used in hooks, however, timers in hooks can be used in return functions clearTimeout:

useEffect(() = > {
  const id = setTimeout(() = > {}, 100);
  return () = > clearTimeout(id);
});
Copy the code

In addition, during the verification process, it is also found that DOM does not exist in the onDisappear event callback. You can use useMountedState in the onDisappear event callback to determine the existence of the current DOM before performing the state operation. Otherwise the same problem will occur.

import useMountedState from 'rax-use-mounted-state';

export default memo((props={}) = > {
  const isMounted = useMountedState();
  const [value, setValue] = useState(true);
  const disappearHandler = useCallback(() = > {
    if (isMounted()) {
      setValue(false);
    }
  }, [setValue]);
  return <View onDisappear={()= > disappearHandler()} />
})
Copy the code

F (X) Team has opened a microblog!
In addition to the article there is more team content to unlock 🔓