12
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Проверка полноты данных о событиях и их параметрах

Время чтения 63 минуты
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
12
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Приветствую специалистов по данным! Я Павел Беляев, тимлид группы обработки данных в дата-офисе сервиса автоматизации интернет-рекламы eLama и ведущий канала «Тимлидское об аналитике».

Одна из наших задач — обеспечивать веб-аналитику, то есть собирать и обрабатывать данные о поведении посетителей сайтов компании. Эти данные представляют собой множество событий, инициированных посетителями. Причем структура события содержит компоненты «Событие + Свойства события + Свойства пользователя». Мы отлавливаем события на сайте и доставляем их в ClickHouse.

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

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

Проверка полноты данных о событиях и их параметрах

Каталог событий

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

  • Хранить метаданные событий — в каких случаях они генерируются, в какой части сайта, кто заказчик события, когда оно начало собираться, когда сбор прекратился.
  • Согласовывать разметку событий и ТЗ на их сбор между продактами, аналитиками, разработчиками и другими акторами.
  • Проверять полноту данных по каждому событию, чтобы обнаружить пропажу.

Наша таблица с событиями имеет такие поля:

  • insert_date — дата внесения события в таблицу.
  • finish_date — заполняется, если событие временное, то есть заранее известно, когда оно перестанет генерироваться (например, для А/В-теста).
  • event_category — категория события, справочное поле для удобства группировки событий. Например: Accounts — группа событий из раздела «Аккаунты» в личном кабинете.
  • event — обозначение события, которое, собственно, и передается в базу данных. Обычно мы закладываем в него иерархический путь к действию, инициирующему событие. Например: Accounts — Statistics by Accounts — Filter and Search — Click Apply.
  • event_description — описание события: что это за действие и где его может выполнить пользователь.
  • parameters — параметры или свойства события, если они необходимы. Указываются через запятую, например: ad_system, title.
  • examples — типовые значения свойств или их полный перечень. Например: «ad_system: [ВКонтакте|Яндекс Директ|Google Ads| …], title: название аккаунта».
  • parameter_description — описание параметров события.
  • task — ссылка на задачу настройки события в таск-трекере.
  • Заказчик/отдел — кто заказал событие.

Постановка задачи

Возможны четыре проблемы, о которых мы хотим своевременно узнавать:

  1. Нового события нет в БД. Прошел месяц с постановки задачи разработчикам на его настройку, но никаких данных о событии так и не пришло.
  2. Данные по событию поступают без требуемых параметров.
  3. Данные по событию перестали поступать.
  4. Данные по параметрам события не всегда полные.

Разберем эти случаи отдельно.

Нет нового события

Это актуально, если настройку события на сайте осуществляют не аналитики, а другая команда. Аналитики ставят задачу, дату постановки заносят в insert_date и спокойно забывают об этом. Следить за исполнением задачи будет запланированный скрипт, который ежедневно запускает запрос:


SELECT event_category, event, start_date, task
FROM
(
-- события, которые передаются разработке для разметки на сайте
SELECT toDate(parseDateTimeBestEffortOrNull(insert_date)) AS start_date,
     event_category, event, task
FROM datamart.events_dictionary -- справочник событий
) dict
 
-- убираем события, по которым данные пошли
ANTI LEFT JOIN
(
SELECT DISTINCT event_name
FROM datamart.user_events -- события в аналитическом хранилище
) events ON dict.event = events.event_name
WHERE start_date + INTERVAL 1 MONTH <= today() -- Даем исполнителям месяц на настройку события
OR start_date IS NULL
 

 

Если результат не пуст, скрипт сигнализирует об этом в корпоративный мессенджер. А далее дежурному аналитику нужно:

  • Проверить в user_events, действительно ли нет такого события. Возможно, не совпадает наименование? Разработчики могли ошибиться с именем. Тогда достаточно исправить имя события в каталоге на реальное, а лучше — добиться правильной отправки событий (хотя уже отправленные не изменятся, их придется править вручную или подменять при обращении к ним).
  • Если данных по новому событию действительно нет, то по задаче из поля task находится и оповещается заказчик. Если событие еще в работе у разработки, меняется insert_date на текущую дату (в ячейке оставляется комментарий, какая была первоначальная дата занесения), и тем самым исполнителям дается еще один месяц.

Нет параметров события

Положим, что события складываются к нам в аналитическое хранилище в таблицу datamart.user_events, причем его параметры хранятся в поле data в виде JSON-словаря. Тогда нам нужно выявить события, у которых есть параметры, которые никогда не приходили заполненными, приблизительно таким запросом:


SELECT event, parameters_upd, task, start_date,
-- подсчитываем заполненные параметры, указанные в справочнике событий
COUNT(nullIf(JSONExtractString(data, parameters_upd), '')) AS value_data_count
FROM
(
SELECT event_name, `data`, toDate(stream_date) AS date
FROM datamart.user_events -- данные о событиях
) AS events
 
JOIN
(
SELECT event, task,
     toDate(parseDateTimeBestEffortOrNull(insert_date)) AS start_date,
     -- разбиваем параметры из справочника на отдельные строки
     arrayJoin(splitByRegexp('([, \n]+)', assumeNotNull(parameters))) AS parameters_upd
FROM datamart.events_dictionary -- справочник событий
WHERE parameters_upd != ''
     -- берем только актуальные события
     AND (toDate(parseDateTimeBestEffortOrNull(finish_date)) AS last_date IS NULL
         OR last_date >= yesterday())
) AS dict ON events.event_name = dict.event
 
GROUP BY ALL
HAVING value_data_count=0
 

 

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

Данные по событиям перестали поступать

А здесь придется действовать хитрее. Мы не знаем, как часто случается событие, и его отсутствие еще не означает, что имеет место инцидент. Нам придется немного погрузиться в статистику и вычислить допустимое количество дней без данных для каждого события. Для этого мы разделим события на 4–5 паттернов по частоте их появления и определим, сколько дней после отсутствия данных можно не беспокоиться, а когда начинать бить тревогу. Вот запрос для определения порога предупреждения и критического порога:


SELECT event_name, median_interval, max_interval, p90_interval,
-- Классификация событий по регулярности на основе медианного интервала
-- Определяет паттерн поступления события для дальнейшего расчета порогов
CASE
     WHEN median_interval <= 1.2 THEN 'daily'     -- События, происходящие почти каждый день
     WHEN median_interval <= 3 THEN 'frequent'    -- Частые события (2-3 раза в неделю)
     WHEN median_interval <= 7 THEN 'weekly'      -- Еженедельные события
     WHEN median_interval <= 14 THEN 'bi_weekly' -- События раз в две недели
     ELSE 'rare'                                  -- Редкие события (реже раза в две недели)
END AS event_pattern,
-- Адаптивный порог для предупреждений (warning)
-- Рассчитывается индивидуально для каждого паттерна событий
CASE
     WHEN event_pattern = 'daily' THEN 2                     -- Ежедневные: тревога после 2 дней отсутствия
     WHEN event_pattern = 'frequent' THEN round(p75_interval + 2)   -- Частые: 75-й перцентиль + 2 дня буфер
     WHEN event_pattern = 'weekly' THEN round(p90_interval + 1)  -- Еженедельные: 90-й перцентиль + 1 день
     WHEN event_pattern = 'bi_weekly' THEN round(p95_interval + 2-- Двухнедельные: 95-й перцентиль + 2 дня
     ELSE 30                                                        -- Для редких событий: фиксированный порог 30 дней
END AS warning_threshold, 
-- Адаптивный порог для критических алертов
-- Более высокие значения, чем warning, для эскалации проблем
CASE
     WHEN event_pattern = 'daily' THEN 5                     -- Критично после 5 дней отсутствия
     WHEN event_pattern = 'frequent' THEN round(p90_interval + 3)   -- 90-й перцентиль + 3 дня
     WHEN event_pattern = 'weekly' THEN round(p95_interval + 3)  -- 95-й перцентиль + 3 дня
     WHEN event_pattern = 'bi_weekly' THEN round(max_interval * 1.2) -- 120% от максимального интервала
     ELSE 60                                                         -- Для редких событий: 60 дней
END AS critical_threshold, 
-- Дата обновления порогов для отслеживания актуальности
today() AS update_date
FROM
(
-- Расчет статистических показателей интервалов между событиями
SELECT event_name
        ,round(median(days_since_last_event), 1) AS median_interval  -- Медианный интервал (дней)
     ,max(days_since_last_event) AS max_interval               -- Максимальный интервал
     ,quantile(0.75)(days_since_last_event) AS p75_interval    -- 75-й перцентиль
     ,quantile(0.90)(days_since_last_event) AS p90_interval    -- 90-й перцентиль
     ,quantile(0.95)(days_since_last_event) AS p95_interval    -- 95-й перцентиль
 
FROM
(
     -- Расчет интервалов между последовательными событиями
     SELECT event_name, event_date,
         -- Вычисляем количество дней с момента предыдущего события того же типа
         greatest(
             date_diff('day',
                    lagInFrame(event_date, 1, event_date) OVER (
                     PARTITION BY event_name
                        ORDER BY event_date
                 ),
                 event_date
             ) - 1-- Вычитаем 1, т. к. события в один день = интервал 0
             0
         ) AS days_since_last_event
     FROM
     (
         -- Выборка уникальных дат событий за последний год
         SELECT DISTINCT event_type,
                toDate(stream_date) AS event_date  -- Преобразование timestamp в дату
         FROM datamart.user_events AS db    -- Таблица с потоковыми событиями пользователей
        
         -- Присоединяем справочник для фильтрации активных событий
         JOIN datamart.events_dictionary AS dict -- Справочник типов событий из Kafka
             ON dict.event = db.event_type
        
         -- Берем события за последние 12 месяцев до вчерашнего дня
         WHERE event_date BETWEEN today() - INTERVAL 12 MONTH AND yesterday()
             -- Фильтруем только активные события (без даты окончания или окончание в будущем)
             AND (toDate(parseDateTimeBestEffortOrNull(finish_date)) AS last_date IS NULL
                  OR last_date >= yesterday())
     )
)
GROUP BY ALL
) AS interval_stats
 

 

Теперь можно сравнить длительность отсутствия события с допустимым порогом. А если порог превышен, выдается соответствующее сообщение. Запрос для такой проверки:


SELECT ev.event_name AS event_name
,last_event_date -- дата последнего появления события
,days_since_last -- количество дней с последнего появления
,warning_threshold -- порог предупреждения
,critical_threshold -- критический порог (инцидент)
,CASE
     WHEN critical_threshold IS NULL OR warning_threshold IS NULL THEN 'NEW EVENT'
     WHEN days_since_last >= critical_threshold THEN 'CRITICAL'
     WHEN days_since_last >= warning_threshold THEN 'WARNING'
     ELSE 'OK'
END AS alert_level -- тип оповещения
,CASE
     WHEN alert_level = 'NEW EVENT' THEN CONCAT('Новое событие: ', 
          toString(first_event_date), ' первые данные')
     WHEN alert_level = 'CRITICAL' THEN CONCAT('Критично: ', 
          toString(days_since_last), ' дней без данных (норма: до ', toString(critical_threshold), ')')
     WHEN alert_level = 'WARNING' THEN CONCAT('Внимание: ', 
          toString(days_since_last), ' дней без данных (норма: до ', toString(warning_threshold), ')')
     ELSE CONCAT('Норма: последнее событие ', toString(days_since_last), ' дней назад')
END AS status_description -- Описание проблемы. Например: 'Критично: 8 дней без данных (норма: до 5)'
,update_date AS score_update_date -- дата перерасчета порогов (мы обновляем их раз в неделю)
,today() AS dq_date_check -- Дата проверки (сегодняшняя)
 
FROM datamart.event_observability_score AS metrics -- таблица с порогами
 
RIGHT JOIN
(
-- все события, по которым была статистика в последний год
SELECT event_name
     ,max(toDate(stream_date)) AS last_event_date
     ,min(toDate(stream_date)) AS first_event_date -- минимальная дата за последний год
     ,date_diff('day', last_event_date, today()) AS days_since_last
FROM datamart.user_events AS db
JOIN datamart.events_dictionary AS dict
     ON dict.event = db.event_name
WHERE toDate(stream_date) BETWEEN today() - INTERVAL 12 MONTH AND yesterday()
     -- Берем только актуальные
     AND (toDate(parseDateTimeBestEffortOrNull(finish_date)) AS last_date IS NULL OR last_date >= yesterday())
GROUP BY ALL
) AS ev ON metrics.event_name = ev.event_name
 
WHERE alert_level != 'OK'  -- Берем только проблемные события
 

 

В результате имеем три типа оповещений:

  • NEW EVENT — данные по новому событию только появились, пороговые значения еще не рассчитаны.
  •  WARNING — предупреждение, данные по событию отсутствуют дольше, чем обычно.
  • CRITICAL — инцидент, данные по событию отсутствуют неприемлемо долго.

Данные по параметрам события перестали поступать

Действуем аналогично третьему кейсу, но применительно не к событиям, а к их параметрам:

  1. Рассчитываем допустимые пороги отсутствия заполненных параметров. Только вместо количества дней используем долю событий с заполненным параметром в общем числе данных событий с этим параметром.
  2. Сравниваем текущую долю заполненных параметров с порогом и при необходимости оповещаем.

Расчет порогов:



 SELECT event AS event_name, parameters
,round(median(not_null_percent_param_per_week), 1) AS median_interval
,min(not_null_percent_param_per_week) AS min_interval
,quantile(0.25)(not_null_percent_param_per_week) AS p25_interval
,quantile(0.10)(not_null_percent_param_per_week) AS p10_interval
-- Классификация параметра события по регулярности
,CASE
     WHEN median_interval = 0 AND min_interval = 0 THEN 'Не настроен'
     WHEN median_interval <= 35 THEN 'Редкий'
     WHEN median_interval <= 75 THEN 'Периодический' 
     WHEN median_interval <= 95 THEN 'Частый'
     WHEN median_interval <= 100 THEN 'Постоянный'
     ELSE NULL
END AS event_pattern
-- Адаптивные пороги
,CASE
     WHEN event_pattern = 'Постоянный' THEN round(median_interval * 0.97, 2)
     WHEN event_pattern = 'Частый' THEN round(p25_interval * 0.97, 2)
     WHEN event_pattern = 'Периодический' THEN round(p25_interval * 0.90, 2)
     WHEN event_pattern = 'Редкий' THEN round(p25_interval * 0.80, 2)
     ELSE 0
END AS warning_threshold
   ,CASE
     WHEN event_pattern = 'Постоянный' THEN round(median_interval * 0.95, 2)
     WHEN event_pattern = 'Частый' THEN round(p25_interval * 0.95, 2)
     WHEN event_pattern = 'Периодический' THEN round(p25_interval * 0.85, 2)
     WHEN event_pattern = 'Редкий' THEN round(p25_interval * 0.70, 2)
     ELSE 0
END AS critical_threshold
,today() AS update_date
FROM
(
SELECT event, parameters, week
     ,count(*) AS total_amount
     ,countIf(event, value_data IS NOT NULL) AS not_null_amount 
     ,round(divide(not_null_amount, total_amount) * 100, 2) AS not_null_percent_param_per_week
     ,first_param_date
FROM
(
     -- Начало недели — это вс (так работает toStartOfWeek без доп. параметра). 
     -- Срез по неделе — с вс по сб. Обновление этой витрины делаем каждое вс.
     -- Получаем, что в каждое вс в витрине данные по сб, последняя неделя получается полной и не искажает статистику.
     SELECT event, parameters_upd AS parameters
         ,toStartOfWeek(date) AS week
         ,nullIf(JSONExtractString(data, parameters_upd), '') AS value_data
         ,minIf(date, value_data IS NOT NULL) OVER(PARTITION BY event, parameters_upd) AS first_param_date
     FROM
     (
         SELECT event_name, data
             ,toDate(stream_date) AS date
        FROM datamart.user_events
         WHERE date BETWEEN today() - INTERVAL 6 MONTH AND yesterday()
     ) AS ev
    
     JOIN
     (
         -- Все актуальные события с параметрами
         SELECT event
             ,arrayJoin(splitByRegexp('([, \n]+)', assumeNotNull(parameters))) AS parameters_upd
         FROM datamart.events_dictionary
         WHERE parameters_upd != ''
             AND (toDate(parseDateTimeBestEffortOrNull(finish_date)) AS last_date IS NULL OR last_date >= yesterday())
     ) AS dict ON ev.event_name = dict.event
)
--    Некоторые параметры разработка добавляет после того, как статистика по событию уже пошла.
--    Чтобы исключить попадание нулевых значений в срез для расчета квантилей, применяем фильтр ниже.
--    Т. е. мы можем потерять пороги по новому параметру события на первой неделе, на второй он появится.
WHERE week > first_param_date
     OR first_param_date IS NULL
GROUP BY ALL
)
GROUP BY ALL
 

 

И проверка доли событий с заполненным параметром:


SELECT ev.event AS event_name, ev.parameters AS parameters
    ,not_null_percent_param_per_current_week
,warning_threshold
,critical_threshold
,CASE
     WHEN critical_threshold IS NULL OR warning_threshold IS NULL THEN 'NEW EVENT PARAMETER'
     WHEN not_null_percent_param_per_current_week < critical_threshold THEN 'CRITICAL'
     WHEN not_null_percent_param_per_current_week < warning_threshold THEN 'WARNING'
     ELSE 'OK' -- сюда же относим event_pattern = 'Не настроен'
END AS alert_level
,CASE
     WHEN alert_level = 'NEW EVENT PARAMETER' THEN CONCAT('Новый параметр: ', 
          toString(min_param_date), ' первые данные')
     WHEN alert_level = 'CRITICAL' THEN CONCAT('Критично: ', 
          toString(not_null_percent_param_per_current_week), 
          ' % заполненности параметра (норма: до ', toString(critical_threshold), ' %)')
     WHEN alert_level = 'WARNING' THEN CONCAT('Внимание: ', 
          toString(not_null_percent_param_per_current_week), 
          ' % заполненности параметра (норма: до ', toString(warning_threshold), ' %)')
     ELSE CONCAT('Норма: ', toString(not_null_percent_param_per_current_week), 
          ' % заполненности параметра')
END AS status_description
,update_date AS score_update_date
,today() AS dq_date_check
FROM datamart.event_parameter_observability_score AS metrics -- таблица с порогами для параметров
 
RIGHT JOIN
(
     SELECT event, parameters_upd AS parameters
         ,min(date) AS min_param_date
         ,count(*) AS total_amount
         -- количество строк с заполненным параметром
         ,countIf(event, nullIf(JSONExtractString(data, parameters_upd), '') IS NOT NULL) AS current_not_null_amount
         -- доля заполненных
         ,round(divide(current_not_null_amount, total_amount) * 100, 2) AS not_null_percent_param_per_current_week
     FROM
     ( -- Статистика событий за неделю (по вчера)
         SELECT event_name, `data` -- JSON с параметрами события
             ,toDate(stream_date) AS date
         FROM datamart.user_events
         WHERE date BETWEEN today() - INTERVAL 1 WEEK AND yesterday()
     ) AS ev
    
     JOIN
     ( -- Все актуальные события с параметрами из справочника
         SELECT event
             ,arrayJoin(splitByRegexp('([, \n]+)', assumeNotNull(parameters))) AS parameters_upd
         FROM datamart.events_dictionary -- справочник событий
         WHERE parameters_upd != ''
             AND (toDate(parseDateTimeBestEffortOrNull(finish_date)) AS last_date IS NULL OR last_date >= yesterday())
     ) AS dict ON ev.event_type = dict.event
    
     GROUP BY ALL
) AS ev ON metrics.event_name = ev.event
AND metrics.parameters = ev.parameters
 
WHERE alert_level != 'OK'

 

Резюме

Мы рассмотрели четыре случая некачественности данных в событиях веб-аналитики, а также способы отследить эти проблемы:

  • События не поступают долгое время после постановки задачи на их настройку. Для мониторинга считаем количество появлений события. Если их ноль более месяца с постановки задачи — даем сигнал.
  • То же для параметров событий. События приходят, но параметры не заполнены. Опять же, считаем количество заполненных и реагируем на пустые.
  • Событие перестало поступать. На основе уже имеющихся данных о событии выясняем его допустимую частоту и допустимое количество пропущенных дней. Если последний пропуск больше порога — даем сигнал.
  • Параметр события поступает с перебоями или перестал поступать. Рассчитываем допустимую долю событий с заполненным параметром и сверяем с ней текущее значение этой доли. Если оно ниже порогового — сигнализируем о проблеме.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники