Перейти к основному контенту

Как трассировать и отлаживать сложные процессы и функционал

⚠️ Важно перед началом

Ниже приведён реальный рабочий плагин, который используется в проектах Metabot для трассировки и отладки сложной логики.

Этот код:

  • не является учебным примером «в вакууме»
  • не упрощён специально для урока
  • отражает практический инженерный подход, применяемый в боевых сценариях

Вы можете:

  • использовать этот плагин как есть в своих проектах
  • брать его в качестве основы для собственных трассировщиков
  • адаптировать под свои таблицы, поля и процессы

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

Рассматривайте этот материал как:

  • пример архитектурного подхода
  • рабочий шаблон
  • отправную точку для собственных решений

Ниже — описание принципов и полный исходный код плагина.

Универсальный трассировщик событий

Автор: Art Yg

Tracer предназначен для записи любых диагностических событий в таблицы базы данных:

  • ошибок
  • проверок
  • ветвлений логики
  • внутренних состояний
  • отладочной информации

Tracer не знает:

  • что такое сессия
  • что такое скрипт, команда или триггер
  • какие поля считаются «правильными»

Он записывает ровно те данные, которые ему передали.


Основные принципы

  • Stateless — не хранит состояние
  • Schema-agnostic — не требует фиксированной схемы
  • Zero mandatory fields — нет обязательных полей
  • Opt-in — работает только если включён
  • Side-effect only — не влияет на выполнение кода

Tracer можно удалить из проекта — бизнес-логика продолжит работать.


Минимальные требования

Для начала работы достаточно:

  1. Создать любую таблицу в БД (даже без полей, кроме id)
  2. Добавить атрибут бота TRACER_CONFIG
  3. Вызвать trace()

Рекомендуемое, но не обязательное поле таблицы:

  • created_at с автозаполнением (NOW)

Tracer не управляет временем. Если поле есть — БД заполнит его автоматически. Если нет — запись всё равно создаётся.


Конфигурация

Tracer настраивается через один JSON в атрибутах бота: TRACER_CONFIG.

{
  "navigation": {
    "enabled": true,
    "table": "nav_trace"
  },
  "ai": {
    "enabled": false,
    "table": "ai_trace"
  }
}
  • каждый tracer имеет имя (navigation, ai, api и т.д.)
  • у каждого tracera своя таблица (или общая)
  • выключенный tracer ничего не пишет

Использование

const Tracer = require("Common.DX.Tracer");

Tracer.trace("navigation", {
  category: "NAVIGATION",
  component: "Actor",
  action: "hasAchievement",
  level: "OK",
  payload: {
    actor_id: 42,
    achievement: "first_step",
    result: true
  }
});

Если tracer выключен — метод молча завершится.


Методы Tracer

Tracer предоставляет несколько эквивалентных методов:

  • trace(name, data) — базовый метод записи
  • log(name, data) — алиас для читаемости
  • info(name, data) — добавляет level: "INFO"
  • error(name, data) — добавляет level: "ERROR"

Все методы:

  • не выбрасывают ошибок
  • не изменяют переданные данные
  • не влияют на бизнес-логику

Данные события

Tracer принимает любой объект.

Все поля:

  • опциональны
  • именуются произвольно
  • записываются «как есть»

Рекомендуемые (но не обязательные):

  • category — область (NAVIGATION, AI, API)
  • component — компонент
  • action — действие
  • source — источник (system, user, webhook)
  • level — уровень ошибки
  • payload — любые данные (тип поля TEXTAREA)

Если таблица не содержит поле — БД вернёт ошибку. В таком случае используйте payload.


Работа со временем

Tracer:

  • не добавляет timestamp
  • не требует поля времени
  • позволяет передать своё время
{
  event_time: "2026-01-16T12:00:00Z"
}

или

{
  created_at: "2026-01-16T12:00:00Z"
}

Когда использовать

  • метод возвращает true / false, но нужна диагностика
  • не хочется усложнять ответы ошибками
  • важно понять, почему логика не сработала
  • нужна отладка без влияния на сценарии

Когда не использовать

  • как бизнес-лог
  • как аудит-лог
  • как аналитику или метрики

Итог

Common.DX.Tracer — простой и ненавязчивый способ видеть, что происходит внутри системы.

Никакой магии. Никаких обязательств. Никакой боли.

/**
 * Common.DX.Tracer
 *
 * Universal stateless event tracer for Metabot.
 *
 * Version: 1.0.0
 * Author: Art Yg
 *
 * Principles:
 *  - Stateless
 *  - Schema-agnostic
 *  - Zero mandatory fields
 *  - Opt-in only
 *  - Side-effect only
 *
 * Tracer writes exactly what it is given.
 * If disabled — does nothing.
 */

class Tracer {

  /**
   * Internal: get tracer config from bot attributes
   *
   * Expected bot attr: TRACER_CONFIG (JSON)
   *
   * Example:
   * {
   *   "navigation": { "enabled": true, "table": "nav_trace" },
   *   "ai":         { "enabled": false, "table": "ai_trace" }
   * }
   *
   * @returns {Object}
   */
  static _getConfig() {
    try {
      const raw =
        bot.getJsonAttr('TRACER_CONFIG') ||
        bot.getAttr('TRACER_CONFIG');

      if (!raw) return {};

      if (typeof raw === 'object') return raw;

      return JSON.parse(raw);
    } catch (e) {
      // Tracer must never break execution
      return {};
    }
  }

  /**
   * Check whether tracer is enabled
   *
   * @param {string} tracerName
   * @returns {Object|null} tracer config or null
   */
  static _getTracer(tracerName) {
    if (!tracerName) return null;

    const cfg = Tracer._getConfig();
    const tracer = cfg[tracerName];

    if (!tracer) return null;
    if (!tracer.enabled) return null;
    if (!tracer.table) return null;

    return tracer;
  }

  /**
   * Write trace event
   *
   * @param {string} tracerName - logical tracer name (navigation, ai, api, etc)
   * @param {Object} data - arbitrary payload
   *
   * @returns {boolean} true if written, false otherwise
   */
  static trace(tracerName, data = {}) {
    const tracer = Tracer._getTracer(tracerName);
    if (!tracer) return false;

    // We do NOT mutate data
    // We do NOT enforce schema
    // We do NOT inject fields
    try {
      table.createItem(tracer.table, data);
      return true;
    } catch (e) {
      // Silent failure by design
      return false;
    }
  }

  /**
   * Alias for semantic readability
   * (optional, but often nice)
   */
  static log(tracerName, data = {}) {
    return Tracer.trace(tracerName, data);
  }

  /**
   * Explicit error tracer helper
   * (pure sugar, no logic)
   */
  static error(tracerName, data = {}) {
    return Tracer.trace(tracerName, {
      ...data,
      level: data.level || 'ERROR'
    });
  }

  /**
   * Explicit info tracer helper
   */
  static info(tracerName, data = {}) {
    return Tracer.trace(tracerName, {
      ...data,
      level: data.level || 'INFO'
    });
  }
}

module.exports = Tracer;