Привет, Хабр! Меня зовут Настя, я - влюблённый в профессию тестировщик.
В этой статье хочу рассказать о том, как я придумала и с помощью AI разработала браузерное расширение, которое записывает шаги воспроизведения, ошибки и сетевые запросы, а потом собирает промпт для LLM.
В статье приведу немного инженерной части: архитектура, приватность и грабли.
Для начала рассмотрим повседневные задачи, знакомые каждому тестировщику:
Описание баг-репорта. Желательно указать не только шаги, но и добавить скриншот, логи и т.д. В итоге это может превратиться в «развлечение» минут на 30.
Проверка списков опций, ссылок, фильтров… На n-ом элементе можно потеряться в том, что проверено, а что ещё нет.
Не открыты DevTools. Вспоминаешь об этом обычно, когда нужны какие-нибудь данные (например, об упавшем запросе). В итоге
страдаешьснова воспроизводишь.Нет времени возникновения проблемы. Сразу на время я не обращаю внимания и только спустя время начинаю искать лог-файл с нужным стектрейсом.
Отчёт о сессии исследовательского тестирования. Обычно на это никогда нет времени.
Всё это воспринималось мной как данность и неотъемлемая часть работы QA...
От вдохновения до идеи
Отвлечёмся от рутины и поговорим о вдохновении. Уверена, каждый из нас вдохновляется разным. Кто-то путешествиями, кто-то музыкой, кто-то прогулками... На разработку браузерного расширения меня вдохновили:
Михаил Новотарский и Евгений Гребенщиков, «Собеседование QA под нейросетью: когда ИИ говорит „Да“» (Heisenbug 2025 Autumn).
Алексей Иванов, «Браузерные инструменты: пишем своё расширение» (Heisenbug 2025 Autumn).
Вадим Никитенко, «Генерация автотестов с помощью MCP + LLM» (SQA Days 37).
Евгений Гребенщиков, «Генерация тест-кейсов на основе требований с помощью AI: от идеи до мультиагентной системы» (SQA Days 37).
Эти доклады (и не только) подтолкнули к размышлениям о тленности моего бытия, так сказать. Насколько я отстала от жизни, что до сих пор описываю баг-репорты вручную, в то время как кто-то уже и тесты генерит, и тест-кейсы. Унывать я долго не стала, и в итоге меня осенило:
Браузер видит всё происходящее: каждый клик, каждый ввод, каждый запрос, каждую ошибку в консоли и т.д. Если это где-то сохранять, у меня будет полный контекст проблемы, а превратить контекст в баг-репорт может LLM.
Так появилась идея: я тестирую, расширение собирает, LLM описывает. Разработку я начала в ноябре 2025-го, весной рассказала о ней на Heisenbug 2026 Spring, уже вышла версия 1.2.0.
Что получилось
QA Trace - AI Bug Reports, Private & Local - расширение для браузеров Chrome, Firefox и Яндекс. Расширение работает только на настроенных адресах и делает следующее:
Записывает действия: click, dblclick, input, change, открытие и перезагрузка вкладки - с селектором элемента, ближайшей меткой (label'ом), введённым значением (пароли маскируются) и временем.
Ловит ошибки: консольные (
window.onerrorиunhandledrejection), сетевые (упавшиеfetch/XHRзапросы со статусом, заголовками и телом) и UI-ошибки по CSS-селектору (можно настроить несколько селекторов, по умолчаниюdiv[id^="__error"]Опционально сохраняет полный лог fetch/XHR запросов на URL.
По кнопке собирает промпт в одном из трёх режимов: «Шаги для тикета», «Задокументировать шаги» и «Полный отчёт действий».
Промпт можно скопировать и вставить в чат с любой LLM, отправить в LLM (OpenAI, DeepSeek или кастомную) из расширения либо кинуть webhook'ом в свою систему (например, n8n). Промпт генерируется на русском или английском языке в зависимости от настроек.
Как это выглядит в работе
-
В настройках добавляю адреса тестовых стендов в «Адреса для мониторинга».
Если хочу записывать все запросы, включаю «Записывать все fetch/XHR запросы для этого URL».
Если хочу записывать действия на стороннем сервисе, с которым проверяю интеграцию, включаю для этого адреса «Не отслеживать ошибки для этого URL».
Тестирую как обычно. В это время расширение отслеживает и записывает действия, ошибки, запросы. Если что-то пошло не так (например, запрос вернул неуспешный статус), расширение показывает всплывающее уведомление на странице (DevTools открывать не обязательно).
Нашла проблему - открываю окно расширения, нажимаю «Сгенерировать промт». Выбираю режим, вкладки для включения в контекст (по умолчанию выбирается текущая вкладка, если нужно учесть несколько вкладок, выбираю несколько) и действие, с которого начать. Исключаю ожидаемые ошибки (например, ошибка логина с неверными кредами) из промпта. Если технических ошибок нет, а поведение неправильное, описываю проблему вручную.
Смотрю промпт, при желании правлю (обычно так не делаю), копирую - или отправляю в LLM и получаю краткое описание и описание для тикета.
Почему QA Trace
Конечно, есть и другие хорошие инструменты с похожим принципом, но почти все устроены одинаково: расширение записывает сессию (часто с видео) и загружает её в облако. Нужен аккаунт, данные лежат в облаке.
Мне хотелось инструмент, который можно использовать без регистрации, смс и всего такого. Просто установить и пользоваться. Отсюда главное архитектурное требование: всё локально.
Архитектура
Как работают расширения
У всех браузерных расширений есть несколько основных компонентов:
manifest.jsonфайл - своего рода сердце расширения. Это первое что считывает браузер. В этом файле определены самые разные данные: имя, версия, иконки, скрипты, ...backgroundпроцесс (service-worker.js) работает с хранилищами (например, local storage): сохраняет данные, читает.content-script.jsработает с DOM страницы: видит элементы, добавляет свои и т.д.popup- окно расширения, с которым мы как раз и работаем.Страница с настройками (
options page). Обычно открывается по соответствующей кнопке и/или после установки.
Компоненты обмениваются сообщениями и данными, это и обеспечивает работу расширения.
Расширение QA Trace
Чтобы расширение перехватывало ошибки в консоли и fetch/XHR запросы, ему нужен доступ к контексту основной страницы, а основные компоненты работают в изолированном контексте. Поэтому помимо основных компонентов расширение QA Trace содержит дополнительный скрипт (page-hooks.js), который внедряется в контекст страницы, обрабатывает запросы и консоль и передаёт эту информацию в основные компоненты.
Схематично расширение можно изобразить так:

Приватность
Для меня это было важно, и на данный момент реализовано это так:
Только разрешённые адреса
Расширение требует разрешение на доступ ко всем сайтам. Это то, что может «напрячь», но это нужно только потому, что я не знаю заранее, какие адреса будут тестироваться. И первое, что делает расширение на странице, - сверяется со списком «Адреса для мониторинга» из настроек. Если текущий адрес не найден в списке, обработка заканчивается (вызывается return), ничего не отслеживается и не записывается (ни действия, ни ошибки).
Query и hash не попадают в хранилище
В query-строках и hash могут быть токены, поэтому при сохранении URL режется до origin + path.
На первый взгляд всё просто, но hash-роутинг в SPA (#/orders/42) - это навигация, которую хочется оставить, чтобы понимать, на какой странице была ошибка. Компромисс получился таким:
// Hash-routed SPAs keep navigation state in the fragment, so the route path is preserved to // identify the page. A query inside the route and non-route fragments (e.g. OAuth implicit // `#access_token=…`) are dropped so credentials never reach storage. private static safeHashRoute(hash: string | undefined | null): string { const route = (String(hash ?? '').replace(/^#/, '').split('?')[0]) ?? '' return /^!?\//.test(route) ? `#${route}` : '' }
Origin режется на выходе, а не при сохранении
Следующий этап: убрать протокол и хост из всего, что уходит в промпт, в LLM, в webhook. Это делается при выгрузке, а не при сохранении. Локально я хочу видеть полные URL - мне же нужно понимать, что и где упало, а модели не нужно знать какой стенд использовался.
Здесь тоже есть нюансы: хост живёт не только в поле url. Он в тексте ошибки, в стеке, в заголовках, в теле ответа. Поэтому для свободного текста есть отдельная функция, и там регулярки чуть хитрее, чем new URL():
// Matches a URL authority embedded in free text and stops before the path, so the path is kept. const EMBEDDED_ORIGIN = /(?:\b[a-z][a-z0-9+.\-]*:\/\/|(?<=^|[\s"'(<])\/\/)[^\s/"'()<>]+/gi // A scheme-less `host:port` (e.g. `db.internal.corp:5432`, `10.0.0.5:6379`) still exposes the host. // To avoid eating stack frames like `app.js:128`, only IPs or multi-label hostnames whose // final label is not a source-file extension match. const EMBEDDED_HOST_PORT = /(?<=^|[\s"'(<])(?:\d{1,3}(?:\.\d{1,3}){3}|(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?!(?:js|jsx|ts|tsx|mjs|cjs|json|map|css|scss|less|html?|vue|svelte|py|rb|go|rs|php|txt|md|ya?ml|xml|png|jpe?g|gif|svg|wasm|woff2?)\b)[a-z][a-z0-9-]*):\d{1,5}/gi
Вторая регулярка - про host:port без схемы. db.internal.corp:5432 в тексте ошибки - утечка хоста, а app.js:128 в стеке - нет. Разделить их удалось по признаку «последний сегмент не похож на расширение файла».
Полный лог запросов - только по явному включению
Запись всех fetch/XHR запросов - это много данных и много чувствительного. По умолчанию пишутся только ошибки, а полный лог включается отдельно на конкретный адрес.
Чувствительные заголовки и поля тел
Authorization, Cookie, X-API-Key и подобные не записываются. В телах запросов и ответов поля вроде password, token, api_key заменяются на [REDACTED].
Ключи шифруются, пароль не хранится
Если включить интеграцию с LLM или webhook, ключ API или пароль лежит в Extension storage зашифрованным, ключ шифрования выводится из кодовой фразы. Сама фраза не сохраняется - её расширение запросит при использовании. Решила сделать так, чтобы не сохранять чувствительные данные.
Лимиты и срок жизни данных
Всё собранное автоматически удаляется через 12 часов. Также есть лимиты на количество действий (по умолчанию 1000), ошибок (по умолчанию 50) и запросов (по умолчанию 150) и на длину текстовых полей (по умолчанию 500).
Так же есть внутренний лимит на количество ошибок в минуту (не больше 120 на вкладку), иначе один setInterval с падающим запросом забьёт хранилище за секунды.
Грабли
window.onerror вместо патчинга console
Очень хотелось перехватывать console.error и ловить всё, и сначала так и было. Но в таком случае терялся контекст ошибки, и вместо стектрейса с указанием файлов и методов, получалось, что ошибку кидает расширение. Из-за этого контекст, необходимый для локализации проблемы, полностью терялся. Поэтому сейчас отслеживаются только необработанные ошибки window.onerror и unhandledrejection.
Storage ограничен
browser.storage.local в Chrome - это примерно 10 МБ на расширение. И в эти 10 МБ должно уместиться всё: действия, ошибки, лог запросов и скриншоты. Пока отслеживались только ошибки, места хватало. Когда добавился полный лог fetch/XHR запросов, хранилище могло довольно быстро переполниться, и это могло привести к потере свежих данных.
Поэтому с версии 1.1.1 внесла изменения:
Скриншоты - JPEG с качеством 60 вместо PNG. Чтобы увидеть красный баннер на странице, этого достаточно.
Тела запросов и ответов считаются в байтах, а не в символах, потому что квота хранилища - в байтах.
По умолчаниюbodyчитается не больше 100 000 символов и сохраняется с обрезкой до 5 000. Есть настройка «Сохранять body сетевых запросов полностью», но и у неё есть потолок - 4 МБ: тело больше заменяется маркером с размером (например,QA Trace: response body too large to store - 12.4 MB), а сама запись о запросе остаётся. Бинарныеcontent-typeне читаются.Переполнение - не исключение, а штатная ситуация. Каждая запись идёт через
persist(): еслиsetотклонён из-за квоты, освобождаем место и повторяем, пока не влезет. Освобождаем по порядку: самый старый скриншот → половина лога запросов → половина тел сетевых ошибок → половина ошибок → половина действий. Остаётся всегда новая половина.Лимит на запросы. К лимиту на ошибки (120 в минуту на вкладку) добавился такой же для полного лога - 600 запросов в минуту на вкладку.
Мелкая, но неприятная деталь: отличить квоту от любой другой ошибки записи нетривиально. Часть браузеров отклоняет промис DOMException с именем, но пустым message, поэтому проверяются оба поля. Любая другая ошибка записи показывается в всплывающем окне: раньше она уходила в console.warn в service-worker.js, который никто не открывает.
Firefox и 24 ГБ памяти
После релиза 1.1.1 с полным логом fetch/XHR несколько раз переставал отвечать браузер Firefox. Начала отслеживать используемые ресурсы и увидела, что Firefox использует почти 24 ГБ. Главный процесс, не процесс расширения. В хранилище расширения при этом лежало 2,5 МБ. Chrome примерно в такой же ситуации - ничего.
Понятно, что причина в записи всех fetch/XHR запросов моим расширением: весь лог запросов хранился одним значением, и каждая батч-запись переписывала массив целиком - 2,4 МБ старого плюс 2,4 МБ нового. Двести записей в минуту - это примерно гигабайт в минуту. В добавок к этому ещё и content-script.js подписывался на storage.onChanged на каждой вкладке, и главный процесс раздавал каждое уведомление всем открытым вкладкам.
Поэтому внесла правки и опубликовала 1.2.0:
Лог запросов - по одной записи на ключ.
networkRequestIndexхранит{id, timestamp, bytes}, сам запрос лежит подnetworkRequest.<id>и пишется один раз. Запись стоит столько, сколько весит запрос, а не весь лог. Ключи, больше не связанные с запросами, каждый час подчищает таймер.Никаких
storage.onChanged. Страница настроек сообщает о сохранении сообщением,service-worker.jsрассылает его открытым вкладкам, те сбрасывают закэшированную конфигурацию.Свой лимит 9,5 МБ на все записанные данные - чуть ниже квоты Chrome (в Firefox, кстати, квоты нет совсем). Та же лестница сброса, что и при переполнении хранилища, только срабатывает до записи.
Вывод, который стоило знать заранее: в Firefox цена storage.local.set пропорциональна байтам в этом вызове - старое плюс новое, - а не тому, что изменилось.
background процесс засыпает
background процессservice-worker.js останавливается после ~30 секунд простоя и поднимает по событию. Две записи в storage.local, пришедшие подряд из разных вкладок, легко превращаются в гонку read-modify-write. Решение - очередь записи на промисах: каждая операция ждёт предыдущую. В 1.1.1 сверху добавила батчинг: события копятся 300 мс и пишутся одной операцией - N добавлений схлопываются в одну запись.
Про AI
Расширение не «понимает» баг. Оно собирает контекст (действия с временем и селекторами, ошибки со стеками и деталями запроса - метод, URL, статус) и кладёт его в промпт вместе с инструкцией и примером тикета. Дальше работает модель, которую выбрали вы. Можно вообще не подключать API: скопировать промпт и вставить в любой чат.
Сам промпт устроен стандартно:
Системная часть объясняет модели формат данных и просит собрать из них тикет с кратким заголовком и описанием (
summaryиdescription).Пользовательская часть - записанные данные, отфильтрованные по вашему выбору и пример тикета как образец формата.
Если настроите интеграцию с LLM в расширении, то ответ будет парситься в { summary, description } и показываться в окне расширения. Если модель вернула что-то не по схеме, будет ошибка.
Инсайты
Качество результата определяют два поля в настройках - пример тикета и пример для документации. Они задают формат, к которому модель подстраивается.
Режим «Задокументировать шаги» может быть полезен для описания регрессионных сценариев и для коллег из поддержки.
Ограничения
Пока нет понятия «сессии»: данные - один общий поток, который живёт 12 часов. Начать, остановить и сохранить отдельную сессию нельзя.
Селекторы простые:
#id,tag.classили простоtag. Для шагов воспроизведения этого хватает, для генерации автотестов - нет. В планахdata-testid,aria-атрибуты и уникальные fallback-селекторы.Не записываются клавиатурные события, submit, drag-and-drop, paste - только click, dblclick, input, change и события вкладок. Часть багов воспроизводится именно на них.
Нет видео. Это осознанно: видео - тяжёлый артефакт, и именно его облачные инструменты делают хорошо. QA Trace про структурированный контекст, а не про запись экрана.
Что дальше
Именованные сессии: start/stop, несколько сессий.
Экспорт и импорт сессии в JSON.
Ошибки, спрятанные в теле успешного ответа: некоторые бэкенды отвечают
200 OK, а внутри JSON -{"status": 500}. Сейчас расширение смотрит только на HTTP-статус, и это следующий кандидат в детектор.Генерация Playwright-тестов из записанных действий. Этот пункт пока на уровне идеи, может быть, в итоге будут и не тесты вовсе, посмотрим.
Исходники
Chrome / Яндекс Браузер: https://chromewebstore.google.com/detail/qa-trace-ai-bug-reports-p/lkekhnhaifeadeijdpaookeomhcoclhh
Firefox: https://addons.mozilla.org/en-US/firefox/addon/qa-trace/
Бесплатно, без аккаунта. Если попробуете, дайте знать, чего не хватает. Обязательно поставьте звёздочки инструменту в каталоге, это поможет другим найти расширение.
Еженедельные вопросы по QA, разбор теории, практика, рабочие кейсы, истории из жизни и про развитие QA Trace — в телеграм-канале QA or not QA?.