react-spy

A set of utilities for collecting UX-analytics of your React-application (ex: clicks, shows, errors and etc.)

Usage no npm install needed!

<script type="module">
  import reactSpy from 'https://cdn.skypack.dev/react-spy';
</script>

README

React Spy

A set of utilities for collecting UX-analytics of your React-application (ex: clicks, shows, errors and etc.)

npm i --save react-spy

Features

  • Easy integration with any ui-library (ex: Ant-Design)
  • Full control over the events

API


Usage

For example with Google Analytics

// Btn.js
import {spy} from 'react-spy';

const Btn = ({name, value}) => (<button name={name}>{value}</button>);
export default spy({
    id: ({name}) => name, // Computed `id` on based component properties
    listen: ['click'],    // DOM-events list
})(Btn);


// LoginForm.js
import {spy} from 'react-spy';

class LoginForm extends React.Component {
    // ...
    handleSubmit(evt) {
        evt.preventDefault();
        try {
            await api.login(this.getFormData());
            spy.send(this, 'success');
        } catch (err) {
            spy.send(this, 'failed', err);
        }
    }

    render() {
        return (
            <form onSubmit={this.handleEvent}>
                {/* ... */}
                <Btn name="login" value="Sign In"/>
                <Btn name="forgot" value="Forgot password"/>
            </form>
        );
    }
}

export default spy({
    id: "login-form",
    host: true,
    listen: ['mount', 'unmount'],
})(LoginForm);


// boot.js
import {addSpyObserver, addSpyErrorObserver} from 'react-spy';

addSpyObserver(chain => {
    // Send to GA
    ga('send', {
        hitType: 'event',
        eventCategory: chain[0], // ex: "login-form"
        eventAction: chain.slice(1).join('_'), // ex: "forgot_click"
    });
});

// Component Errors
addSpyErrorObserver(({error}) => {
    ga('send', 'exception', {
        exDescription: error.message,
        exFatal: false,
    });
});

ReactDOM.render(<App/>, document.body);

spy<Props>(options)

Decorate the component to collect analytics

  • options
    • id: string | (props, context?) => string — default @see propName description
    • propName: string — prop-name for id, by default spyId
    • listen: string[] — DOM-events to listen + error, mount and unmount
    • callbacks — list of observed callbacks that are passed to it via props
    • propName: string — name of the property responsible for the spy's id, by default spyId
    • host: boolean
import {spy} from 'react-spy';

export default spy({
    id: ({name}) => name,
    listen: ['click'],
})(function Btn({value}) {
    return <button>{value}</button>;
})

// Somewhere in the code
<Btn
    name="login"
    value="Sign in"
/>
// *click* -> ["login", "click"]

spy.send(cmp: React.Component, chain: string | string [], detail?: object): void

Send stats from the component and not only

  • cmp: React.Component — instance of React.Component
  • chain: string | string[] — name of metric
  • detail: object
import {spy} from 'react-spy';

export default spy({id: 'parent'})(class Box extends React.Component {
    render() {
        return (
            <button onClick={() => {spy.send(this, 'foo');}}>First</button>
            <button onClick={() => {spy.send(this, ['bar', 'baz'], {val: 123});}}>Second</button>
        );
    }
});

// Somewhere in a code
//   click on <First>:
//     - ["parent", "foo"] {}
//   click on <Second>:
//     - ["parent", "bar", "baz"] {val: 123}
//
// Somewhere in an another place:
//    spy.send(['global', 'label'], {time: 321}):
//      - ["global", "label"] {time: 321}

spy.error(cmp: React.Component, chain: string | string [], error: Error): void

send an error from the component and not only

  • cmp: React.Component — instance of React.Component
  • chain: string | string[] — name of metric
  • error: Error — any an error

addSpyObserver(fn: (chain: string[], detail: object) => void): UnsubsriberFn

Add observer of events for sending to the accounting system of analytics

import {addSpyObserver} from 'react-spy';

const unsubscribe = addSpyObserver(chain => {
    // Send to GA
    ga('send', {
        hitType: 'event',
        eventCategory: chain[0], // ex: "login-form"
        eventAction: chain.slice(1).join('_'), // ex: "forgot_click"
    });
});

// Somewhere (if you need to)
unsubscribe();

addSpyErrorObserver(fn: (detail: ErrorDetail) => void): UnsubsriberFn

Add observer of component errors

  • detail
    • error: Error — JavaScript error
    • info: object — React error info
    • chain string[] — spy id chain
import {addSpyErrorObserver} from 'react-spy';

addSpyErrorObserver(({error, chain}) => {
    // Send to GA
    ga('send', 'exception', {
        exDescription: error.message,
        exFatal: false,
    });

    // For dev
    console.error('[react-spy]', chain.join(' -> '));
    console.error(error);
});

intercept(rules: InterceptRules)

Intercepting a chain of events

import {intercept, UNCAUGHT} from 'react-spy';

intercept({
    'login-form': {
        // Interception of all chains, ex:
        //  - ["login-form", "forgot", "mount"]
        //  - ["login-form", "forgot", "click"]
        //  - etc
        'forgot'(send, chain, detail) {
            send(chain.concat('additional-id'));
        },

        // Processing of non-intercepted chains, ex:
        //  - ["login-form", "login", "click"]
        [UNCAUGHT](send, chain) {
            send(chain.concat('UNCAUGHT'));
            return false; // continue;
        }
    },
});

<Spy>...</Spy>

import {Spy} from 'react-spy';

const SomeFragment = ({condition, onShowDetail}) => (
    <div>
        <Spy id="top">
            <Button name="detail" value="Show detail" onClick={onShowDetail}/>
        </Spy>

        {condition &&
            <Spy id="bottom" listen={['mount', 'unmount'}>
                Detail
            </Spy>
        }
    </div>
);

// 1. *click on button* -> ["top", "detail", "click"]
// 2. *mounting* -> ["bottom", "mount"]

<SpyStep name="..."/>

The hidden spy element for steps monitoring

  • name: string — a step name
  • enter: string | string[] — the enter phase (optional)
  • leave: string | string[] — the leave phase (optional)

broadcast(chain: string[], detail?: object)

import {broadcast} from 'react-spy';

broadcast(['custom', 'event', 'chain'], {value: 'Wow'});
// or just
//   spy.send(['custom', 'event', 'chain'], {value: 'Wow'})

broadcastError(detail: ErrorDetail)

  • detail
    • error: Error — JavaScript error
    • chain string[] — spy id chain
    • info: object — React error info (optional)
import {broadcastError} from 'react-spy';

broadcastError({
    chain: ['login', 'submit', 'failed'],
    error: new Error('Internal Error'),
});
// or just
//   spy.error('localStorage', new Error('Read'));
//   spy.error(thisReactCmp, 'localStorage', new Error('save'));

Development