@ecomfe/react-track

A declarative, component based solution to track page views and user events with react & react-router

Usage no npm install needed!

<script type="module">
  import ecomfeReactTrack from 'https://cdn.skypack.dev/@ecomfe/react-track';
</script>

README

react-track

基于React的声明式PV及用户行为采集框架。

为什么自研

在NPM上有若干个类似的包,但它们存在着一些缺陷,这其中主流的两类是:

  • react-tracking:偏向于声明式,但使用HOC的形式限制了使用的场景,且通过拦截类方法而臧props来进行数据的采集,与React的数据流形式略有不和。
  • react-tracker:采用类似react-redux的思想,使用connectProvider的形式将功能联系起来。但是这种做法更偏向于命令式,从使用的角度来说繁琐之余也不易追踪。

除此之外,这些包均没有提供PV采集的能力。而PV采集中,有一个非常关键的问题至今没有得到很好的解决:

当URL中包含参数时,如/posts/123/posts/456在PV上会被认为是两个页面,但事实上它们对应的路由均是/posts/:id,是相同的。

这一问题导致如果需要将包含参数的URL进一步的汇总与分组来更精确地计算“页面”的PV,则会需要额外的数据分析成本。因此我们希望从源头,即在数据采集的时候就解决这一问题,这也导致需要与react-router进行关联。

我们的目标是:

  • 使用声明式的形式进行数据采集。
  • 与React的组件树结构进行整合,可在JSX中形象地表达。
  • 提供PV采集的能力,且能够获取react-router的配置
  • 尽可能小的移除成本,当一个行为或页面PV不再需要采集时,可以用最简单的手段移除而不影响已有组件的逻辑。

使用文档

全局环境准备

类似于reduxreact-routerreact-track需要一个全局的环境来定义数据的采集过程和数据的记录形式,这些环境由Tracker组件来定义。Tracker组件需要以下2个属性:

  • {Function} collect:定义如何采集和组装需要记录的数据。
  • {Object} provider:定义如何将数据记录下来或发送至指定服务。

并且支持以下可选配置:

  • {boolean} reportPageViewOnLeafOnly:仅让作为叶子节点的TrackRoute组件报告PV,如果一个TrackRoutechildren属性,则不作为叶子节点处理。该配置默认关闭。
  • {boolean} warnNestedTrackRoute:当一个TrackRoute在另一个TrackRoute里面时,在控制台打印一个警告信息,用于检查一些不符合预期的路由嵌套。该配置默认打开。

以上2个属性也可以直接用于TrackRoute组件上,以覆盖Tracker组件配置的默认值。

定义数据采集

Tracker组件中,collect属性用来定义“采集哪些数据”以及“数据的最终结构”。connect是一个函数,其签名如下:

type collectPageView = (type: 'pageView', location: Location) => object;
type collectEvent = (type: 'event') => object;
type collect = collectPageView | collectEvent;

react-track内置了几种常用的采集函数:

  • browser():添加浏览器相关的信息,包括UA、分辨率、操作系统、浏览器版本、系统语言。这个采集仅在类型为pageView时才会生效。
  • context(env):将固定的env对象放到采集数据中去,常用于添加当前登录用户名、系统名称、系统版本等信息。
  • session(storageKey):跟踪一次用户的访问,为每一次访问生成一个唯一的标识,并存放在sessionStorage中,这个唯一标识会变为名为session的属性值。可以通过storageKey来自定义sessionStorage中对应的键名。
  • basename(prefix):默认采集路径信息是基于location.pathname的,但它并不包含basename。所以,当history设置basename时,需要使用该collect指定basename才能采集准确的路径

当希望同时使用多个collect函数时,可以通过combineCollects将它们组合成一个函数。以下代码展示了如何使用多个collect函数,并通过combineCollects将它们组合成一个:

import {combineCollects, browser, context, session} from '@ecomfe/react-track';

const app = {
    name: 'My App',
    version: '1.0.0',
    branch: 'stable'
};

const collect = combineCollects(
    context(app),
    browser(),
    session()
);

定义数据应用

Tracker组件的provider属性用于控制采集到的数据如何使用,通常在生产环境我们会选择将其发送到指定的服务,如百度统计、Google Analysis等,在开发环境中则可以忽略或者显示在控制台中。

provider是一个对象,其定义如下:

type PageViewData = {
    location: Location,
    referrer: Location,
    [key: string]: any
};

type EventData = {
    category: string,
    action: string,
    label: string,
    [key: string]: any
};

interface TrackProvider {
    install(): void;
    uninstall(): void;
    trackPageView(data: PageViewData): void;
    trackEvent(data: EventData): void;
}

通常使用install来做初始化的工作,uninstall进行清理,而trackPageViewtrackEvent则会在每一次PV或自定义事件数据采集完成后被调用。

react-track同样内置了几个常用的处理器:

  • holmes(site):封装了百度统计,接受对应的百度统计id。
  • print():用于调试,通过控制台打印对应的数据。
  • empty() :忽略所有的数据。

collect相似地,一个应用中我们可能会需要同时将数据进行多重处理,如既发送到百度统计,又打印在控制台中,此时可以使用composeProvider函数进行组装。以下代码自定义了一个provider用于将数据通过POST发送到指定的服务器,同时将2个处理器组合为一个,最后仅在生产环境才生效,开发环境仅打印在控制台:

import {holmes, print, composeProvider} from '@ecomfe/react-track';
import axios from 'axios';

const post = url => {
    const send = type => data => axios.post(url, {type, ...data});

    return {
        install: noop,
        uninstall: noop,
        trackPageView: send('pageView'),
        trackEvent: send('event')
    };
};

const trackProvider = process.env.NODE_ENV === 'production'
    ? composeProvider(
        post('http://127.88.88.88:8888/v1/log'),
        holmes(mySiteID)
    )
    : print();

定义环境

在有了collectprovider的定义后,将Tracker组件置于应用的最外层即可以完成全部环境的准备:

import {Tracker} from '@ecomfe/react-track';
import {BrowserRouter} from 'react-router';
import {App} from 'components';

<Tracker collect={collect} provider={trackProvider}>
    <BrowserRouter>
        <App />
    </BrowserRouter>
</Tracker>

采集PV

对于Web应用,PV是数据采集中的必要信息。传统的PV采集会存在一个问题,假设我们有一个展示用户信息的页面,其路由是/users/:username,那么对于不同的用户,我们将会得到不同的URL,如/users/Alice/users/Bob。在常见的PV采集方案中,采集工具仅对URL作出反应,因此在数据的统计中,我们会看到2个页面分别有n和m的访问量。

但是在对应用系统的分析时,我们更希望得到这样一个信息:用户信息页被访问了多少次。然而问题是,在采集的数据中,要通过/users/Alice/users/Bob去还原用户信息页被问题的问题(n + m)是相对困难的,当路由规则更加复杂时,甚至可能是无法实现的。

为此,react-track在PV采集上,提供了将/users/:username这个URL模块也一并捕获的能力,使用react-router的定义,称之为path。由于react-router 4.x的特征,从全局顶层来获取path是不可能的,因此为了实现这一功能,使用react-track的系统不得不在代码上做出一些微小的改变。

声明PV采集点

react-track要求用户显式地声明需要采集PV信息的路由位置,提供了TrackRoute这一组件来进行声明。

TrackRoute的功能与react-routerRoute组件完全兼容,因此对于一个已经使用了react-router的系统,只需要在合适的位置将<Route>修改为<TrackRoute>即可:

import {Switch, Route} from 'react-router-dom';
import {CommonHeader, AboutTab, Info, Contact} from 'components';

const App = () => (
    <div>
        <CommonHeader />
        <Switch>
            <TrackRoute exact path="/console" component={Console} />
            <TrackRoute exact path="/service" render={() => <Service />} />
            <Route path="/about" component={AboutMe}>
                <AboutTab />
                <TrackRoute exact path="/about/info" component={Info} />
                <TrackRoute exact path="/about/contact" component={Contact} />
            </Route>
        </Switch>
    </div>
);

需要注意的一点是,TrackRoute就当仅应用在最底层的路由上。如上述代码中的/about这一级路由并不是最底层的,其下还有/about/info/about/contact,如果将这一层的<Route>改为<TrackRoute>的话,当访问/about/info时,由于上下两个路由都会触发PV采集,最终将会形成2条数据:

  • {pathname: "/about/info", path: "/about"}
  • {pathname: "/about/info", path: "/about/info"}

它们的pathname是一样的,始终指向当前真实的URL,而path则不同,指向<TrackRoute>上的path属性。这会导致数据的重复。

高阶组件

同时react-track还提供了trackPageView高阶组件,可以将任意的组件声明为PV采集点。trackRoute并不会声明路由信息,因此还需要将组件放置在<Route>下:

import {trackPageView} from 'react-track';
import {Route} from 'react-route';

const Console = () => (
    <div>
        ...
    </div>
);
const ConsoleWithTrack = trackPageView(Console);

<Route exact path="/console" component={ConsoleWithTrack} />

采集事件

通过事件采集可以分析用户的行为,帮助理解用户的真实需求并进行产品的改进。react-track提供了简单直接的采集方式,允许通过对事件回调类型的属性进行拦截来采集相关数据。

在常见的自定义事件模型中,一个事件由categoryactionlabel三个属性组成。

定义采集点

react-track提供了TrackEvent组件,使用它包裹在对应组件的外层,并通过eventPropName指定需要拦截的事件名称,用categoryactionlabel声明事件的相关信息。除以上4个属性外,其它的属性会透传给其子元素。

子组件需要支持eventPropName对应的属性,且必须是函数类型。需要支持多个事件类型时可以嵌套使用:

import {TrackEvent} from '@ecomfe/react-track';
import {NavLink} from 'react-router-dom';

const NavItem = ({name, to}) => (
    <TrackEvent eventPropName="onMouseEnter" category="navigation" action="mouseEnter" label={name}>
        <TrackEvent eventPropName="onMouseLeave" category="navigation" action="mouseLeave" label={name}>
            <li>
                <NavLink exact to={to}>{name}</NavLink>
            </li>
        </TrackEvent>
    </TrackEvent>
);

hooks

const Item = ({name, onClick}) => {
    const trackEvent = useTrackEvent();
    const handleClick = useCallback(
        e => {
            trackEvent({category: 'navigation', action: 'click', label: name});
            onClick(e)
        },
        [name]
    )
    return <Button onClick={handleClick}>Click Me</Button>
};

高阶组件

react-track同时提供了trackEvent高阶组件,用于直接在一个现有组件上添加事件采集的能力。在希望采集多个事件时,与recompose一起配合能取得更好的代码可读性:

import {trackEvent} from '@ecomfe/react-track';
import {NavLink} from 'react-router-dom';
import {compose} from 'recompose';

const NavItem = ({name, to}) => (
    <li>
        <NavLink exact to={to}>{name}</NavLink>
    </li>
);

const track = action => {
    const options = {
        eventPropName: 'on' + action[0].toUpperCase() + action.slice(1),
        category: 'navigation',
        action: action,
        label: null
    };

    return trackEvent(options);
};

const enhance = compose(
    track('mouseEnter'),
    track('mouseLeave')
);

export default enhance(NavLink);