Перейти к содержанию

Договор событий учебного сайта

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

Создадим договор событий для analytics-lab. Он связывает действие, параметры и момент вызова. Внешняя аналитика пока не нужна: локальный журнал покажет, какой объект формирует страница. Документы xml и markdown сохраняют прежние ID и URL, а вымышленный маршрут reading-demo остаётся отдельным учебным упражнением.

Название и момент события

Назовём события lesson_open, lesson_next и sample_download. Первое вызывается один раз после подготовки конкретного экземпляра страницы. Если человек перезагрузил документ, появился другой экземпляр, поэтому возможно новое открытие. Повторная установка обработчиков внутри той же страницы не должна создавать второе открытие без нового действия посетителя.

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

Для всех сообщений нужны schema_version, article_id, course_id, course_version и surface. У перехода добавляется target_article_id. Поле surface отвечает на вопрос, откуда выполнено действие: статья, навигация или блок примера. Оно принимает маленький известный набор значений, а не произвольную строку из HTML.

Поле Пример Назначение
schema_version 1 Версия смысла и состава сообщения
article_id markdown Исходный документ
course_id reading-demo Учебный маршрут
course_version 1 Версия последовательности
target_article_id xml Назначение перехода
surface navigation Место действия

Название статьи сознательно не используется вместо ID. Заголовок может стать яснее после редактирования, однако это тот же материал. Полный URL тоже избыточен для наших событий: в нём бывают метки кампании и другие параметры. Постоянный ID позволяет соединить сообщение с проверенным реестром документов, не передавая случайные строки посетителя.

Локальный интерфейс отправки

Подготовим полный файл assets/events.js. Его функции работают с известными именами и параметрами. По умолчанию сообщение остаётся в браузере. Платформенный приёмник добавляется отдельно через setAnalyticsSink, поэтому UI не зависит от прямого вызова Метрики или GA4 в каждом обработчике.

const documents = new Set(["xml", "markdown"]);
const names = new Set(["lesson_open", "lesson_next", "sample_download"]);
const surfaces = new Set(["article", "navigation", "example"]);
let sink = event => console.info("analytics-lab", event);

export function setAnalyticsSink(nextSink) {
  if (typeof nextSink !== "function") throw new TypeError("Sink required");
  sink = nextSink;
}

export function recordLessonEvent(name, fields) {
  if (!names.has(name) || !documents.has(fields.article_id)) {
    throw new TypeError("Unknown event or article");
  }
  if (!surfaces.has(fields.surface)) throw new TypeError("Unknown surface");
  const event = {
    event_name: name, schema_version: 1,
    article_id: fields.article_id,
    course_id: "reading-demo", course_version: 1,
    surface: fields.surface
  };
  if (name === "lesson_next") {
    if (!documents.has(fields.target_article_id)) {
      throw new TypeError("Unknown target");
    }
    event.target_article_id = fields.target_article_id;
  }
  window.dispatchEvent(new CustomEvent("pw:analytics", {detail: event}));
  try { sink(event); } catch (error) { console.warn("Analytics sink", error); }
  return event;
}

Функция создаёт новый объект из разрешённых полей, а не пересылает весь аргумент. Даже если вызывающий код случайно добавил email, объект сообщения его не содержит. Такая конструкция не заменяет общую политику данных, но делает договор заметным в программе. Неизвестное имя события вызывает ошибку разработки вместо новой случайной строки отчёта.

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

Привязка к HTML

На странице Markdown укажем её личность и действие продолжения. Это фрагмент тела учебного HTML; остальные области страницы остаются в общих шаблонах. Модуль article-events.js ниже добавляется одним подключением, которое также может находиться в общей оболочке.

<main data-article-id="markdown">
  <h1>Статический сайт из Markdown</h1>
  <a href="/my/LINQ/linq_xml/level7/7_1.php"
     data-next-article-id="xml">Посмотреть учебный пример XML</a>
  <a href="/examples/markdown.txt" data-sample-download>
    Открыть учебный исходник
  </a>
</main>
<script type="module" src="/assets/article-events.js"></script>

Полный assets/article-events.js использует определённый выше интерфейс. Защита data-analytics-ready относится к конкретной странице, поэтому повторная инициализация модуля не размножает обработчики. Она не сохраняется между загрузками и не пытается распознать посетителя.

import {recordLessonEvent} from "./events.js";
const root = document.querySelector("main[data-article-id]");
if (root && root.dataset.analyticsReady !== "true") {
  root.dataset.analyticsReady = "true";
  const article_id = root.dataset.articleId;
  recordLessonEvent("lesson_open", {article_id, surface: "article"});
  root.addEventListener("click", event => {
    if (!(event.target instanceof Element)) return;
    const link = event.target.closest("a[data-next-article-id],a[data-sample-download]");
    if (!link || !root.contains(link)) return;
    if (link.hasAttribute("data-next-article-id")) {
      recordLessonEvent("lesson_next", {
        article_id, target_article_id: link.dataset.nextArticleId,
        surface: "navigation"
      });
    } else {
      recordLessonEvent("sample_download", {article_id, surface: "example"});
    }
  });
}

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

Ожидаемая локальная запись при активации содержит lesson_next, исходный ID markdown и назначение xml. Если в HTML перепутано назначение, аналитика может принять допустимый, но неверный ID. Проверка списка значений защищает форму сообщения, а согласование href и data-next-article-id требует чтения HTML и контрольного сценария.

Договор важнее удобства платформы

В GA4 пользовательские события имеют правила именования и ограничения параметров. В Метрике целевое событие связано с идентификатором настроенной цели. Собственная схема должна быть достаточно простой, чтобы явный адаптер мог передать её в выбранную платформу, однако значения интерфейса сервиса не должны незаметно менять смысл исходного действия. Правила названий GA4, целевые события Метрики.

Предположим, редактор назвал скачивание sample_download, а позже добавил отдельную кнопку «скопировать код». У этих действий разные значения. Новую кнопку нельзя подключить к старому событию только потому, что обе находятся рядом с примером. Для неё потребуется отдельное определение и оценка нужности. Иначе изменение UI незаметно изменит исторический показатель.

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

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

Теперь страница формирует понятный объект и остаётся работоспособной без внешнего приёмника. Следующий урок добавит адаптер Метрики, сохранив договор и обычную навигацию. Мы будем отдельно наблюдать вызов локальной функции, передачу платформе и появление данных в её отчёте.