Основные продуктовые группы The New York Times вкладывают значительные средства в React.js, и мы создаем совершенно новую веб-платформу для всех наших продуктов. В рамках этого у нас была редкая возможность переосмыслить то, как мы делаем многие вещи, включая получение данных, объединение приложений, рекламу, отслеживание и аналитику.

Традиционно проблемы отслеживания и аналитики в нашем приложении были «закреплены» постфактум. То есть мы часто использовали DOM как источник истины о существовании и взаимодействии различных элементов на странице. Это было не только непросто, но и было очень трудно обойтись без раскрытия несемантических данных, необходимых для просмотра и прикрепления аналитических модулей. В некоторых случаях это было невозможно, и нам приходилось размещать аналитический код в кажущихся случайными местах в нашем приложении.

Мы увидели в этой переплатформенной работе прекрасную возможность «встроить» аналитику и отслеживание в структуру, которую мы строили. Мы хотели отойти от DOM как источника истины и вместо этого использовать сами компоненты, чтобы сигнализировать о взаимодействиях, представляющих интерес. Это также позволило нам использовать саму иерархию приложений для установления часто необходимых событий отслеживания контекстной осведомленности.

TL;DR

Наше решение завершилось разработкой response-tracking, который предоставляет богатый декларативный API для встраивания отслеживающей информации в ваше приложение. Мы открыли исходный код этой библиотеки.

Response-tracking позволил нам уйти от CSS-выбора интересующих элементов и присоединения обработчиков. Вместо этого мы можем украсить сами обработчики соответствующими данными отслеживания.

Как это работает

Вкратце, как работает отслеживание реакции:

  1. Вы определяете dispatch() функцию верхнего уровня, которая определяет, куда идут ваши объекты отслеживания. Если вы его не определите, по умолчанию они будут переведены в window.dataLayer[] - хороший вариант по умолчанию, если вы используете Google Analytics, - но легко переопределить, если вам нужно, чтобы он перешел в другое место, или вам нужно улучшить с помощью некоторого вызова API перед отправив его в GA.
  2. Вы украшаете различные компоненты (страницы, формы, кнопки, ссылки и т. Д.) И любые обработчики или методы жизненного цикла в этих компонентах (onClick, componentDidMount и т. Д.) Только теми данными, которые у вас есть, когда они у вас есть. Это можно использовать как статический объектный литерал или как функцию, возвращающую объектный литерал, и это полезно в тех случаях, когда данные, которые вам нужны, являются функцией реквизита.
  3. Вот и все! Всякий раз, когда запускается событие (например, вы украсили обработчик кликов), объект отслеживания будет глубоко объединен, начиная с этого компонента на всем пути вверх по иерархии компонентов до верхнего уровня, а затем объединенного объекта. будет передан dispatch().

Это означает, что задачи отслеживания каждого компонента должным образом разделены на этот компонент. Вам не нужно «просачивать» информацию вверх (или вниз) по иерархии компонентов. Это избавляет вас от необходимости беспокоиться о связывании компонентов или обновлении информации отслеживания по мере перемещения компонентов или их рефакторинга.

Пример

Вот как это выглядит на практике (на теоретическом примере страницы входа с упрощенными битами, относящимися к отслеживанию реакции):

/* SignInPage.js */
import React, { Component } from 'react';
import track from 'react-tracking';
import SignInForm from './SignInForm';
import { Header, Footer } from './PageFurniture';
@track({
  page: 'sign-in',
})
export default class SignInPage extends Component {
  render() {
    return (
      <div>
        <Header />
        <SignInForm />
        <Footer />
      </div>
    );
  }
}

И форма входа:

/* SignInForm.js */
import React, { Component } from 'react';
import track from 'react-tracking';
@track({
  module: 'sign-in',
})
export default class SignIn extends Component {
  
  @track({
    event: 'sign-in-attempt',
  })
  handleSignIn = () => {
    // API call ...
  };
  @track({
    event: 'register-attempt'
  })
  handleRegister = () => {
    // register form ...
  }
  render() {
    return (
      <form>
        <input type="text" name="username" placeholder="username" />
        <input type="password" name="password" placeholder="password" />
        <button onClick={this.handleSignIn}>Sign in</button>
        <button onClick={this.handleRegister}>Register</button>
      </form>
    );
  }
}

Вот как выглядит объект отслеживания, если пользователь, например, нажал «Войти» (действие регистрации будет выглядеть примерно так же):

{
  page: 'sign-in',
  module: 'sign-in',
  event: 'sign-in-attempt'
}

Простой! Это, конечно, при условии, что в приложении нет других данных контекстного отслеживания. На практике одним из распространенных шаблонов является определение некоторых данных отслеживания верхнего уровня в корневой <App /> оболочке для определения глобальных вещей, таких как имя приложения, номер сборки и т. Д. В этом случае эти данные приложения будут частью результирующего отправляемого объекта. .

Что это нам дает

Вот несколько замечаний по поводу этого шаблона.

  1. Как и в случае с любой чистой компонентной архитектурой, <SignInForm /> фактически не знает, где он рендерится, однако контекстная информация page появилась автоматически.
  2. Более того, Sign In <button /> не знает, в каком модуле (или странице) он отображается (особенно полезно, если кнопка была определена в другом файле), он просто знает о событии, которое необходимо отправить при нажатии, это единственное место, где должна быть известна эта информация.
  3. Мы украсили <SignInPage /> и <SignInForm /> информацией для отслеживания, но мы получили объект отслеживания только тогда, когда произошло событие (которое также было оформлено!), Как и следовало ожидать.

СОВЕТ. Существует распространенный вариант использования события «готовность страницы» при отрисовке страницы. Это также поддерживается путем определения функции process() в некотором компоненте верхнего уровня; пример этого рассматривается в следующем разделе.

Простой и расширяемый шаблон

Мы надеемся, что предыдущий пример демонстрирует, насколько простым, но мощным и гибким является этот шаблон. Однако есть надежная поддержка для некоторых довольно сложных случаев использования.

Независимость от платформы аналитики

Функция верхнего уровня dispatch() позволяет делать все, что вам нужно, с данными отслеживания, которые отправляются по всему приложению. Например, вы можете захотеть улучшить данные с помощью некоторого внутреннего вызова API, прежде чем отправлять их в window.someOtherDataLayer[].

Гибкое управление

Функция верхнего уровня process() позволяет вам «подключиться» к любому декорированному компоненту и вернуть объект для отправки в dispatch() (возврат ложного значения не сработает). Распространенным вариантом использования является автоматическая отправка события «страница готово» к любым компонентам страницы, например:

process: (data) => (data.page ? { event: 'page-ready' } : null)

При этом проверяется свойство page объекта отслеживания, и если оно есть, предполагается, что это страница, и возвращается event «готовая страница», которая затем объединяется с остальными данными отслеживания приложения и передается в dispatch().

Расширенные варианты использования

Декоратор @track() также может принимать функцию. Это полезно, если ваши данные отслеживания основаны на некоторых props данных.

Он также будет передавать любые аргументы, которые будут отправлены функциям-обработчикам (начиная со второго параметра, поскольку props всегда является первым). Это полезно для ввода формы, например:

import React, { Component } from 'react';
import track from 'react-tracking';
// In this case, the "status" tracking data
// is a function of one of its props (isNew)
@track(props => {
  return { status: props.isNew ? 'new' : 'existing' };
})
export class SomeButton extends Component {
  // In this case the tracking data depends on
  // some unknown (until runtime) value (event).
  @track((props, [event]) => ({
    action: 'click',
    label: event.currentTarget.title || event.currentTarget.textContent,
  }))
  handleClick = event => {
    if (this.props.onClick) {
      this.props.onClick(event);
    }
  };
render() {
    return (
      <button onClick={this.handleClick}>
        {this.props.children}
      </button>
    );
  }
}

Проверка

В The New York Times мы пошли еще дальше, определив схему отслеживания для проверки. Традиционно контракты с объектами отслеживания координировались вручную, обычно через внутренний документ Google, но мы переходим к более строгой и более структурированной схеме, применяемой программно. Мы используем отличную библиотеку ajv для определения схемы JSON для нашего уровня данных отслеживания.

В нашей среде разработки мы определили dispatch() для прохождения validate() функции схемы отслеживания для проверки всех объектов отслеживания и выдачи ошибок при возникновении каких-либо проблем (чтобы уменьшить размер пакета, мы отключаем это в производственной сборке).

/** 
 * Our internal tracking-schema.
 * The `validator()` function has a process.env.NODE_ENV check
 * to no-op in production, but validate against JSON schema in dev.
 */
import { validator } from 'tracking-schema';
import enhanceWithUserData from './userDataApi';
const PAGE_DATA_READY = 'pageDataReady';
// checks dataLayer[] to be available and pushes data to it
const pushtoDataLayer = data => {
  validator(data); // logs errors in dev; no-op in prod
  (window.dataLayer = window.dataLayer || []).push(data);
};
// dispatch() will decide whether to push directly into the DL
// or enhance with API-provided data first
export const dispatch = data => {
  if (data.event === PAGE_DATA_READY) {
    enhanceWithUserData(data).then(pushtoDataLayer);
  } else {
    pushtoDataLayer(data);
  }
};

Попытайся!

Мы обнаружили, что отслеживание реакции было легко освоить различными командами, но при этом достаточно сложным, чтобы справиться со всеми нашими сценариями использования до сих пор. Мы будем рады, если вы попробуете это и поделитесь с нами своим мнением. Не стесняйтесь задавать вопросы на нашем Github, если у вас возникнут какие-либо проблемы или возникнут вопросы о том, как делать определенные вещи. И, как всегда, запросы на включение более чем приветствуются!

Спасибо

Большое спасибо кросс-функциональной команде в The Times, которая до сих пор помогла создать отслеживание реакции: Джереми Гайед, Олег Зиньяк, Аневди Абреу, Макс Болдуин, Иван Кравченко и Николь. Барам ».