Определение продолжительности пребывания пользователя на сайте — разумный подход

В одном из проектов компании где я работаю, мне пришлось столкнуться с интересной задачей: с точностью до секунды нужно было определить время, в течение которого пользователь находился на сайте. Ничего сложного, обычная задача, если бы не несколько условий:
  • время должно считаться одинаково для всего сеанса, независимо от количества одновременно открытых страниц;
  • если пользователь полностью закроет браузер и заново откроет вебсайт в течение 5 минут, то следует продолжить считать время (продолжаем сеанс), а не начинать отсчет с 0.

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

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

Прежде всего было принято решение что все промежуточные данные, касающиеся данного алгоритма должны храниться на стороне клиента в Local Storage браузера.

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

//User Behaviour Tracking
var UBT_Logic = function() {
    this.ActiveTimeout = 60; //1min
    this.IdleTimeout = 60; //1min
    this.IntervalId = 0;
    this.TimeFlag = "UBT_TimeFlag";
    this.TimeMsgFlag = "UBT_TimeMsgFlag";
    this.LastActiveTimeFlag = "UBT_LastActiveTimeFlag";
    this.LastUserActivityTimeFlag = "UBT_LastUserActivityTimeFlag";
};

UBT_Logic.prototype.StartFunc = function () {
    //here can be some additional logic
    ubtLogic.InitTimeFlag();
};

UBT_Logic.prototype.InitTimeFlag = function(){
    var now = Math.floor((new Date().getTime()) / 1000);
    if ((typeof localStorage[ubtLogic.LastActiveTimeFlag]) == "undefined") {
        localStorage[ubtLogic.LastActiveTimeFlag] = now;
        localStorage[ubtLogic.TimeFlag] = 0;
        localStorage[ubtLogic.TimeMsgFlag] = 0;
    } else {
        var t = parseInt(localStorage[ubtLogic.LastActiveTimeFlag]);
        if (t <= 0 || t + 300 < now || t > now) {
            localStorage[ubtLogic.LastActiveTimeFlag] = now;
            localStorage[ubtLogic.TimeFlag] = 0;
            localStorage[ubtLogic.TimeMsgFlag] = 0;
        }
    }
    if ((typeof localStorage[ubtLogic.TimeFlag]) == "undefined") {
        localStorage[ubtLogic.TimeFlag] = 0;
        localStorage[ubtLogic.TimeMsgFlag] = 0;
    } else {
        var t = parseInt(localStorage[ubtLogic.TimeFlag]);
        if (t < 0 || t > now) {
            localStorage[ubtLogic.TimeFlag] = 0;
            localStorage[ubtLogic.TimeMsgFlag] = 0;
        }
    }
    if ((typeof localStorage[ubtLogic.TimeMsgFlag]) == "undefined")
        localStorage[ubtLogic.TimeMsgFlag] = 0;
    else {
        var t = parseInt(localStorage[ubtLogic.TimeMsgFlag]);
        if (t != 0 && t != 1) localStorage[ubtLogic.TimeMsgFlag] = 0;
    }
    localStorage[ubtLogic.LastUserActivityTimeFlag] = now;
    document.onmousemove = function (event) {
        localStorage[ubtLogic.LastUserActivityTimeFlag] = Math.floor((new Date().getTime()) / 1000);
    };
    document.onkeydown = function (event) {
        localStorage[ubtLogic.LastUserActivityTimeFlag] = Math.floor((new Date().getTime()) / 1000);
    };
    IntervalId = setInterval(ubtLogic.TimeFunc, 300);
};

UBT_Logic.prototype.TimeFunc = function () {
    var now = Math.floor((new Date().getTime()) / 1000);
    //console.log(now);
    var lastActiveTimeFlag = parseInt(localStorage[ubtLogic.LastActiveTimeFlag]);
    if (lastActiveTimeFlag < now) {
        localStorage[ubtLogic.LastActiveTimeFlag] = now;
        var timeFlag = parseInt(localStorage[ubtLogic.TimeFlag]);
        timeFlag++;
        localStorage[ubtLogic.TimeFlag] = timeFlag;
        var timeMsgFlag = parseInt(localStorage[ubtLogic.TimeMsgFlag]);
        var lastUserActivityTimeFlag = parseInt(localStorage[ubtLogic.LastUserActivityTimeFlag]);
        if (timeMsgFlag == 0) {
            if (timeFlag >= ubtLogic.ActiveTimeout) {
                localStorage[ubtLogic.TimeMsgFlag] = 1;
                ubtLogic.ActiveTimeoutFunc();
            }
            else if (lastUserActivityTimeFlag + ubtLogic.IdleTimeout <= now) {
                localStorage[ubtLogic.TimeMsgFlag] = 1;
                ubtLogic.IdleTimeoutFunc();
            }
        }
        if (localStorage[ubtLogic.TimeMsgFlag] != 0) {
            clearInterval(IntervalId);
        }
    }
};

UBT_Logic.prototype.ActiveTimeoutFunc = function () {
    //todo: do something
    //...
    console.log("ActiveTimeout");
};

UBT_Logic.prototype.IdleTimeoutFunc = function () {
    //todo: do something
    //...
    console.log("IdleTimeout");
};

var ubtLogic = new UBT_Logic();
ubtLogic.StartFunc();

Давайте разберем вышеприведенный код. Параметры ActiveTimeout и IdleTimeout содержат соответствующие значения таймаутов. Функция InitTimeFlag отвечает за начальную инициализацию флагов. Собственно за обработку отвечает функция TimeFunc.

Как же происходит синхронизация данных между несколькими открытыми вкладками? Все просто: парамерт UBT_LastActiveTimeFlag в Local Storage содержит последнее время запуска функции TimeFunc и соответственно если с того момента еще не прошло 1 секунды, то функция ничего не делает. Таким образом, независимо от количества открытых вкладок, счетчик работает одинаково. Значения в Local Storage хранятса с точностю до 1 секунды. Касательно сохранения продолжительности сессии при закрытии браузера до 5 минут — за это отвечает проверка t + 300 <now.

Стоит сказать, что есть две незначительные вещи, которые формируют погрешность:
  • при переходах между страницами может опуститься несколько вызовов TimeFunc, но этим можно пренебречь, ведь по факту в это время пользователь не читает вебсайт, а ожидает его загрузки;
  • мы не высчитываем точное количество миллисекунд, а проверяем стоит ли инкриминировать значение UBT_TimeFlag раз в 300 миллисекунд, но этим можно пренебречь, ведь в конечном итоге погрешность никак не выйдет за пределы 1 секунды.

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

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.