Эффективное отслеживание поведения в приложениях JavaScript

Spread the love

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

Если вы не знаете, что такое поведенческое отслеживание (behavioral tracking ), или вы не внедрили отслеживание в свои проекты, я приведу краткое объяснение:

Поведенческое отслеживание – это способ, которым компании получают информацию о значимых событиях, которые произошли в их платформе / приложениях; это особенно полезно для понимания поведения пользователей и выявления потенциальных недостатков и возможностей на конкретных страницах.

Как вы могли заметить в приведенном выше упрощенном определении, все это связано с получением ценной информации от событий, то есть от кликов по “призыву к действию”, входов пользователей в систему и т.д. … Что бы это реализовать разработчики, должны определить техническую реализацию, которая позволяет им создать эффективный и масштабируемый способ отслеживания, но, как вы скоро поймете, отслеживание сопряжено с некоторыми техническими проблемами.

Отправная точка

Как правило, самый распространенный способ это создание отдельных модулей, предназначенные для отслеживания, эти модули представляют собой просто инкапсулированные функциональные возможности, которые позволяют отправлять информацию в конечную точку, которая хранит полезную нагрузку, полученную от пользователей, на основе определенных типов событий.

Ниже простая реализация того, как может выглядеть модуль отслеживания:

class Tracker {
    static get ENDPOINT_URL() {
        return "my.endpoint.domain/tracking"
    }

    async track(payload) {
        const response = await fetch(
            Tracker.ENDPOINT_URL,
            {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json',
                 },
                 body: JSON.stringify(payload)
            }
        );

        return response;
    }

    ...
}

Как вы можете видеть выше, мы просто создаем класс, который содержит метод, который позволяет нам отправлять информацию в конечную точку; этот пример слишком прост, но этого достаточно для целей данной статьи, в реальном сценарии у вас будет/должна быть модель, которая проверяет параметры, которые вы хотите отслеживать, и тип данных, отправляемых в качестве полезной нагрузки.

Для этой статьи мы начнем с того, что в качестве цели будем отслеживать кнопку подписки, чтобы мы могли понять, сколько пользователей задействуют ее.

<button class="js-tracked-click subscription-button">
    Subscription Button 1
</button>

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

Модуль отслеживания

Этот вариант заключается в импорте модуля отслеживания в модули вашего приложения и внедрения функции отслеживания в части логики соответствующего блока кода. Реализация этого шаблона будет выглядеть примерно так:

import Tracker from './Tracker';

class SubscriptionButton {
    constructor() {
        this._buttonHandler();
    }

    _onButtonClick() {
        console.log('Click handler function');

        Tracker.track({            type: 'click',            element: 'Subscription_button_1'        });    }

    _buttonHandler() {
        const button = document.querySelector('.js-tracked-click');

        button.addEventListener('click', this._onButtonClick.bind(this));
    }

    ...
}

Это очень простой и функциональный подход, он широко используется, и у него есть несколько достоинств и недостатков, давайте проанализируем их:

Плюсы:

  • Гибкость. Так как мы добавляем метод отслеживания в функциональность скрипта, легко добавить отслеживание практически к любой логике.
  • Простота. Добавление трекеров является простой задачей, поскольку это просто вопрос добавления функции к логике, которая требует этого.
  • Унификация. Код отслеживания находится в том же месте, что и исходный код скрипта, хотя с одной стороны это плохо, он хорош тем, что позволяет вам узнать об этом в любое время, когда вам нужно внести изменения в функциональность.

Минусы:

  • Не соблюдается принцип единой ответственность. Добавление функции отслеживания в основной код скрипта нарушает принцип единой ответственности.
  • Отслеживаемые элементы не легко идентифицировать. Каждый скрипт содержит в своем ядре функцию отслеживания, что означает, что нам нужно перейти к его определению и изучить код, в который может быть добавлено отслеживание.
  • Риск масштабируемости: поскольку этот подход очень гибок, он может быстро выйти из-под контроля, поэтому было бы неплохо установить некоторые основные правила.

Изоляция отслеживаемых методов путем расширения исходного определения

Расширение исходного класса – еще один подход, целью которого является изоляция отслеживаемых элементов от функциональности исходного сценария. Идея состоит в том, чтобы расширить код для создания дополнительного слоя, предназначенного для отслеживания событий, давайте рассмотрим пример:

Мы реализуем функциональность скрипта:

class SubscriptionButton {
    constructor() {
        this._buttonHandler();
    }

    _buttonHandler() {
        this._button = document.querySelector('.js-tracked-click');

        this._button.addEventListener('click', this.onButtonClick.bind(this));
    }

    _onButtonClick() {
        this.elementHasClass = e.currentTarget.classList.contains('subscription-button');

        if (this.elementHasClass) {
            console.log('Click handler function');
        }
    }

    ...
}

Затем мы реализуем отслеживание:

import Tracker from './Tracker';

class TrackedSubscriptionButton extends SubscriptionButton {
    constructor() {
        super();

        this._trackedMethods();
    }

    _trackedMethods() {
        this._onButtonClickTracking();
        this._anotherTrackedElement();
    }

    _onButtonClickTracking() {
        if (super.elementHasClass) {
            super._button.addEventListener(
                'click',
                () => Tracker.track({
                    type: 'click',
                    element: 'Subscription_button_1'
                });
            );
        }
    }

    _anotherTrackedElement() { ... }
}

Обратите внимание, как мы можем изолировать код, связанный с отслеживанием, в другом классе, важно, чтобы вы понимали, что мы должны быть осторожны, чтобы не дублировать логику для элемента, который вы хотите отслеживать, убедиться, что есть логика отслеживается и она может использоваться повторно Исходный класс, что в вышеприведенном случае мы используем нового слушателя события addEventListener и условие, но условие фактически совпадает с классом родителя, мы просто повторно используем свойство, которое его определяет. Этот подход не должен быть реализован с наследованием; если вы хотите вместо этого написать функциональный и декларативный код, вы можете использовать функцию более высокого порядка, которая оборачивает функцию отслеживания.

Плюсы

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

Минусы

  • Гибкий, но с ограничениями. Мы можем добавить трекинг к любому элементу, который хотим, но мы всегда должны помнить о существование класса трекинга.
  • Изменение мышления. При использовании этого подхода вы всегда должны иметь в виду отслеживание точно так же, как и при модульном тестировании, вы всегда должны убедиться, что ваш код отслеживается в изолированном классе, это может быть очень полезно, но это должно быть хорошо продумано.
  • Опасный код и дублированная логика. Если вы посмотрите на класс отслеживания, вы увидите, что мы добавляем определенного слушателя для отслеживания события клика, что может быть опасно, особенно если есть логика, которую необходимо добавить вокруг отслеживания (например, условие). Кроме того, вам придется выставлять свойства через this, чтобы родительский класс мог наследоваться и использоваться.

Персонализированный подход

Другой способ отслеживать – это создать настраиваемую центрированную систему отслеживания, этот очень популярный шаблон, и я видел, что он используется в нескольких компаниях, обычно он состоит из отслеживания взаимодействий на основе свойств набора данных, например, скажем, вы хотите отслеживать клик по элементу:

Элементы для отслеживания:

<button data-click-tracking="subscription_button_left">    Subscribe
</button>

<button data-click-tracking="subscription_button_right">    Subscribe
</button>

Единая функциональность трекера кликов:

import Tracker from './Tracker';

class ClickTracker {
    constructor() {
        this._bindClicks();
    }

    static get TRACKED_ATTRIBUTE() {
        return 'data-click-tracking';
    }

    static get TRACKED_ELEMENTS() {
        return document.querySelectorAll(`[${ClickTracker.TRACKED_ATTRIBUTE}]`);
    }

    _onClickHandler(event) {
        const element = event.currentTarget.getAttribute(ClickTracker.TRACKED_ATTRIBUTE);

        Tracker.track({ type: 'click', element }));
    }

    _bindClicks() {
        ClickTracker.TRACKED_ELEMENTS.forEach(element => {
            element.addEventListener('click', this._onClickHandler.bind(this));
        });
    }
}

Таким образом, все отслеживаемые клики проходят через обработчик кликов, и мы можем идентифицировать их, используя пользовательский идентификатор, передаваемый через свойство набора данных. Отличным примером компаний, использующих этот подход, является менеджер тегов Google в google tag manager, где вы можете определить собственные классы или свойства данных, которые будут отслеживаться, и отправлять информацию в Google Analytics. Я считаю, что этот подход является лучшим из упомянутых до сих пор, поскольку вы можете применять этот же шаблон для других типов событий, таких как события прокрутки, он не ограничивается только кликами.

Плюсы

  • Индивидуальная реализация. Сделано для конкретных нужд компании.
  • Масштабируемость. За отслеживание отвечает один сценарий, поэтому остальные сценарии остаются нетронутыми.
  • Единственная ответственность, сохраняется, потому что функция отслеживания находится в выделенном модуле.

Минусы

  • Присутствуют ограничения. Поскольку этот подход состоит в отслеживании элементов из DOM, охватить все случаи не удастся. Вы скоро обнаружите, что для особых функциональных возможностей все еще необходимо отслеживание в его основном коде. Например когда нужно отследить факт входа в систему а не событие клика по кнопке. Это означает, что в особых случаях вам придется импортировать “Модуль отслеживания”.

Отслеживание асинхронных запросов

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

Для этого вы можете использовать подход с использованием модуля отслеживания, добавив функцию отслеживания к ответу 200. Это было бы простой и хорошей идеей, но мы должны были бы создать много условий для каждого запроса, который необходимо отслеживать.

Допустим, у вас есть централизованный HTTP-клиент, который вы используете для всех асинхронных запросов (что почти всегда будет так); этот клиент возвращает promise, чтобы вы могли выполнить некоторый код для каждого модуля, а затем мы получили некоторые требования к отслеживанию, как указано ниже.

Допустим на нужно отслеживать следующие события, чтобы получить значимую информацию о наших пользователях и узнать, как мы можем улучшить их работу на платформе:

  • Успешные события входа
  • Успешные события подписки
  • События выхода
  • События клика по кнопке “Призыв к действию”

Итак, мы понимаем, что “призыв к действию” можно легко отследить с помощью события отслеживания кликов, но как насчет других? Все это разные события, использующие разные URL и требующие отслеживания разных данных, поэтому если мы используем централизованный HTTP-клиент, это будет выглядеть примерно так:

function HTTPPost(url = '', data = {}) {
    return fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
         },
        cache: 'no-cache',
        redirect: 'follow',
        referrer: 'no-referrer',
        body: JSON.stringify(data),
    })
    .then(response => response.json());
}

export default HTTPPost;

и тогда мы сможем использовать его для отслеживания таких данных, как:

import HTTPPost from './http-client';

HTTPPost('/api/login', {userId, password, source: 'modal' })
    .then(response => {
        Tracker.track({ type: 'successful-login', ...response })    }
    .catch(error => console.error(error))

Приведенный выше подход на самом деле не является плохим, но нам придется каждый раз импортировать модуль Tracker в каждый файл, который будет выполнять асинхронный запрос.

Централизация асинхронного отслеживания

Это будет последний подход, который мы рассмотрим в этой статье, и он мне действительно нравится. Основы этого подхода основаны на добавлении функции отслеживания один раз в метод HTTPPost, после чего мы можем использовать словарь, который будет содержать URL-адреса, которые мы хотим отслеживать, и они будут сопоставлены с моделью свойств, где каждый URL-адрес должен отслеживаться.

Добавляем отслеживание в HTTPClient

В основном мы берем код из предыдущего подхода и добавляем отслеживание ответа на promises:

import Tracker from './Tracker';

function HTTPPost(url = '', data = {}) {
    return fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
         },
        cache: 'no-cache',
        redirect: 'follow',
        referrer: 'no-referrer',
        body: JSON.stringify(data),
    })
    .then(response => response.json())
    .then(response => Tracker.request(url, response));}

export default HTTPPost;

Как вы видите, мы выполняем Tracker.request для всех запросов, теперь мы должны определить, какие запросы мы на самом деле хотим отслеживать и какие параметры релевантны для отслеживания этих запросов, поэтому мы можем использовать словарь, подобный этому:

const TRACKED_URLS = {
    '/api/login': ['userId', 'source', 'url', 'type'],
    '/api/logout': ['userId', 'time', 'type'],
    'api/subscription': ['userId', 'source', 'type'],
    ...
};

export default TRACKED_URLS;

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

import TRACKED_URLS from './tracked-urls';

class Tracker {
    static get ENDPOINT_URL() {
        return "my.endpoint.domain/tracking"
    }

    async track(payload) {
        const response = await fetch(
            Tracker.ENDPOINT_URL,
            {
                 method: 'POST',
                 headers: {
                     'Content-Type': 'application/json',
                 },
                 body: JSON.stringify(payload)
            }
        );

        return response;
    }

    request(url, data) {        
       const URL_PROPERTIES = TRACKED_URLS[url];        
       const PAYLOAD_PROPERTIES = Object.keys(data);        
       const arePropertiesValid = URL_PROPERTIES &&
          URL_PROPERTIES.every(property => (                
             PAYLOAD_PROPERTIES.includes(property)
       ));        
       
       if (!arePropertiesValid) return false;        
       
       this.track(data);    
    }
}

Метод request проверяет, что все отслеживаемые элементы имеют правильные переданные свойства, он служит в качестве централизованного фильтра и словаря отслеживания централизованного запроса. Этот подход прост и очень хорошо масштабируется, потому что у вас есть все отслеживаемые URL-адреса в одном месте, которое позволяет быстро добавлять и удалять их по требованию.

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

На данный момент это все, я надеюсь, что вам понравилось это статья – если это так, помните, что вы можете поделиться им со своими друзьями или оставить комментарий в Reddit или Twitter, нажав на социальные ссылки (в оригинальной статье).

Оригинальная статья Enmanuel DuránEfficient behavioral tracking in javascript applications

Была ли вам полезна эта статья?
[0 / 0]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments