- BrainTools - https://www.braintools.ru -
Привет, Хабр! Меня зовут Настя, я – влюблённый в профессию тестировщик.
В этой статье хочу рассказать о том, как я придумала и с помощью AI разработала браузерное расширение, которое записывает шаги воспроизведения, ошибки [1] и сетевые запросы, а потом собирает промпт для LLM.
В статье приведу немного инженерной части: архитектура, приватность и грабли.
Для начала рассмотрим повседневные задачи, знакомые каждому тестировщику:
Описание баг-репорта. Желательно указать не только шаги, но и добавить скриншот, логи и т.д. В итоге это может превратиться в «развлечение» минут на 30.
Проверка списков опций, ссылок, фильтров… На n-ом элементе можно потеряться в том, что проверено, а что ещё нет.
Не открыты DevTools. Вспоминаешь об этом обычно, когда нужны какие-нибудь данные (например, об упавшем запросе). В итоге страдаешь снова воспроизводишь.
Нет времени возникновения проблемы. Сразу на время я не обращаю внимания [2] и только спустя время начинаю искать лог-файл с нужным стектрейсом.
Отчёт о сессии исследовательского тестирования. Обычно на это никогда нет времени.
Всё это воспринималось мной как данность и неотъемлемая часть работы QA…
Отвлечёмся от рутины и поговорим о вдохновении. Уверена, каждый из нас вдохновляется разным. Кто-то путешествиями, кто-то музыкой, кто-то прогулками… На разработку браузерного расширения меня вдохновили:
Михаил Новотарский и Евгений Гребенщиков, «Собеседование QA под нейросетью: когда ИИ говорит „Да“» [3] (Heisenbug 2025 Autumn).
Алексей Иванов, «Браузерные инструменты: пишем своё расширение» [4] (Heisenbug 2025 Autumn).
Вадим Никитенко, «Генерация автотестов с помощью MCP + LLM» [5] (SQA Days 37).
Евгений Гребенщиков, «Генерация тест-кейсов на основе требований с помощью AI: от идеи до мультиагентной системы» [6] (SQA Days 37).
Эти доклады (и не только) подтолкнули к размышлениям о тленности моего бытия, так сказать. Насколько я отстала от жизни, что до сих пор описываю баг-репорты вручную, в то время как кто-то уже и тесты генерит, и тест-кейсы. Унывать я долго не стала, и в итоге меня осенило:
Браузер видит всё происходящее: каждый клик, каждый ввод, каждый запрос, каждую ошибку в консоли и т.д. Если это где-то сохранять, у меня будет полный контекст проблемы, а превратить контекст в баг-репорт может LLM.
Так появилась идея: я тестирую, расширение собирает, LLM описывает. Разработку я начала в ноябре 2025-го, весной рассказала [7] о ней на 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 открывать не обязательно).
Нашла проблему – открываю окно расширения, нажимаю «Сгенерировать промт». Выбираю режим, вкладки для включения в контекст (по умолчанию выбирается текущая вкладка, если нужно учесть несколько вкладок, выбираю несколько) и действие, с которого начать. Исключаю ожидаемые ошибки (например, ошибка логина с неверными кредами) из промпта. Если технических ошибок нет, а поведение [8] неправильное, описываю проблему вручную.
Смотрю промпт, при желании правлю (обычно так не делаю), копирую – или отправляю в LLM и получаю краткое описание и описание для тикета.
Конечно, есть и другие хорошие инструменты с похожим принципом, но почти все устроены одинаково: расширение записывает сессию (часто с видео) и загружает её в облако. Нужен аккаунт, данные лежат в облаке.
Мне хотелось инструмент, который можно использовать без регистрации, смс и всего такого. Просто установить и пользоваться. Отсюда главное архитектурное требование: всё локально.
У всех браузерных расширений есть несколько основных компонентов:
manifest.json файл – своего рода сердце расширения. Это первое что считывает браузер. В этом файле определены самые разные данные: имя, версия, иконки, скрипты, …
background процесс (service-worker.js) работает с хранилищами (например, local storage): сохраняет данные, читает.
content-script.js работает с DOM страницы: видит элементы, добавляет свои и т.д.
popup – окно расширения, с которым мы как раз и работаем.
Страница с настройками (options page). Обычно открывается по соответствующей кнопке и/или после установки.
Компоненты обмениваются сообщениями и данными, это и обеспечивает работу расширения.
Чтобы расширение перехватывало ошибки в консоли и fetch/XHR запросы, ему нужен доступ к контексту основной страницы, а основные компоненты работают в изолированном контексте. Поэтому помимо основных компонентов расширение QA Trace содержит дополнительный скрипт (page-hooks.js), который внедряется в контекст страницы, обрабатывает запросы и консоль и передаёт эту информацию в основные компоненты.
Схематично расширение можно изобразить так:
Для меня это было важно, и на данный момент реализовано это так:
Расширение требует разрешение на доступ ко всем сайтам. Это то, что может «напрячь», но это нужно только потому, что я не знаю заранее, какие адреса будут тестироваться. И первое, что делает расширение на странице, – сверяется со списком «Адреса для мониторинга» из настроек. Если текущий адрес не найден в списке, обработка заканчивается (вызывается return), ничего не отслеживается и не записывается (ни действия, ни ошибки).
В 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}` : ''
}
Следующий этап: убрать протокол и хост из всего, что уходит в промпт, в 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 с падающим запросом забьёт хранилище за секунды.
Очень хотелось перехватывать console.error и ловить всё, и сначала так и было. Но в таком случае терялся контекст ошибки, и вместо стектрейса с указанием файлов и методов, получалось, что ошибку кидает расширение. Из-за этого контекст, необходимый для локализации проблемы, полностью терялся. Поэтому сейчас отслеживаются только необработанные ошибки window.onerror и unhandledrejection.
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 отклонён из-за квоты, освобождаем место и повторяем [9], пока не влезет. Освобождаем по порядку: самый старый скриншот → половина лога запросов → половина тел сетевых ошибок → половина ошибок → половина действий. Остаётся всегда новая половина.
Лимит на запросы. К лимиту на ошибки (120 в минуту на вкладку) добавился такой же для полного лога – 600 запросов в минуту на вкладку.
Мелкая, но неприятная деталь: отличить квоту от любой другой ошибки записи нетривиально. Часть браузеров отклоняет промис DOMException с именем, но пустым message, поэтому проверяются оба поля. Любая другая ошибка записи показывается в всплывающем окне: раньше она уходила в console.warn в service-worker.js, который никто не открывает.
После релиза 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 процессservice-worker.js останавливается после ~30 секунд простоя и поднимает по событию. Две записи в storage.local, пришедшие подряд из разных вкладок, легко превращаются в гонку read-modify-write. Решение – очередь записи на промисах: каждая операция ждёт предыдущую. В 1.1.1 сверху добавила батчинг: события копятся 300 мс и пишутся одной операцией – N добавлений схлопываются в одну запись.
Расширение не «понимает» баг. Оно собирает контекст (действия с временем и селекторами, ошибки со стеками и деталями запроса – метод, 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 [10]
Firefox: https://addons.mozilla.org/en-US/firefox/addon/qa-trace/ [11]
Код: https://github.com/AnastasyaNN/QA-Trace [12]
Бесплатно, без аккаунта. Если попробуете, дайте знать, чего не хватает. Обязательно поставьте звёздочки инструменту в каталоге, это поможет другим найти расширение.
Еженедельные вопросы по QA, разбор теории, практика, рабочие кейсы, истории из жизни и про развитие QA Trace — в телеграм-канале QA or not QA?.
Присоединяйтесь! [13]
Автор: nesterovaas
Источник [14]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/37031
URLs in this post:
[1] ошибки: http://www.braintools.ru/article/4192
[2] внимания: http://www.braintools.ru/article/7595
[3] «Собеседование QA под нейросетью: когда ИИ говорит „Да“»: https://vkvideo.ru/video-127622637_456240299?pl=-127622637_39
[4] «Браузерные инструменты: пишем своё расширение»: https://vkvideo.ru/video-127622637_456240313?pl=-127622637_39
[5] «Генерация автотестов с помощью MCP + LLM»: https://vkvideo.ru/video-27699095_456239508?pl=-27699095_11
[6] «Генерация тест-кейсов на основе требований с помощью AI: от идеи до мультиагентной системы»: https://vkvideo.ru/video-27699095_456239548?pl=-27699095_11
[7] рассказала: https://heisenbug.ru/archive/2026%20Spring/talks/20009710-bug-reporting-speed-up-with-a-browser-extension/
[8] поведение: http://www.braintools.ru/article/9372
[9] повторяем: http://www.braintools.ru/article/4012
[10] https://chromewebstore.google.com/detail/qa-trace-ai-bug-reports-p/lkekhnhaifeadeijdpaookeomhcoclhh: https://chromewebstore.google.com/detail/qa-trace-ai-bug-reports-p/lkekhnhaifeadeijdpaookeomhcoclhh
[11] https://addons.mozilla.org/en-US/firefox/addon/qa-trace/: https://addons.mozilla.org/en-US/firefox/addon/qa-trace/
[12] https://github.com/AnastasyaNN/QA-Trace: https://github.com/AnastasyaNN/QA-Trace
[13] Присоединяйтесь!: https://t.me/QA_or_not_QA
[14] Источник: https://habr.com/ru/articles/1081394/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1081394
Нажмите здесь для печати.