Выгрузки

Заявка на файл со списком ошибок или сырых событий проекта — CSV, JSON или NDJSON, собирается в фоне и ждёт скачивания на странице /projects/{id}/exports.

Что выгружается

Два вида заявки:

  • Группы ошибок — та же таблица, что список Проблем: заголовок, culprit (место в коде), уровень, статус, число попаданий, первое/последнее появление, окружения, ответственный, ссылка на группу.
  • События — сырые события выбранной группы либо всего проекта: время, идентификатор события и группы, уровень, сообщение, тип и значение исключения, окружение, релиз, имя сервера, SDK, trace_id, идентификатор/IP/email пользователя, теги. В CSV — только эти колонки (полный объект превратил бы таблицу в нечитаемое полотно); в JSON/NDJSON — событие целиком, включая stacktrace, contexts, breadcrumbs и request.

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

Порядок строк — часть контракта файла, не случайность: группы отсортированы по последнему появлению, от новых к старым, а группы с ОДИНАКОВЫМ временем последнего появления — по убыванию id (самая недавно созданная из них первой). На него можно полагаться при обработке файла программой — правило зафиксировано и не меняется молча.

Форматы

ФорматЧто этоКогда использовать
CSVТабличный файл, запятая-разделитель, кодировка UTF-8 с BOMОткрыть в Excel/таблицах, быстро просмотреть
JSONОдин массив объектовИмпорт в другую систему, программная обработка
NDJSONПо одному JSON-объекту на строкуПотоковая обработка большого файла без загрузки целиком в память

Если Excel не разбил CSV-файл на колонки. Файл — валидный UTF-8 CSV с запятой в качестве разделителя; на локали, где Excel ждёт точку с запятой (например, русская Windows), двойной клик по файлу может свалить всё в один столбец. Откройте файл не двойным щелчком, а через Данные → Из текста/CSV (или Файл → Импорт) и явно укажите запятую как разделитель — так колонки разложатся правильно независимо от локали Excel.

Формат выбирается явно на каждой из четырёх точек постановки заявки: на самой странице «Выгрузки», и на компактных раскрывающихся формах — «Экспорт групп»/«Экспорт событий» на странице Проблемы и «Экспорт событий группы» на странице отдельной ошибки. У всех четырёх — один и тот же выпадающий список CSV/JSON/NDJSON и (если доступна) галка «Выгружать PII без маскирования», описанная ниже.

Кто может выгружать

Создавать заявки, скачивать и удалять свои — любой оператор проекта (тот же уровень доступа, что у настроек алертов и подавления). Скачивание перепроверяет доступ к проекту в момент скачивания, а не только на постановке: доступ мог быть отозван позже — чужая или недоступная заявка отвечает 404, а не 403.

В списке на странице «Выгрузки» админ или владелец организации видит ВСЕ заявки проекта — свои и чужие; оператор без этой роли видит только свои собственные. Список — не больше 50 последних заявок и без пагинации: более старые заявки (или чужие — у обычного оператора) в нём просто не появляются.

Удалить чужую заявку может только админ или владелец организации; удаление доступно только для завершённых заявок (готова, ошибка, истёк срок) — у ещё выполняющейся заявки кнопки удаления нет, вместо неё пояснение, что заявка ещё в работе. Само удаление — двухшаговое: кнопка ведёт на страницу подтверждения, необратимое действие происходит только после явного подтверждения на ней.

PII: маскирование по умолчанию

user_email и user_ip по умолчанию заменяются на [masked]; user_id — на псевдоним (случайная строка, стабильная только внутри одного файла, см. ниже «Псевдонимы user_id не сопоставимы между выгрузками»), а не остаётся как есть и не заменяется той же статичной маской, что email/IP: псевдоним, в отличие от [masked], позволяет посчитать, сколько РАЗНЫХ пользователей затронуто, не раскрывая ни одного исходного значения. В JSON/NDJSON та же маска [masked]/скраб применяется к ключам request, contexts, stacktrace и breadcrumbs, похожим на персональные данные (локальные переменные фрейма, URL с query-параметрами).

Галка «Выгружать PII без маскирования» на форме постановки доступна только админу или владельцу организации — оператору проекта без этой роли она не показывается, а переданное значение молча игнорируется (заявка всё равно ставится с маской, а не отказом). При включённой галке user_id тоже уезжает как есть, без псевдонимизации. Факт применения галки виден в списке заявок каждому, кому видна сама заявка.

Если на приёме включено серверное маскирование (GOTCHA_SCRUB_EMAIL/ GOTCHA_SCRUB_IP, см. Приватность), user_email/user_ip уже занулены в хранилище на момент приёма события. Галка «Выгружать PII без маскирования» не воскрешает то, что не было сохранено — если адреса и так пустые, снятие маски на выгрузке ничего не изменит.

В выгрузке групп (kind=issues) той же галкой и той же маской [masked] управляется колонка assignee_email — email пользователя, назначенного на группу: это прямой идентификатор пользователя, а не деталь самой группы. Пустая колонка (группа без назначенного) маской не заменяется — как и user_email/user_ip выше.

Псевдонимы user_id не сопоставимы между выгрузками

Псевдоним user_id строится по случайному одноразовому ключу, который генерируется заново на КАЖДУЮ заявку и нигде не сохраняется. Следствие: внутри ОДНОГО файла одинаковый user_id всегда даёт одинаковый псевдоним (можно посчитать уникальных пользователей события/группы), но у ДВУХ разных файлов — даже поставленных с одинаковым фильтром одну за другой — один и тот же живой пользователь получит РАЗНЫЕ псевдонимы. Склеить две выгрузки по user_id нельзя, и это осознанное поведение, а не дефект: обратное дало бы корреляцию активности пользователя между файлами, которую никто не заказывал.

Пометка об этом — не только здесь. Файлы выгрузки (CSV/JSON/NDJSON) остаются ЧИСТЫМИ: CSV — только строка колонок и строки данных без единого комментария, JSON — только массив записей, NDJSON — только однородные строки; ни один из трёх форматов не несёт служебного элемента, который пришлось бы отличать от данных. Машиночитаемые сведения о заявке — scope_issue_id, filter_code и, для events без снятой маски PII, pseudonym_note — лежат РЯДОМ с файлом, тремя путями:

  • Соседний ответ того же скачивания: GET .../exports/{jobID}/download?meta=1 отдаёт эти поля в JSON под теми же гейтами доступа, что и сам файл, — тому, кому нужен только идентификатор, не приходится парсить переведённую фразу вида «issue #123» из человекочитаемой сводки;
  • На странице «Выгрузки»: у ячейки колонки «Фильтры» есть атрибуты data-scope-issue-id/data-filter-code/data-pseudonym-masked — те же значения рядом с локализованным текстом, читаемые скриптом без разбора текста;
  • В письме о готовности: нелокализуемая строка gotcha-export-meta: job_id=… scope_issue_id=… filter_code=… и (когда применимо) сама пометка о псевдонимах добавляются к телу письма отдельно от переведённой фразы выше.

Лимиты

  • 3 активных заявки (в очереди или уже собирается) на пользователя, 10 на проект одновременно — постановка новой при превышении отклоняется. Предел на пользователя общий по ВСЕМ его проектам, а не по каждому в отдельности.
  • Потолок строк (GOTCHA_EXPORT_MAX_ROWS, по умолчанию 200 000) и потолок размера файла (GOTCHA_EXPORT_MAX_BYTES, по умолчанию 256 МиБ): при достижении любого из них сборка останавливается, а заявка помечается «обрезана» — на странице это видно явной пометкой рядом со статусом, файл при этом всё равно доступен для скачивания.
  • Общий бюджет каталога выгрузок (GOTCHA_EXPORT_DISK_BUDGET_BYTES, по умолчанию 5 ГиБ на инстанс, без пер-проектной квоты) и реальное свободное место на файловой системе, где лежит GOTCHA_EXPORT_DIR — оба резервируют место под MaxBytes ТЕКУЩЕЙ заявки, а не только под уже собранные файлы. Отказ по любой из двух причин — ВРЕМЕННЫЙ (заявка возвращается в очередь, до 3 попыток): нехватка места самоустраняется первым же проходом джанитора, который освобождает диск от истёкших файлов. От пользователя действий не требуется; если заявка всё же добита до статуса «ошибка» — за три попытки место так и не освободилось, нужно вручную освободить диск или поднять GOTCHA_EXPORT_DISK_BUDGET_BYTES (см. Конфигурацию) и поставить заявку заново, сама она не перезапускается.
  • Выгрузка событий без выбранной конкретной группы (kind=events, весь проект с фильтром) — если фильтр резолвится в более чем 20 000 групп ошибок, отказ окончательный (в отличие от нехватки места, повторных попыток не будет): нужно сузить период, окружение или поиск и поставить заявку заново. Потолок — константа продукта, переменной окружения не вынесен. Выгрузку групп ошибок и выгрузку событий ОДНОЙ уже известной группы (со страницы этой ошибки) этот отказ не касается.

Хранение и удаление

Готовый файл живёт GOTCHA_EXPORT_RETENTION_HOURS часов от момента завершения (по умолчанию 168 — семь суток): по истечении джанитор удаляет файл с диска, а заявку помечает статусом «истекла». Строка заявки в истории переживает файл — она хранится не меньше 30 суток от завершения, а если GOTCHA_EXPORT_RETENTION_HOURS задан больше 30 суток, то ровно этот срок (срок хранения строки растёт вместе с TTL файла, но не бывает короче него), и только затем джанитор убирает её из истории без следа. Автору приходит письмо ровно один раз на заявку — когда она перешла в «готова» или «ошибка» (если настроена почта, см. Конфигурацию); отдельного письма-напоминания об истечении файла нет. Причину отказа не обязательно искать в письме — у заявки со статусом «ошибка» она показана прямо в её строке в списке на странице «Выгрузки». Статус на странице «Выгрузки» сам не обновляется, пока заявка ещё в очереди или собирается — узнать о готовности можно письмом или перезагрузкой страницы; скачать файл стоит до истечения его собственного срока, повторно он уже не появится.

Файлы выгрузок и сам каталог GOTCHA_EXPORT_DIR — единственное место продукта, где персональные данные (email/IP пользователя, contexts, request) ложатся на диск: права на них сужены до владельца процесса (файл 0600, каталог 0700). Это касается только НОВОГО каталога — MkdirAll не меняет права уже существующего, поэтому на инстансе, поднятом до этой правки, каталог остаётся с прежними правами до ручного chmod 0700.

Одноинстансное ограничение

Файлы выгрузок лежат на диске того процесса, который их собрал (GOTCHA_EXPORT_DIR). Раздел рассчитан на одноинстансный деплой: при нескольких репликах приложения за балансировщиком скачивание может попасть на реплику, где файла нет физически. Горизонтальное масштабирование этой фичи потребует общего хранилища (сетевой том, S3-совместимое хранилище и т.п.) — сейчас это не реализовано.

Перезапуск процесса

Заявка, которую воркер как раз собирал в момент штатной остановки процесса (рестарт, деплой, docker compose up -d), не остаётся зависшей в статусе «выполняется» и не считается отказавшей: она возвращается в очередь без траты одной из трёх попыток и полностью пересобирается заново на следующем старте — файл, недописанный до остановки, отбрасывается. Письмо об ошибке в этом случае не приходит: с точки зрения заявки ничего не сломалось, сборка просто продолжится минутой позже. Заявок, ещё стоявших в очереди на момент остановки, эта пауза не касается вовсе — они и так ждут своей очереди.

Воркер и джанитор выгрузок поднимаются только в процессах с режимом web или all — там же, где обрабатывается скачивание. При раздельном развёртывании --mode=ingest/--mode=web (см. Обновление) это устраняет самый опасный сценарий (заявку собирает реплика без единого маршрута выгрузок), но не снимает одноинстансное ограничение целиком: если web-процесс запущен в НЕСКОЛЬКИХ репликах за балансировщиком, джанитор каждой из них по-прежнему видит только свой локальный диск. Если истёкшую заявку собрала соседняя web-реплика, джанитор текущей реплики не находит файл (ENOENT) и всё равно помечает заявку «истекла» — реальный файл на диске другой реплики этим не удаляется и остаётся лежать там до чистки истории заявок (не меньше 30 суток от завершения), занимая место в GOTCHA_EXPORT_DISK_BUDGET_BYTES той реплики.