Що це. Баг-репорт — це документ, за яким інша людина має відтворити проблему, зрозуміти її вплив і вирішити, коли її чинити. Мінімальний набір полів: заголовок, оточення і build, передумови (роль, дані, feature flags), кроки відтворення, очікуваний і фактичний результат, severity, частота відтворення та вкладення (скриншот, відео, HAR, лог). Priority здебільшого ставить не QA, а власник беклогу.
What it is. A bug report is a document that lets another person reproduce the problem, understand its impact and decide when to fix it. The minimum set of fields: title, environment and build, preconditions (role, data, feature flags), steps to reproduce, expected and actual result, severity, reproduction rate and attachments (screenshot, video, HAR, log). Priority is usually set by the backlog owner rather than by QA.
Навіщо. Кожне поле закриває конкретне питання, яке інакше розробник поставить у коментарях: «на якому стенді?», «під яким юзером?», «а що мало статися?». Один такий цикл питання-відповідь коштує пів дня, а баг без повного набору полів часто повертається зі статусом Cannot Reproduce.
Why it matters. Each field answers a question the developer would otherwise ask in the comments: "which environment?", "which user?", "what was supposed to happen?". One such question-and-answer round costs half a day, and a report with missing fields often comes back as Cannot Reproduce.
Приклад з проєкту. На проєкті інтернет-магазину я знайшла, що промокод не застосовується до кошика з уцінованим товаром. Перша версія репорту була «Промокод не працює» — і розробник витратив годину, пробуючи інші коди. Після того як я додала конкретний код SPRING10, SKU товару зі знижкою, роль «зареєстрований покупець» і відповідь API з помилкою, фікс зайняв двадцять хвилин. Повний репорт — у прикладі нижче.
Example from a project. On an online shop project I found that a promo code was not applied to a cart containing a discounted item. My first version was "Promo code doesn't work", and the developer spent an hour trying other codes. Once I added the exact code SPRING10, the SKU of the discounted item, the "registered customer" role and the API error response, the fix took twenty minutes. The full report is in the example below.
Підводні камені. Змішувати кілька проблем в одному тікеті: один баг — один репорт, інакше його неможливо закрити частково. Писати висновки замість фактів («бекенд неправильно рахує знижку») — ти можеш помилятися щодо причини, а факт «сума 1 200 замість 1 080» безперечний. Забувати build: через тиждень ніхто не знатиме, на якій версії це було.
Pitfalls. Mixing several problems in one ticket: one bug, one report, otherwise it cannot be closed partially. Writing conclusions instead of facts ("the backend calculates the discount wrong") — you may be wrong about the cause, while "total 1,200 instead of 1,080" is indisputable. Forgetting the build: a week later nobody will know which version it was.
Уточнююче питання. «Які поля ти вважаєш обов’язковими, а які — опційними?» Обов’язкові: заголовок, кроки, expected/actual, оточення з build і severity. Опційні: workaround, припущення про причину, посилання на схожі тікети — вони корисні, але без них баг усе одно можна відтворити.
Follow-up question. "Which fields do you treat as mandatory and which as optional?" Mandatory: title, steps, expected/actual, environment with build, severity. Optional: workaround, a guess at the root cause, links to similar tickets — useful, but the bug can still be reproduced without them.
Title: [Checkout] Promo code SPRING10 is rejected when cart has a discounted item
Environment: staging-2, Chrome 129 / macOS 14.6, web build 4.12.0 (commit a1b9e3f)
Preconditions: logged in as registered customer qa.buyer+07@example.com
cart contains SKU 88412 (sale price 900) + SKU 10233 (300)
Steps to reproduce:
1. Open /cart
2. Enter "SPRING10" in the Promo code field
3. Click Apply
Expected: 10% off the whole cart, total 1,080 (per promo rules in PROMO-45)
Actual: toast "Promo code is not valid", total stays 1,200;
POST /api/cart/promo returns 422 {"code":"PROMO_NOT_APPLICABLE"}
Severity: Major (promo campaign fails for any cart with a sale item)
Reproduction rate: 5/5
Workaround: remove the sale item, apply the code, add the item back
Attachments: promo-422.mp4, cart-promo.har (cookies stripped)
Title з модулем у дужках, Environment з build і commit, Preconditions з конкретним юзером і SKU, нумеровані кроки, Expected з посиланням на правило, Actual з відповіддю API, частота у форматі «скільки з скількох».Title with the module in brackets, Environment with build and commit, Preconditions with an exact user and SKUs, numbered steps, Expected pointing to the rule, Actual with the API response, and the rate as "how many out of how many".Що це. Заголовок — це стисле речення, яке відповідає на три питання: де (модуль або екран), що сталося (симптом) і за якої умови. Робоча формула: [Модуль] Симптом + умова. Наприклад: «[Checkout] Promo code rejected when cart has a sale item». У заголовку немає слів «не працює», «баг», «помилка» без уточнення.
What it is. A title is one short sentence that answers three questions: where (module or screen), what happened (the symptom) and under which condition. A working formula: [Module] Symptom + condition. For example: "[Checkout] Promo code rejected when cart has a sale item". A title has no bare "doesn't work", "bug" or "error" without specifics.
Навіщо. Заголовок бачать у списку на тріажі, у звіті релізу, у пошуку дублів і в повідомленнях Slack. Більшість людей ніколи не відкриє тікет — вони вирішують за заголовком, чи це їхня зона. Хороший заголовок також рятує від дублів: наступний тестувальник знайде тікет за ключовими словами.
Why it matters. The title is what people see in the triage list, the release report, duplicate searches and Slack notifications. Most people never open the ticket; they decide from the title whether it is their area. A good title also prevents duplicates: the next tester will find the ticket by its keywords.
Приклад з проєкту. У мобільному банку в беклозі було три тікети «Crash on transfer». Коли я їх розібрала, виявилося, що це три різні баги: падіння при переказі на картку іншого банку, при сумі з копійками і при повороті екрана під час підтвердження. Після перейменування за формулою «модуль + симптом + умова» тріаж призначив їх трьом різним командам за п’ять хвилин.
Example from a project. In a mobile banking app the backlog had three tickets called "Crash on transfer". When I went through them, they turned out to be three different bugs: a crash on transfer to another bank's card, with an amount that has cents, and on rotating the screen during confirmation. Once I renamed them with the "module + symptom + condition" formula, triage routed them to three teams in five minutes.
Підводні камені. Надто довгий заголовок з усіма кроками — він обрізається в списку. Емоції і капслок («ТЕРМІНОВО!!!») — терміновість передає priority, а не регістр. Опис причини замість симптому («Null pointer in PromoService») — розробник може бути правий, але продукт і підтримка такий заголовок не зрозуміють і не знайдуть.
Pitfalls. A title so long it contains all the steps — it gets cut off in the list. Emotions and caps ("URGENT!!!") — urgency is carried by priority, not by capital letters. Describing the cause instead of the symptom ("Null pointer in PromoService") — the developer may agree, but product and support will neither understand nor find such a title.
Уточнююче питання. «Англійською чи українською?» Мовою, якою команда шукає в трекері. У міжнародних командах це англійська, і тоді важливо вживати ті самі назви екранів і кнопок, що і в інтерфейсі, щоб пошук спрацьовував.
Follow-up question. "English or your native language?" Whatever language the team searches the tracker in. In international teams that is English, and then it matters to use the same screen and button names as the UI, so the search actually matches.
# bad -> good
Checkout doesn't work -> [Checkout] Pay button stays disabled after card is added via Apple Pay
Crash -> [iOS][Transfers] App crashes on rotation during OTP confirmation
Wrong price!!! -> [Cart] Total ignores 10% promo when cart has a sale item
Null pointer in PromoService -> [Cart] Promo SPRING10 returns 422 for mixed carts
Search is slow -> [Search] Results take 8-12 s for queries with 3+ filters (prod EU)
Error on profile page -> [Profile] 500 on saving phone number with +44 prefix
[Модуль] симптом + умова: у квадратних дужках платформа чи модуль, далі те, що бачить користувач, і умова, за якої це стається.[Module] symptom + condition formula: platform or module in square brackets, then what the user sees, then the condition under which it happens.Що це. Expected — як система має поводитися згідно з джерелом істини: вимогою, acceptance criteria, макетом, контрактом API або поведінкою попередньої версії. Actual — що вона зробила насправді, в конкретних значеннях: текст помилки, статус-код, сума, стан кнопки. Баг — це саме різниця між цими двома рядками.
What it is. Expected is how the system should behave according to a source of truth: a requirement, acceptance criteria, a mockup, an API contract or the behaviour of the previous version. Actual is what it really did, in concrete values: the error text, the status code, the amount, the button state. The bug is exactly the difference between those two lines.
Навіщо. Коли expected і actual злиті в одне речення, неможливо зрозуміти, де закінчується факт і починається думка тестувальника. Окремий expected з посиланням дозволяє за секунду перевірити, чи це справді баг, чи QA неправильно зрозуміла вимогу. Якщо джерела немає, це сигнал, що проблема у вимозі, а не в коді.
Why it matters. When expected and actual are merged into one sentence, nobody can tell where the fact ends and the tester's opinion begins. A separate expected with a reference lets anyone check in a second whether this is really a bug or QA misread the requirement. If there is no source at all, that is a sign the problem is in the requirement, not in the code.
Приклад з проєкту. У застосунку доставки я репортила, що після скасування замовлення кошти повертаються за 5–7 днів. Expected я записала з посиланням на AC в історії DLV-310: «refund is issued instantly to the original payment method». Розробник спершу написав «так і має бути, банк повертає довго», але посилання на AC перенесло розмову до продакт-менеджера, і з’ясувалося, що миттєвий рефанд обіцяли в маркетинговій кампанії. Баг отримав priority High.
Example from a project. In a delivery app I reported that after cancelling an order the money came back in 5–7 days. I wrote expected with a link to the AC in story DLV-310: "refund is issued instantly to the original payment method". The developer first replied "works as intended, banks are slow", but the AC link moved the discussion to the product manager, and it turned out instant refunds had been promised in a marketing campaign. The bug got High priority.
Підводні камені. Писати в actual «не працює» замість конкретики. Вигадувати expected з голови, коли вимоги немає, — тоді чесніше написати «Expected: не визначено у вимогах; за аналогією з екраном X очікую Y» і позначити тікет для уточнення. Вписувати в actual кілька різних симптомів — якщо їх два, можливо, це два баги.
Pitfalls. Writing "doesn't work" in actual instead of specifics. Inventing expected out of thin air when there is no requirement — it is more honest to write "Expected: not defined in the requirements; by analogy with screen X I expect Y" and flag the ticket for clarification. Listing several symptoms in actual — if there are two, it may be two bugs.
Уточнююче питання. «Що робиш, якщо вимоги немає зовсім?» Беру поведінку попередньої версії, конкурентів або загальноприйняті конвенції (наприклад, WCAG для доступності), прямо вказую джерело і ставлю питання продукту в тікеті, а не в особистих повідомленнях.
Follow-up question. "What do you do if there is no requirement at all?" I use the previous version's behaviour, competitors or common conventions (WCAG for accessibility, for instance), name the source explicitly and ask product in the ticket rather than in a private message.
Expected: Refund of 459.00 UAH is issued instantly to the original card
Source: DLV-310, AC #3 ("refund is issued instantly ...")
Actual: Order status "Cancelled", refund status "Pending";
GET /api/orders/88120/refund -> {"status":"PENDING","eta_days":7}
Card balance unchanged after 30 min
# when there is no written requirement:
Expected: Not specified in DLV-310. By analogy with the web app (v3.8),
I expect an instant refund. @product please confirm.
Expected завжди має рядок Source з номером історії й пункту AC. Actual містить дані, які можна перевірити: статуси, відповідь API, баланс. Нижній блок показує, як чесно оформити expected, коли вимоги немає.Expected always has a Source line with the story and AC number. Actual holds verifiable data: statuses, the API response, the balance. The lower block shows how to write expected honestly when no requirement exists.Що це. Кроки відтворення — це пронумерований список дій від стартового стану до моменту, коли видно баг. Кожен крок — одна дія користувача з конкретними даними: що відкрити, куди натиснути, що ввести. Стартовий стан (юзер, дані, прапорці) виносять у передумови, щоб не роздувати кроки. Останній крок — той, після якого видно actual.
What it is. Steps to reproduce are a numbered list of actions from a starting state to the moment the bug is visible. Each step is one user action with concrete data: what to open, what to click, what to type. The starting state (user, data, flags) goes into preconditions so the steps stay short. The last step is the one after which the actual result shows.
Навіщо. Розробник і тестувальник, що перевірятиме фікс, мають пройти той самий шлях. Пропущений крок («а, я ще змінила мову на англійську») — найчастіша причина Cannot Reproduce. Нумерація дозволяє в коментарях посилатися на конкретне місце: «на кроці 4 у мене інша поведінка».
Why it matters. The developer and whoever verifies the fix must walk the same path. A skipped step ("oh, I also switched the language to English") is the most common cause of Cannot Reproduce. Numbering lets comments point to an exact place: "at step 4 I get different behaviour".
Приклад з проєкту. На SaaS-проєкті розробник двічі закривав баг про неправильну дату в рахунку як невідтворюваний. Я переписала кроки з нуля в чистому профілі браузера і зрозуміла, що пропустила крок: у налаштуваннях акаунта був часовий пояс America/Los_Angeles. Після того як це з’явилося в передумовах, баг відтворився з першої спроби: рахунок, створений о 23:30, мав дату наступного дня.
Example from a project. On a SaaS project a developer closed a bug about a wrong invoice date as not reproducible twice. I rewrote the steps from scratch in a clean browser profile and realised I had skipped one: the account settings had the time zone America/Los_Angeles. Once that went into the preconditions, the bug reproduced on the first try: an invoice created at 23:30 carried the next day's date.
Підводні камені. Кроки на кшталт «Оформити замовлення» — це десять дій, а не одна. Протилежна крайність — описувати очевидне («Відкрити браузер», «Ввести URL»). Не вказувати дані: «ввести email» замість «ввести qa+07@example.com». Не перевірити кроки повторно перед відправкою — найкраще пройти їх ще раз, дивлячись у свій же репорт.
Pitfalls. Steps like "Place an order" hide ten actions, not one. The opposite extreme is describing the obvious ("Open the browser", "Type the URL"). Leaving out the data: "enter an email" instead of "enter qa+07@example.com". Not rerunning the steps before sending — the best check is to walk them again while reading your own report.
Уточнююче питання. «Як бути, якщо шлях дуже довгий?» Скорочую передумовами: «замовлення в статусі Shipped створене через API, скрипт у вкладенні». Так кроки починаються з місця, де цікаво, а стартовий стан відтворюється однією командою.
Follow-up question. "What if the path is very long?" I shorten it with preconditions: "order in Shipped status created via the API, script attached". That way the steps start where it gets interesting, and the starting state is recreated with one command.
# before
1. Create an invoice
2. Date is wrong
# after
Preconditions:
- account "acme-qa" with Settings > Time zone = America/Los_Angeles
- browser time zone: Europe/Kyiv
Steps:
1. Log in as owner@acme-qa.example
2. Open Billing > Invoices, click "New invoice"
3. Add line "Consulting", qty 1, price 100.00
4. Click "Issue" at 23:30 local account time (freeze time via ?debugNow=2024-05-14T23:30)
5. Open the issued invoice
Actual: Issue date shows 15 May 2024
Expected: Issue date shows 14 May 2024 (account time zone, BILL-77 AC #2)
Preconditions; кожен крок — одна дія з даними; параметр debugNow фіксує час, щоб баг не залежав від того, коли його перевіряють.Preconditions; each step is one action with data; the debugNow parameter freezes time so the bug does not depend on when someone checks it.Що це. Мінімальне відтворення — найкоротший набір кроків і даних, який стабільно показує баг, коли все зайве прибрано. Якщо баг проявляється на кошику з двадцятьма товарами, мінімальне відтворення відповідає на питання: який саме один товар чи комбінація його викликає. Техніка — послідовно прибирати змінні і дивитися, чи баг залишився, по суті ручний двійковий пошук.
What it is. A minimal reproduction is the shortest set of steps and data that still shows the bug reliably once everything unnecessary is removed. If the bug appears on a cart with twenty items, the minimal repro answers which single item or combination triggers it. The technique is to remove variables one at a time and check whether the bug survives — a manual binary search, essentially.
Навіщо. Розробник витрачає більшу частину часу не на виправлення, а на локалізацію. Чим менше змінних у репорті, тим швидше він знаходить місце в коді. Крім того, мінімальне відтворення часто саме підказує причину («падає тільки з символом апострофа в прізвищі») і стає готовим автотестом для регресу.
Why it matters. Developers spend most of their time localising a bug, not fixing it. The fewer variables in the report, the faster they find the place in the code. A minimal repro also often points at the cause by itself ("fails only with an apostrophe in the surname") and becomes a ready-made regression test.
Приклад з проєкту. В CRM імпорт CSV на 5 000 рядків падав з помилкою 500. Я ділила файл навпіл: 2 500 рядків падали, інша половина проходила; далі 1 250, 625... За вісім ітерацій залишився один рядок: компанія «O’Brien & Sons» з типографським апострофом і амперсандом. Репорт із файлом з одного рядка виправили того ж дня, а цей рядок додали в тестові дані імпорту.
Example from a project. In a CRM, importing a 5,000-row CSV failed with a 500. I split the file in half: 2,500 rows failed, the other half passed; then 1,250, 625 and so on. After eight iterations one row was left: the company "O’Brien & Sons" with a typographic apostrophe and an ampersand. The report with a one-row file was fixed the same day, and that row was added to the import test data.
Підводні камені. Зупинитися на першому «відтворилося» і не спробувати спростити. Прибирати кілька змінних одночасно — тоді незрозуміло, яка з них важлива. Втратити оригінальний сценарій: у репорті варто залишити і мінімальний кейс, і початковий, щоб було видно реальний вплив на користувача.
Pitfalls. Stopping at the first "it reproduced" without trying to simplify. Removing several variables at once, so you cannot tell which one mattered. Losing the original scenario: the report should keep both the minimal case and the original one, so the real user impact stays visible.
Уточнююче питання. «Скільки часу виправдано витрачати на звуження?» Я ставлю собі таймбокс 20–30 хвилин. Якщо за цей час не вдалося, відправляю те, що є, з позначкою, які змінні я вже виключила, — це теж цінна інформація.
Follow-up question. "How much time is it worth spending on narrowing it down?" I timebox it to 20–30 minutes. If it does not work out, I send what I have and note which variables I have already ruled out — that is valuable information too.
$ wc -l customers.csv
5001 customers.csv # header + 5000 rows
$ head -1 customers.csv > h.csv
$ (cat h.csv; sed -n '2,2501p' customers.csv) > part1.csv # import -> 500
$ (cat h.csv; sed -n '2502,5001p' customers.csv) > part2.csv # import -> 200 OK
# ... repeat on the failing half: 1250 -> 625 -> 313 -> ... -> 1 row
$ cat minimal.csv
name,email,country
"O’Brien & Sons",ops@obrien.example,IE
Minimal repro: importing minimal.csv (1 row) returns 500
Original case: customers.csv (5000 rows) from ticket SUP-1893, attached
sed -n '2,2501p' вирізає діапазон рядків, head -1 зберігає заголовок CSV, щоб кожна половина була валідним файлом. Коментарі праворуч показують результат імпорту. Внизу — як оформити в репорті обидва кейси: мінімальний і оригінальний.sed -n '2,2501p' cuts a range of lines, head -1 keeps the CSV header so each half is a valid file. The comments on the right show the import result. At the bottom is how the report records both cases: the minimal one and the original.Що це. API-баг описують мовою контракту: метод і URL, заголовки, що впливають на поведінку (Content-Type, Accept-Language, версія API), тіло запиту, статус-код і тіло відповіді, request id або trace id і час запиту з часовим поясом. Найзручніший формат кроків — готова команда curl, яку розробник запустить у себе. Токени і персональні дані замінюють на позначки.
What it is. An API bug is described in the language of the contract: method and URL, the headers that change behaviour (Content-Type, Accept-Language, API version), the request body, the status code and response body, a request id or trace id, and the request time with its time zone. The handiest format for the steps is a ready curl command the developer can run. Tokens and personal data are replaced with markers.
Навіщо. Бекенд-розробник дебажить не через UI, а через запит і логи. Request id дозволяє за секунди знайти запит у Kibana чи Grafana Loki, а curl знімає питання «що саме відправив фронтенд». Порівняння з контрактом (OpenAPI) одразу показує, хто порушив домовленість: клієнт чи сервер.
Why it matters. A backend developer debugs through the request and the logs, not the UI. A request id lets them find the call in Kibana or Grafana Loki in seconds, and curl removes the "what exactly did the frontend send" question. Comparing with the contract (OpenAPI) immediately shows who broke the agreement: client or server.
Приклад з проєкту. У платіжному сервісі POST на створення рефанду повертав 500 для суми 0.10. Я додала curl, тіло відповіді з trace id і фрагмент специфікації, де amount описано як number з мінімумом 0.01. Розробник знайшов запис у логах за trace id за хвилину: сума перетворювалася на копійки через множення float, і 0.10 давало 10.000000000000002. Повний репорт — нижче.
Example from a project. In a payment service, a POST creating a refund returned 500 for the amount 0.10. I attached the curl command, the response body with the trace id and the spec fragment defining amount as a number with a minimum of 0.01. The developer found the log entry by trace id in a minute: the amount was converted to cents by float multiplication, and 0.10 became 10.000000000000002. The full report is below.
Підводні камені. Залишити в curl справжній Bearer-токен — це витік доступу. Прикласти тільки скриншот Postman, з якого не можна скопіювати запит. Не вказати оточення і версію API. Написати «сервер повертає помилку» без тіла відповіді: 400 з полем валідації і 500 зі stack trace — зовсім різні історії.
Pitfalls. Leaving a real Bearer token in the curl command — that is an access leak. Attaching only a Postman screenshot nobody can copy the request from. Omitting the environment and API version. Writing "the server returns an error" without the response body: a 400 with a validation field and a 500 with a stack trace are completely different stories.
Уточнююче питання. «Як відрізнити баг API від бага фронтенду?» Повторюю той самий запит через curl напряму. Якщо відповідь сервера неправильна — баг на бекенді. Якщо сервер відповідає правильно, а екран показує інше — баг у клієнті.
Follow-up question. "How do you tell an API bug from a frontend bug?" I replay the same request with curl directly. If the server response is wrong, the bug is on the backend. If the server answers correctly but the screen shows something else, the bug is in the client.
Title: [Payments API] POST /v2/refunds returns 500 for amount 0.10
Environment: staging-eu, payments-api 2.31.4 (commit 7c0d2e1), 2024-06-03 14:22 UTC
Severity: Critical (refunds under 1.00 cannot be issued) Rate: 3/3
Steps to reproduce:
curl -i -X POST https://staging-eu.pay.example/v2/refunds \
-H "Authorization: Bearer <REDACTED>" \
-H "Content-Type: application/json" \
-d '{"payment_id":"pay_7Q2k91","amount":0.10,"currency":"EUR"}'
Expected: 201 Created, refund object (openapi.yaml#/components/schemas/Refund,
amount: number, minimum 0.01)
Actual: HTTP/1.1 500 Internal Server Error
x-request-id: 5f1c9a0e-2b7d-4c1e-9d3a-81b0e6f4a221
{"error":"internal","message":"amount_minor must be an integer"}
Notes: 0.20, 1.00 and 15.50 return 201. 0.10, 0.30, 0.70 return 500.
Attachments: refund-500.log (lines for the request id above)
curl -i: -i показує заголовки відповіді, -H задає заголовки запиту, -d — тіло. Токен замінено на <REDACTED>. x-request-id — ключ для пошуку в логах, а рядок Notes показує межі: які суми проходять, а які ні.curl -i command: -i prints the response headers, -H sets request headers, -d is the body. The token is replaced with <REDACTED>. x-request-id is the key for searching the logs, and the Notes line shows the boundaries: which amounts pass and which do not.Що це. Баг — відхилення від зафіксованої очікуваної поведінки: вимоги, дизайну, контракту чи попередньої версії. Improvement (або feature request) — пропозиція змінити поведінку, яка відповідає вимогам, але могла б бути кращою. Тест простий: чи можна вказати джерело, яке порушено? Якщо так — баг. Якщо аргумент «було б зручніше» — improvement.
What it is. A bug is a deviation from recorded expected behaviour: a requirement, a design, a contract or the previous version. An improvement (or feature request) proposes to change behaviour that meets the requirements but could be better. The test is simple: can you name the source that is violated? If yes, it is a bug. If the argument is "it would be more convenient", it is an improvement.
Навіщо. Тип тікета визначає процес. Баги йдуть на тріаж і можуть блокувати реліз; improvements потрапляють у беклог продукту, де їх пріоритезують разом з іншими фічами. Якщо записувати побажання як баги, метрики якості спотворюються, а справжні дефекти губляться серед «зробіть кнопку синьою».
Why it matters. The ticket type decides the process. Bugs go to triage and can block a release; improvements go to the product backlog, where they are prioritised along with other features. Filing wishes as bugs distorts the quality metrics, and real defects get lost among "make the button blue".
Приклад з проєкту. На маркетплейсі я помітила, що фільтр ціни скидається після переходу на другу сторінку результатів. У специфікації пошуку було явно написано, що фільтри зберігаються під час пагінації, тож я оформила баг. А те, що фільтр не запам’ятовується між сесіями, я оформила як improvement з описом, як це роблять конкуренти, — у вимогах такого не було.
Example from a project. On a marketplace I noticed that the price filter reset when moving to the second page of results. The search specification said explicitly that filters persist across pagination, so I filed a bug. The fact that the filter was not remembered between sessions I filed as an improvement, describing how competitors handle it — the requirements said nothing about that.
Підводні камені. Маскувати побажання під баг, щоб його швидше взяли в роботу, — продукт це помічає, і довіра до репортів QA падає. Протилежне: не репортити нічого, що «формально за вимогами», навіть коли поведінка явно шкодить користувачам, — такі речі все одно варто зафіксувати як improvement з аргументами.
Pitfalls. Dressing a wish up as a bug to get it picked up faster — product notices, and trust in QA reports drops. The opposite: never reporting anything that is "technically per spec", even when the behaviour clearly hurts users; such things are still worth recording as an improvement with arguments.
Уточнююче питання. «А якщо розробник каже, що це не баг, а фіча?» Прошу посилання на джерело, де така поведінка описана. Якщо його немає, питання йде до продукту: він вирішує, чи це дефект, чи нова вимога, і тип тікета змінюють за його рішенням.
Follow-up question. "What if the developer says it is a feature, not a bug?" I ask for the source that describes that behaviour. If there is none, the question goes to product: they decide whether it is a defect or a new requirement, and the ticket type changes according to their decision.
Type: Bug
Title: [Search] Price filter resets when opening page 2 of results
Expected: Filters persist across pagination (SRCH-12, AC #4)
Actual: Page 2 shows unfiltered results; URL loses &price=100-500
Type: Improvement
Title: [Search] Remember last used filters between sessions
Problem: Returning users re-enter the same 3 filters every visit
(analytics: 41% of sessions start with the same price filter)
Proposal: Persist filters in the user profile, add a "Clear all" chip
Reference: similar behaviour on two competitor marketplaces (screenshots attached)
Expected з посиланням на порушений пункт вимог. У improvement замість цього — Problem з даними про користувачів і Proposal, тобто аргументи для продукту, а не відхилення від специфікації.Expected linked to the violated requirement item. The improvement instead has a Problem backed by user data and a Proposal — arguments for product, not a deviation from the spec.Що це. Severity — це оцінка технічного впливу дефекту на систему і користувача, незалежно від того, коли його чинитимуть. Типова шкала: Blocker (неможливо продовжити роботу або тестування), Critical (ключова функція зламана, втрата даних чи грошей), Major (функція працює неправильно, але є обхідний шлях), Minor (незручність, другорядна функція), Trivial (косметика, текст). Письмова шкала з прикладами для свого продукту перетворює це з відчуття на правило.
What it is. Severity is an assessment of the technical impact of a defect on the system and the user, regardless of when it will be fixed. A typical scale: Blocker (work or testing cannot continue), Critical (a key function is broken, data or money is lost), Major (a function misbehaves but there is a workaround), Minor (an inconvenience, a secondary function), Trivial (cosmetics, wording). A written scale with examples from your own product turns this from a feeling into a rule.
Навіщо. Без спільної шкали кожен тестувальник оцінює по-своєму: для одного друкарська помилка в ціні — Trivial, для іншого — Critical. Тоді severity перестає нести інформацію, і тріаж щоразу сперечається з нуля. Письмові критерії дають однакові оцінки, швидший тріаж і коректні метрики на кшталт «скільки Critical ми випустили в прод».
Why it matters. Without a shared scale every tester rates differently: a typo in a price is Trivial to one person and Critical to another. Severity then carries no information, and triage argues from scratch every time. Written criteria give consistent ratings, faster triage and honest metrics such as "how many Critical bugs reached production".
Приклад з проєкту. На фінтех-проєкті ми записали шкалу з прикладами прямо в Confluence і прив’язали до шаблону бага в Jira. Ключовий рядок: будь-яка розбіжність у сумі, яку бачить клієнт, — мінімум Critical, навіть якщо вона на копійку. Суперечки на тріажі щодо severity майже зникли, а нові QA почали оцінювати однаково вже з першого тижня.
Example from a project. On a fintech project we wrote the scale with examples straight into Confluence and linked it from the Jira bug template. The key line: any discrepancy in an amount the customer sees is at least Critical, even if it is one cent. Severity arguments at triage all but disappeared, and new QA engineers rated consistently from their first week.
Підводні камені. Плутати severity з priority: баг може бути Critical, але з низьким пріоритетом, якщо фічею ніхто не користується. Завищувати severity, щоб баг швидше взяли, — команда перестає вірити оцінкам. Шкала без прикладів: «Major — значний вплив» нічого не пояснює, потрібні конкретні кейси зі свого продукту.
Pitfalls. Confusing severity with priority: a bug can be Critical yet low priority if nobody uses the feature. Inflating severity to get a bug picked up sooner — the team stops trusting the ratings. A scale without examples: "Major — significant impact" explains nothing; you need concrete cases from your product.
Уточнююче питання. «Хто остаточно вирішує severity?» Початкову оцінку ставить QA за шкалою. На тріажі її можуть змінити, якщо з’ясувалися нові факти, наприклад що є обхідний шлях, але зміна має бути з коментарем, чому.
Follow-up question. "Who has the final say on severity?" QA sets the initial rating using the scale. Triage may change it if new facts come up, for example a workaround, but the change should come with a comment explaining why.
Blocker Checkout or login unavailable for everyone; staging deploy broken
e.g. "Pay" returns 502 for all cards
Critical Money, orders or personal data are wrong or lost; no workaround
e.g. total differs from line items by any amount; order not saved
Major Key flow broken for a segment, or a workaround exists
e.g. Apple Pay fails on iOS 16 only; promo works after page reload
Minor Secondary feature misbehaves; user can still finish the task
e.g. wishlist counter not updated until refresh
Trivial Cosmetic, wording, alignment
e.g. "Adress" typo in the footer; 2px icon offset
Rule: if unsure between two levels, pick the higher one and say why in a comment.
Що це. Priority — це черговість виправлення: як скоро команда має взятися за баг відносно інших задач. Зазвичай його визначає product owner або тріаж-група, а QA пропонує значення і дає факти. Рішення враховує severity, кількість зачеплених користувачів, гроші і репутацію, наближення релізу, наявність обхідного шляху і вартість виправлення.
What it is. Priority is the order of fixing: how soon the team should take the bug relative to other work. It is usually decided by the product owner or a triage group, while QA suggests a value and supplies the facts. The decision weighs severity, the number of affected users, money and reputation, how close the release is, whether a workaround exists, and the cost of the fix.
Навіщо. Команда завжди має більше багів, ніж часу. Priority відповідає на питання «що робимо першим у цьому спринті», тоді як severity описує сам дефект. Якщо priority ставить той, хто бачить тільки технічну сторону, у роботу підуть цікаві баги, а не ті, що коштують бізнесу грошей.
Why it matters. A team always has more bugs than time. Priority answers "what do we do first this sprint", whereas severity describes the defect itself. If priority is set by someone who only sees the technical side, the interesting bugs get fixed rather than the ones costing the business money.
Приклад з проєкту. У застосунку для бронювання я знайшла два баги. Перший: падіння при експорті бронювань у CSV в адмінці — Critical, але цією функцією користувалися двоє співробітників раз на місяць, і був обхідний шлях через API. Другий: на головній сторінці неправильна дата акції «Black Friday» — Minor за severity, але кампанія стартувала через два дні. PO поставив другому Highest, першому — Medium.
Example from a project. In a booking app I found two bugs. The first: the admin CSV export of bookings crashed — Critical, but two staff members used it once a month and there was a workaround through the API. The second: the home page showed the wrong date for the Black Friday campaign — Minor by severity, but the campaign started in two days. The PO gave the second Highest and the first Medium.
Підводні камені. Ставити priority за автоматичною таблицею «Critical завжди High» — це ігнорує бізнес-контекст. QA, що самовільно виставляє Highest, підміняє рішення продукту. Не оновлювати priority, коли змінилися обставини: баг, який був Low, стає High, коли фічу вмикають для всіх клієнтів.
Pitfalls. Setting priority from an automatic table like "Critical is always High" ignores business context. A QA who sets Highest on their own is overriding a product decision. Not updating priority when circumstances change: a Low bug becomes High once the feature is switched on for all customers.
Уточнююче питання. «А якщо ти не згодна з пріоритетом, який поставив PO?» Додаю в тікет факти, яких, можливо, не було: кількість звернень у підтримку, частку зачеплених користувачів, втрачені замовлення. Рішення залишається за ним, але вже на повних даних.
Follow-up question. "What if you disagree with the priority the PO set?" I add facts to the ticket they may not have had: the number of support requests, the share of affected users, lost orders. The decision stays theirs, but now it is made on full data.
Severity: Minor (wrong text, no functional impact)
Suggested priority: Highest
Impact:
- Home page banner shows "Black Friday: Nov 30" instead of "Nov 29"
- Campaign goes live in 2 days; banner is seen by ~180k visitors/day
- Paid ads already point to this page (MKT-512)
Fix cost: text change in CMS, no deploy needed
Decision: @po please confirm priority
Що це. Severity і priority — незалежні осі, тому можливі всі чотири комбінації. High/High: не проходить оплата. High severity / low priority: падіння у функції, якою майже ніхто не користується, або на застарілій платформі. Low severity / high priority: помилка в логотипі чи назві бренду на головній, неправильний текст у юридичному дисклеймері. Low/Low: зсув іконки на внутрішній сторінці налаштувань.
What it is. Severity and priority are independent axes, so all four combinations exist. High/High: payments fail. High severity / low priority: a crash in a feature almost nobody uses, or on a deprecated platform. Low severity / high priority: a mistake in the logo or brand name on the home page, wrong wording in a legal disclaimer. Low/Low: an icon offset on an internal settings page.
Навіщо. Це класичне питання на співбесіді, бо воно перевіряє, чи розуміє кандидат, що severity оцінює дефект, а priority — рішення бізнесу. Уміння навести власні приклади показує, що ти бачила реальний тріаж, а не вивчила визначення.
Why it matters. It is a classic interview question because it checks whether the candidate understands that severity rates the defect while priority is a business decision. Having your own examples shows you have sat through real triage rather than memorised definitions.
Приклад з проєкту. На проєкті страхового порталу: калькулятор полісу падав в Internet Explorer 11 (High severity), але частка IE була 0,3% і його офіційно перестали підтримувати — priority Low, тікет закрили як Won’t Fix з посиланням на політику браузерів. Водночас у футері стояв неправильний номер ліцензії регулятора (Trivial за severity), але юристи вимагали виправити його до кінця дня — Highest.
Example from a project. On an insurance portal the policy calculator crashed in Internet Explorer 11 (high severity), but IE had a 0.3% share and was officially out of support — low priority, closed as Won't Fix with a link to the browser policy. At the same time the footer showed the wrong regulator licence number (trivial severity), but legal demanded it fixed by the end of the day — Highest.
Підводні камені. Наводити хрестоматійний приклад «логотип компанії» без пояснення, чому priority високий. Вважати, що High severity завжди означає негайний фікс. Забувати, що обидві оцінки змінюються з часом: баг у рідкісній функції стає High priority, коли маркетинг її анонсує.
Pitfalls. Giving the textbook "company logo" example without explaining why priority is high. Assuming high severity always means an immediate fix. Forgetting that both ratings change over time: a bug in a rarely used feature becomes high priority once marketing announces it.
Уточнююче питання. «Наведи приклад з власного досвіду». Варто мати напоготові два реальні кейси: Low severity / High priority і High severity / Low priority, з поясненням бізнес-причини в кожному.
Follow-up question. "Give an example from your own experience." Have two real cases ready: low severity / high priority and high severity / low priority, each with its business reason.
| Priority HIGH | Priority LOW
-----------------+-----------------------------------+-----------------------------------
Severity HIGH | Card payment returns 502 | IE11 calculator crash (0.3% users,
| for all users | browser out of support)
-----------------+-----------------------------------+-----------------------------------
Severity LOW | Wrong regulator licence number | Settings icon 2px off on
| in footer (legal deadline today) | internal admin page
Що це. UX-баг — це дефект, за якого функція формально працює, але користувач плутається, робить зайві кроки або кидає сценарій: незрозуміле повідомлення про помилку, кнопка поза видимою областю на мобільному, фокус, що стрибає. За severity такі баги часто Minor, тому аргументувати priority доводиться даними: частотою (скільки людей проходить цей екран), впливом на конверсію, доступністю і зверненнями в підтримку.
What it is. A UX bug is a defect where the function technically works, but users get confused, take extra steps or abandon the flow: an unclear error message, a button off-screen on mobile, focus that jumps around. By severity these bugs are often Minor, so priority has to be argued with data: frequency (how many people go through this screen), impact on conversion, accessibility and support requests.
Навіщо. Без даних UX-баги застрягають у беклозі назавжди, бо «працює ж». Водночас саме вони часто коштують бізнесу найбільше: користувач не бачить помилки, він просто йде. Якщо QA приносить цифри з аналітики, UX-баг конкурує з функціональними на рівних.
Why it matters. Without data, UX bugs sit in the backlog forever because "it works". Yet they often cost the business the most: users do not see an error, they simply leave. When QA brings numbers from analytics, a UX bug competes with functional bugs on equal terms.
Приклад з проєкту. У формі реєстрації на мобільному при відкритій клавіатурі кнопка «Продовжити» ховалася під нею, і треба було закрити клавіатуру, щоб її побачити. Severity — Minor. Я взяла з аналітики воронку: на цьому кроці на мобільних відвалювалося 23% користувачів проти 6% на десктопі, а в підтримку за місяць надійшло 40 звернень «не можу зареєструватися». З цими цифрами PO поставив High і взяв баг у поточний спринт.
Example from a project. On a mobile sign-up form the Continue button hid behind the open keyboard, and you had to close the keyboard to see it. Severity: Minor. I pulled the funnel from analytics: 23% of mobile users dropped at that step against 6% on desktop, and support had 40 "can't sign up" requests that month. With those numbers the PO set High and took the bug into the current sprint.
Підводні камені. Писати «незручно» без конкретики — це сприймуть як смак. Не перевірити доступність: якщо проблема блокує користувача зі скрінрідером або з клавіатури, це вже порушення WCAG, і severity зростає. Перетворювати UX-баг на редизайн: репорт має описувати конкретну проблему, а не пропонувати новий екран.
Pitfalls. Writing "inconvenient" without specifics — it gets read as a matter of taste. Not checking accessibility: if the problem blocks a screen reader or keyboard user, it is a WCAG violation and severity goes up. Turning a UX bug into a redesign: the report should describe a specific problem, not propose a new screen.
Уточнююче питання. «Де ти береш дані, якщо немає доступу до аналітики?» Прошу в продакт-аналітика або підтримки, дивлюся теги звернень, а для доступності — на критерії WCAG 2.2, які порушено. Навіть одне число сильніше за «мені здається».
Follow-up question. "Where do you get data if you have no analytics access?" I ask a product analyst or support, look at the request tags, and for accessibility I cite the WCAG 2.2 criteria that are violated. Even one number beats "I feel like".
Title: [Sign-up][Mobile] Continue button hidden behind keyboard on step 2
Environment: prod, iPhone 13 / iOS 17.5 Safari; Pixel 7 / Android 14 Chrome
Severity: Minor (user can close the keyboard and continue)
Suggested priority: High
Evidence:
- Funnel step 2 -> 3: mobile drop-off 23%, desktop 6% (Amplitude, last 30 days)
- 40 support tickets tagged "signup-stuck" in May (Zendesk view #118)
- Keyboard users: focus moves to the hidden button, no scroll into view
(WCAG 2.2 SC 2.4.11 Focus Not Obscured)
Attachments: signup-keyboard.mp4, funnel.png
Evidence дає три незалежні аргументи для високого priority: воронка з аналітики, звернення в підтримку з тегом і конкретний критерій WCAG. Джерело кожного числа вказане, щоб його можна було перевірити.Evidence block gives three independent arguments for high priority: the analytics funnel, tagged support tickets and a specific WCAG criterion. The source of every number is named so it can be checked.Що це. Блокер релізу — дефект, з яким команда погодилася не виходити в прод. Критерії варто записати заздалегідь, а не вигадувати в день релізу. Типовий набір: зламаний критичний шлях (логін, оплата, оформлення замовлення), втрата чи пошкодження даних, вразливість безпеки або витік персональних даних, порушення юридичних вимог, регресія, що зачіпає значну частку користувачів без обхідного шляху. Рішення go/no-go ухвалює власник релізу (PO, release manager), а QA дає статус і ризики.
What it is. A release blocker is a defect the team has agreed not to ship with. The criteria should be written down in advance, not invented on release day. A typical set: a broken critical path (login, payment, checkout), data loss or corruption, a security vulnerability or personal data leak, a legal compliance breach, a regression that hits a significant share of users with no workaround. The go/no-go decision belongs to the release owner (PO, release manager); QA provides the status and the risks.
Навіщо. Без заздалегідь узгоджених критеріїв кожен реліз перетворюється на торг: розробка тисне на дедлайн, QA — на якість, і рішення залежить від того, хто голосніший. Записані критерії роблять go/no-go передбачуваним і знімають з QA роль «того, хто не пускає реліз».
Why it matters. Without criteria agreed in advance, every release becomes a negotiation: engineering pushes the deadline, QA pushes quality, and the decision depends on who is louder. Written criteria make go/no-go predictable and take QA out of the role of "the person blocking the release".
Приклад з проєкту. На e-commerce проєкті ми тримали release blocker criteria на сторінці релізного процесу і JQL-фільтр «потенційні блокери» на дашборді. За день до релізу фільтр показав баг: при оплаті частинами кошик очищався, якщо користувач повертався назад з платіжного шлюзу. Це підпадало під «зламаний критичний шлях без обхідного шляху». Release manager переніс реліз на день, фікс пройшов ретест, і реліз вийшов з приміткою в release notes.
Example from a project. On an e-commerce project we kept the release blocker criteria on the release process page and a "potential blockers" JQL filter on the dashboard. The day before a release the filter surfaced a bug: with pay-in-instalments, the cart emptied if the user came back from the payment gateway. That matched "broken critical path with no workaround". The release manager moved the release by a day, the fix passed the retest, and the release went out with a note in the release notes.
Підводні камені. QA, яка одноосібно «забороняє» реліз, — рішення має ухвалювати власник з урахуванням бізнесу, QA дає факти. Протилежне — блокер понижують до Major, щоб вкластися в дату, без запису ризику. Додавати блокер без перевірки на останньому білді: інколи його вже виправили.
Pitfalls. A QA engineer single-handedly "forbidding" the release — the owner should decide with the business in mind; QA supplies facts. The opposite: a blocker downgraded to Major to hit the date, with no risk recorded. Raising a blocker without checking it on the latest build: sometimes it is already fixed.
Уточнююче питання. «Що робити, якщо блокер знайшли за годину до релізу?» Факти в тікеті за 10 хвилин (кроки, вплив, частка користувачів), повідомлення власнику релізу, варіанти: відкласти, вимкнути фічу прапорцем, випустити з відомою проблемою і планом. Вибирає власник, а рішення фіксується в тікеті.
Follow-up question. "What if a blocker turns up an hour before release?" Facts in the ticket within ten minutes (steps, impact, share of users), notify the release owner, lay out the options: postpone, switch the feature off with a flag, or ship with a known issue and a plan. The owner chooses, and the decision is recorded in the ticket.
# Release blocker criteria (agreed with PO and engineering, v2)
# 1. Login, search, cart, checkout or payment broken with no workaround
# 2. Data loss/corruption: orders, payments, user profiles
# 3. Security issue rated High+ or any personal data exposure
# 4. Legal/compliance breach (prices, consent, invoices)
# 5. Regression affecting >5% of sessions per analytics
project = SHOP AND issuetype = Bug
AND fixVersion = "4.13.0"
AND statusCategory != Done
AND (priority in (Highest, High) OR labels = release-blocker)
ORDER BY priority DESC, created ASC
release-blocker; statusCategory != Done ловить усі незавершені статуси незалежно від назв у workflow.release-blocker label; statusCategory != Done catches every unfinished status regardless of the names in the workflow.Що це. Баг від клієнта — це звичайний баг-репорт плюс блок впливу на клієнтів: посилання на звернення в службі підтримки, кількість зачеплених акаунтів або замовлень, тариф чи сегмент клієнта, дата першої появи, чи є обхідний шлях, який підтримка вже дає. QA відтворює проблему на тестовому стенді з анонімізованими даними, а не копіює персональні дані з тікета підтримки.
What it is. A customer-reported bug is a normal bug report plus a customer impact block: links to the support tickets, the number of affected accounts or orders, the customer's plan or segment, when it first appeared, and any workaround support is already giving out. QA reproduces the problem on a test environment with anonymised data instead of copying personal data from the support ticket.
Навіщо. Для продукту і тріажу важливо бачити, що баг уже зачепив реальних людей: це впливає на priority, на потребу в hotfix і на комунікацію з клієнтом. Підтримці важливо отримати статус назад, щоб відповісти людині. Без посилання на звернення цей зв’язок губиться, і клієнт чекає тижнями.
Why it matters. Product and triage need to see that the bug has already hit real people: it affects priority, the need for a hotfix and the communication with the customer. Support needs the status back so they can reply. Without a link to the support ticket that connection is lost and the customer waits for weeks.
Приклад з проєкту. У B2B SaaS великий клієнт написав, що звіт за квартал показує на 12 угод менше, ніж експорт у CSV. Я знайшла ще три схожі звернення за тиждень, відтворила на staging: угоди, закриті в останній день кварталу після 21:00 за UTC, не потрапляли в звіт через межу часового поясу. У репорті були посилання на чотири звернення, тариф Enterprise у двох клієнтів і обхідний шлях через експорт. Баг отримав priority Highest і пішов як hotfix.
Example from a project. In a B2B SaaS product a large customer wrote that the quarterly report showed 12 fewer deals than the CSV export. I found three more similar tickets from that week and reproduced it on staging: deals closed on the last day of the quarter after 21:00 UTC were left out of the report because of a time zone boundary. The report linked all four support tickets, noted two customers on the Enterprise plan and gave the export workaround. The bug got Highest priority and shipped as a hotfix.
Підводні камені. Вставити в репорт скриншот з email, телефоном чи сумою клієнта — персональні дані не мають потрапляти в трекер, де їх бачить уся компанія. Не спробувати відтворити самостійно, а переслати текст клієнта як є. Забути повідомити підтримку про фікс — клієнт дізнається останнім.
Pitfalls. Pasting a screenshot with the customer's email, phone or amounts — personal data must not end up in a tracker the whole company can read. Not trying to reproduce it yourself and forwarding the customer's text as is. Forgetting to tell support about the fix — the customer finds out last.
Уточнююче питання. «Як поєднати звернення підтримки і баг?» Через інтеграцію трекерів (Zendesk або Jira Service Management з Jira) або хоча б посиланням у полі. Тоді підтримка бачить статус бага і кількість звернень автоматично рахується.
Follow-up question. "How do you connect support tickets and a bug?" Through a tracker integration (Zendesk or Jira Service Management with Jira), or at least a link field. Then support sees the bug status and the number of related requests is counted automatically.
Title: [Reports] Quarterly report misses deals closed after 21:00 UTC on the last day
Environment: prod (reported), reproduced on staging-1, app 7.4.2
Customer impact:
- Support tickets: ZD-44102, ZD-44117, ZD-44150, ZD-44163
- 4 accounts affected, 2 on Enterprise plan; first report 2024-07-01
- Workaround given by support: use Deals > Export CSV
Preconditions: account tz = America/New_York; 3 deals closed 2024-06-30 22:15 UTC
Steps to reproduce:
1. Open Reports > Sales > Q2 2024
2. Compare "Closed deals" count with Deals > Export CSV for the same range
Expected: Report and export both show 57 deals
Actual: Report shows 54; the 3 deals closed after 21:00 UTC on 30 Jun are missing
Severity: Critical (wrong business figures, no error shown) Rate: 3/3
Data: reproduced with synthetic account qa-tz-ny; no customer data copied
Customer impact відрізняє цей репорт від звичайного: номери звернень, кількість і сегмент клієнтів, дата першої появи, обхідний шлях. Рядок Data прямо каже, що відтворення зроблене на синтетичному акаунті без копіювання даних клієнта.Customer impact block sets this report apart from a normal one: ticket numbers, the number and segment of customers, the first occurrence, the workaround. The Data line states plainly that the reproduction used a synthetic account and copied no customer data.Що це. Докази — це все, що підтверджує actual без твоїх слів: скриншот для статичної проблеми, відео для послідовності дій і анімацій, консоль браузера для JS-помилок, HAR або відповідь API для мережевих проблем, лог сервера або trace id для бекенду. Найцінніше те, що прив’язане до конкретного build і часу, а не просто «ось як воно виглядає».
What it is. Evidence is anything that confirms the actual result without your word for it: a screenshot for a static problem, a video for a sequence of actions or animations, the browser console for JS errors, a HAR or API response for network problems, a server log or trace id for the backend. The most valuable evidence is tied to a specific build and time rather than "here is what it looks like".
Навіщо. Розробник може не бачити того, що бачила ти: інший браузер, інші дані, баг зник після перезавантаження. Доказ фіксує момент. Відео з видимою консоллю часто замінює пів сторінки тексту, а помилка з консолі з назвою файлу і рядком прямо вказує на місце в коді.
Why it matters. The developer may not see what you saw: another browser, other data, the bug gone after a reload. Evidence freezes the moment. A video with the console visible often replaces half a page of text, and a console error with a file name and line number points straight at the code.
Приклад з проєкту. На проєкті з онлайн-навчанням прогрес уроку інколи не зберігався. На звичайному скриншоті видно лише «0%». Я записала відео з відкритими DevTools: на ньому видно, як PATCH /progress повертає 409 Conflict саме тоді, коли дві вкладки з уроком відкриті одночасно. Відео тривало 25 секунд, і розробник зрозумів проблему, не читаючи кроків.
Example from a project. On an e-learning project, lesson progress sometimes failed to save. A plain screenshot only showed "0%". I recorded a video with DevTools open: it showed PATCH /progress returning 409 Conflict exactly when the lesson was open in two tabs at once. The video was 25 seconds long, and the developer understood the problem without reading the steps.
Підводні камені. Скриншот усього екрана 4K без виділення — ніхто не знайде, куди дивитися. Відео на три хвилини з пошуком потрібного місця: обріж до 20–40 секунд і вкажи таймкод. На скриншоті видно чужі персональні дані, токени у вкладці Network або відкритий месенджер. Відсутній годинник чи URL: без них незрозуміло, коли і де це було.
Pitfalls. A full 4K screenshot with nothing highlighted — nobody knows where to look. A three-minute video you have to scrub through: trim it to 20–40 seconds and give a timestamp. Someone else's personal data, tokens in the Network tab or an open chat visible in the screenshot. No clock or URL: without them nobody knows when and where it happened.
Уточнююче питання. «Що краще: відео чи текст?» Обидва: текст для кроків, які можна скопіювати і за якими можна шукати, відео — як підтвердження. Відео без текстових кроків не можна ні знайти пошуком, ні автоматизувати.
Follow-up question. "Which is better, a video or text?" Both: text for steps people can copy and search, video as confirmation. A video without written steps can be neither found by search nor automated.
Screenshot
[x] cropped to the relevant area, problem highlighted with a box/arrow
[x] URL bar and time visible (or noted in the file name)
[x] no personal data, tokens or other apps in the frame
Video (20-40 s)
[x] DevTools open on Console or Network when relevant
[x] starts one step before the problem, not from login
[x] timestamp in the report: "00:14 - 409 on PATCH /progress"
Console / network
[x] full error text copied as text, not only as a picture
[x] request id / trace id from response headers
File names
progress-409_staging-2_build-5.8.1_2024-06-12T10-41.mp4
Що це. Оточення — це все, від чого може залежати відтворення: стенд (dev, staging, prod, регіон), версія застосунку і build, браузер і його версія або пристрій і ОС, роздільна здатність і масштаб, мова і часовий пояс, роль і тип акаунта, увімкнені feature flags і A/B-варіант. Для мобільних — ще тип мережі і версія збірки з магазину чи TestFlight.
What it is. The environment is everything the reproduction might depend on: the environment itself (dev, staging, prod, region), the app version and build, the browser and its version or the device and OS, resolution and zoom, language and time zone, the role and account type, active feature flags and the A/B variant. For mobile, add the network type and the build number from the store or TestFlight.
Навіщо. Значна частина багів залежить від оточення: верстка ламається лише в Safari, дата зсувається лише в певному поясі, кнопки немає лише у варіанті B експерименту. Якщо оточення не вказане, розробник тестує у своєму, баг не відтворюється і тікет повертається. Повне оточення — найдешевший спосіб уникнути Cannot Reproduce.
Why it matters. A large share of bugs depend on the environment: the layout breaks only in Safari, a date shifts only in one time zone, a button is missing only in variant B of an experiment. If the environment is not stated, the developer tests in their own, the bug does not reproduce and the ticket bounces back. A full environment is the cheapest way to avoid Cannot Reproduce.
Приклад з проєкту. На сайті авіаквитків кнопку «Оплатити» не було видно у частини тестувальників. У репорті я вказала не тільки Chrome 126, а й масштаб 125% у Windows і варіант B експерименту new-checkout (його видно в cookie ab_checkout). Розробник одразу ввімкнув той самий варіант і масштаб і побачив, що кнопка виходить за межі контейнера фіксованої висоти.
Example from a project. On a flight booking site the Pay button was invisible for some testers. In the report I listed not just Chrome 126 but also 125% Windows scaling and variant B of the new-checkout experiment (visible in the ab_checkout cookie). The developer switched on the same variant and zoom and immediately saw the button overflowing a fixed-height container.
Підводні камені. Писати «Chrome, останній» — через місяць «останній» буде іншим. Забувати про feature flags і A/B-тести — вони змінюють поведінку сильніше, ніж версія браузера. Переписувати оточення вручну з помилками замість того, щоб скопіювати його з системної сторінки чи консолі.
Pitfalls. Writing "Chrome, latest" — in a month "latest" will be something else. Forgetting feature flags and A/B tests — they change behaviour more than the browser version does. Retyping the environment by hand with mistakes instead of copying it from a system page or the console.
Уточнююче питання. «Як зібрати оточення, якщо бракує часу?» Багато команд мають сторінку /about або /debug з версією і прапорцями, а в браузері достатньо одного рядка в консолі DevTools, як у прикладі нижче. Для мобільних — екран «Про застосунок» або логи з Xcode чи Android Studio.
Follow-up question. "How do you collect the environment when you are short on time?" Many teams have an /about or /debug page with the version and flags, and in the browser one line in the DevTools console is enough, as in the example below. On mobile, use the About screen or the logs from Xcode or Android Studio.
// paste into the DevTools console; copy() puts the result on the clipboard
copy(JSON.stringify({
url: location.href,
userAgent: navigator.userAgent,
language: navigator.language,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
viewport: innerWidth + "x" + innerHeight,
devicePixelRatio: devicePixelRatio,
abCheckout: (document.cookie.match(/ab_checkout=([^;]+)/) || [])[1],
appVersion: document.querySelector("meta[name=app-version]")?.content
}, null, 2));
copy() — вбудована утиліта консолі Chrome DevTools, яка копіює значення в буфер обміну. Intl.DateTimeFormat().resolvedOptions().timeZone повертає часовий пояс браузера, devicePixelRatio відображає масштаб, а регулярний вираз дістає A/B-варіант з cookie. Назви cookie і meta-тегу залежать від проєкту.copy() is a built-in Chrome DevTools console utility that puts a value on the clipboard. Intl.DateTimeFormat().resolvedOptions().timeZone returns the browser time zone, devicePixelRatio reflects the zoom, and the regular expression pulls the A/B variant out of the cookie. The cookie and meta tag names depend on the project.Що це. Build (номер збірки) або commit SHA однозначно прив’язують симптом до конкретного стану коду. На стенді, куди деплоять кілька разів на день, «staging» без номера нічого не означає: через годину там інший код. Для регресійних багів корисно також вказати останню відому робочу версію, тоді розробник бачить діапазон змін, серед яких шукати причину.
What it is. A build number or commit SHA ties the symptom unambiguously to a specific state of the code. On an environment deployed several times a day, "staging" without a number means nothing: an hour later there is different code on it. For regressions it also helps to note the last known good version, so the developer sees the range of changes to search.
Навіщо. З build розробник відкриває саме той код, перевіряє, чи фікс уже є в новішій збірці, і не витрачає час на «у мене в main усе працює». Діапазон «працювало в 4.11.2, зламалося в 4.12.0» звужує пошук до кількох десятків комітів, а git bisect — до одного.
Why it matters. With the build the developer opens exactly that code, checks whether the fix is already in a newer build and does not waste time on "it works for me on main". A range like "worked in 4.11.2, broke in 4.12.0" narrows the search to a few dozen commits, and git bisect narrows it to one.
Приклад з проєкту. На проєкті логістики після релізу 4.12.0 перестав друкуватися маршрутний лист. Я знала, що в 4.11.2 він працював. Розробник запустив git bisect між цими тегами з автотестом друку як перевіркою, і за шість кроків з 57 комітів знайшов коміт, де оновили бібліотеку генерації PDF. Без діапазону версій шукали б наосліп.
Example from a project. On a logistics project the route sheet stopped printing after release 4.12.0. I knew it worked in 4.11.2. The developer ran git bisect between those tags with the print autotest as the check, and in six steps out of 57 commits found the one that upgraded the PDF generation library. Without the version range they would have been searching blind.
Підводні камені. Брати версію з пам’яті або з назви гілки — треба дивитися на реально задеплоєний build. Вказувати версію фронтенду, коли баг у бекенді (і навпаки): у мікросервісах кожен сервіс має свою версію. Не перевірити баг на найновішій збірці перед репортом — можливо, його вже виправили.
Pitfalls. Taking the version from memory or from the branch name — you need the build actually deployed. Giving the frontend version when the bug is in the backend (or vice versa): in microservices each service has its own version. Not checking the bug on the latest build before reporting it — it may already be fixed.
Уточнююче питання. «Як дізнатися версію, якщо її ніде не видно?» Попросити розробників додати endpoint /version або meta-тег з build у HTML — це невелика задача, яка окупається за тиждень. До того часу — дивитися останній успішний деплой у CI.
Follow-up question. "How do you find the version if it is shown nowhere?" Ask the developers to add a /version endpoint or a build meta tag in the HTML — a small task that pays for itself within a week. Until then, check the last successful deploy in CI.
$ curl -s https://staging.logistics.example/api/version
{"service":"routes-api","version":"4.12.0","commit":"e41c07b","built_at":"2024-05-20T09:12:44Z"}
# Report line:
# Build: routes-api 4.12.0 (e41c07b); last known good: 4.11.2 (9a2f310)
$ git bisect start
$ git bisect bad v4.12.0
$ git bisect good v4.11.2
Bisecting: 28 revisions left to test after this (roughly 5 steps)
$ git bisect run npx playwright test tests/print-route-sheet.spec.ts
...
3f8d2c1 is the first bad commit
chore(deps): bump pdf-renderer from 2.4.1 to 3.0.0
$ git bisect reset
curl -s тихо забирає JSON з версією і commit. git bisect ділить діапазон комітів навпіл: bad і good задають межі, run автоматично проганяє тест на кожному кроці (код виходу 0 — good, інший — bad), reset повертає репозиторій у початковий стан.curl -s quietly fetches the JSON with the version and commit. git bisect halves the commit range: bad and good set the boundaries, run executes the test at every step automatically (exit code 0 means good, anything else bad), and reset returns the repository to where it was.Що це. HAR (HTTP Archive) — JSON-файл з усіма запитами сторінки: URL, методи, заголовки, тіла, статуси і таймінги. Записують його у вкладці Network у DevTools: увімкнути Preserve log, відтворити баг, експортувати. Chrome починаючи з версії 130 за замовчуванням експортує «санітизований» HAR без cookie і заголовка Authorization; повний експорт вмикається окремим налаштуванням. Але тіла запитів і відповідей з персональними даними залишаються в будь-якому варіанті.
What it is. A HAR (HTTP Archive) is a JSON file holding every request the page made: URLs, methods, headers, bodies, statuses and timings. You record it in the DevTools Network tab: turn on Preserve log, reproduce the bug, export. Since version 130 Chrome exports a "sanitized" HAR by default, without cookies and the Authorization header; full export needs a separate setting. Request and response bodies with personal data remain in either variant.
Навіщо. HAR показує мережеву картину цілком: порядок запитів, перенаправлення, повторні виклики, повільні відповіді, CORS-помилки. Розробник може відкрити його у своїх DevTools або в HAR-переглядачі і побачити те саме, що бачила ти, навіть якщо баг більше не відтворюється. Це особливо корисно для проблем, які залежать від таймінгів.
Why it matters. A HAR shows the complete network picture: request order, redirects, repeated calls, slow responses, CORS errors. The developer can open it in their own DevTools or a HAR viewer and see what you saw, even when the bug no longer reproduces. That is especially useful for timing-dependent problems.
Приклад з проєкту. У банківському кабінеті виписка інколи показувала дані за попередній місяць. HAR пояснив чому: два запити GET /statements з різними параметрами місяця йшли паралельно, і повільніша відповідь на старий запит перезаписувала новішу. У HAR це видно по таймінгах: відповідь на перший запит прийшла через 2,1 секунди, на другий — через 0,4. Це класичний race condition, який на скриншоті не побачиш.
Example from a project. In an online banking account the statement sometimes showed the previous month. The HAR explained why: two GET /statements requests with different month parameters ran in parallel, and the slower response to the older request overwrote the newer one. The timings made it visible: the first response arrived after 2.1 seconds, the second after 0.4. A classic race condition you would never see on a screenshot.
Підводні камені. Прикласти повний HAR з активною сесією: будь-хто з доступом до тікета може перехопити акаунт, поки cookie дійсні. Забути, що в тілах відповідей є імена, email і номери карток. Не ввімкнути Preserve log, і тоді після перенаправлення потрібні запити зникнуть. Записувати HAR на проді під реальним клієнтським акаунтом.
Pitfalls. Attaching a full HAR with a live session: anyone with access to the ticket can hijack the account while the cookies are valid. Forgetting that response bodies contain names, emails and card numbers. Not enabling Preserve log, so the relevant requests vanish after a redirect. Recording a HAR on production under a real customer account.
Уточнююче питання. «HAR чи скриншот вкладки Network?» HAR, бо його можна фільтрувати, шукати й відкрити повністю. Скриншот показує лише те, що вмістилося на екран, і не має тіл відповідей.
Follow-up question. "HAR or a screenshot of the Network tab?" A HAR, because it can be filtered, searched and opened in full. A screenshot shows only what fit on the screen and has no response bodies.
{ "log": { "entries": [
{ "startedDateTime": "2024-06-11T08:14:02.118Z", "time": 2104,
"request": { "method": "GET", "url": "https://bank.example/api/statements?month=2024-05",
"headers": [ { "name": "Authorization", "value": "Bearer eyJhbGciOi..." } ] },
"response": { "status": 200, "content": { "text": "{\"month\":\"2024-05\",\"owner\":\"Olena K.\"}" } } },
{ "startedDateTime": "2024-06-11T08:14:02.301Z", "time": 412,
"request": { "method": "GET", "url": "https://bank.example/api/statements?month=2024-06" },
"response": { "status": 200 } } ] } }
# strip auth headers and cookies before attaching:
$ jq '.log.entries[] |= (
.request.headers |= map(select(.name | test("^(authorization|cookie)$"; "i") | not))
| .response.headers |= map(select(.name | test("^set-cookie$"; "i") | not))
| del(.request.cookies, .response.cookies))' statements.har > statements.clean.har
$ grep -ci "bearer\|set-cookie" statements.clean.har # expect 0
month=2024-05) почався раніше, але тривав 2104 мс, новіший — 412 мс, тож відповідь на старий прийшла останньою. Команда jq проходить по всіх entries, відфільтровує заголовки Authorization, Cookie і Set-Cookie та видаляє масиви cookies. Фінальний grep -ci перевіряє, що токенів не залишилося; тіла відповідей з іменами треба перевірити окремо.month=2024-05) started first but took 2104 ms, the newer one 412 ms, so the old response arrived last. The jq command walks every entries item, filters out the Authorization, Cookie and Set-Cookie headers and deletes the cookies arrays. The final grep -ci checks that no tokens are left; response bodies with names still need a separate check.Що це. Найкорисніше — не весь лог, а ключ кореляції: request id (часто заголовок x-request-id) або trace id з W3C-заголовка traceparent, плюс точний час запиту в UTC. З цим ключем розробник знаходить у системі логів (Kibana, Grafana Loki, Datadog) усі записи всіх сервісів, через які пройшов запит. До бага прикладають кілька рядків навколо помилки і посилання на збережений запит у системі логів, а не мегабайти тексту.
What it is. The most useful thing is not the whole log but the correlation key: a request id (often the x-request-id header) or the trace id from the W3C traceparent header, plus the exact request time in UTC. With that key the developer finds every record from every service the request passed through in the log system (Kibana, Grafana Loki, Datadog). The bug gets a few lines around the error and a link to the saved log query, not megabytes of text.
Навіщо. У мікросервісній системі один клік користувача зачіпає п’ять-десять сервісів. Без trace id пошук схожий на пошук голки в копиці: «десь о 14:00 щось упало». З trace id розробник одразу бачить ланцюжок і сервіс, який повернув помилку першим, навіть якщо користувачу показали загальне «Щось пішло не так».
Why it matters. In a microservice system one user click touches five to ten services. Without a trace id the search is a needle in a haystack: "something failed around 14:00". With a trace id the developer sees the whole chain and the service that failed first, even when the user only saw a generic "Something went wrong".
Приклад з проєкту. На маркетплейсі оформлення замовлення інколи закінчувалося помилкою 502. Фронтенд показував загальне повідомлення, gateway писав лише «upstream timeout». Я взяла traceparent з відповіді і знайшла трейс у Grafana Tempo: запит у сервіс складу чекав на блокування в базі 30 секунд. У репорті були trace id, три рядки логу складу і скриншот трейсу, і команда складу взяла баг без жодного перепитування.
Example from a project. On a marketplace, checkout sometimes ended in a 502. The frontend showed a generic message and the gateway logged only "upstream timeout". I took traceparent from the response and found the trace in Grafana Tempo: the call to the inventory service waited 30 seconds on a database lock. The report had the trace id, three lines of the inventory log and a screenshot of the trace, and the inventory team picked the bug up without a single question.
Підводні камені. Копіювати в баг сотні рядків логів без виділення важливого. Прикладати логи з персональними даними і токенами без очищення. Шукати в логах за часом без поясу: «14:05» за Києвом і за UTC — різниця у три години. Посилатися на запит у системі логів, який зникне через сім днів ротації, — важливі рядки варто скопіювати в тікет.
Pitfalls. Pasting hundreds of log lines into the bug with nothing highlighted. Attaching logs with personal data and tokens without cleaning them. Searching logs by time without a zone: "14:05" in Kyiv and in UTC are three hours apart. Linking to a log query that will disappear after seven days of retention — copy the important lines into the ticket.
Уточнююче питання. «Що робити, якщо у QA немає доступу до логів?» Приносити розробнику request id і час — навіть без доступу цього достатньо. А в довгостроковій перспективі просити read-only доступ до логів тестових стендів: це окупається на першому ж складному багу.
Follow-up question. "What if QA has no access to the logs?" Bring the developer the request id and the time — that is enough even without access. In the long run, ask for read-only access to test environment logs; it pays off on the very first hard bug.
$ curl -s -D - -o /dev/null -X POST https://staging.market.example/api/checkout \
-H "Authorization: Bearer <REDACTED>" -d @order.json | grep -i -E "^(HTTP|traceparent|x-request-id)"
HTTP/2 502
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
x-request-id: 9e1d3a52-6c0f-4b8e-a1f4-0c77d2b5e913
# Loki / LogQL query for the report:
{env="staging", app=~"gateway|checkout|inventory"} |= "4bf92f3577b34da6a3ce929d0e0e4736"
# Lines to paste into the ticket (UTC):
2024-06-18T11:05:12.004Z gateway ERROR upstream timeout after 30000ms route=/api/checkout
2024-06-18T11:04:42.010Z inventory WARN lock wait on stock_items id=88412 waited=29994ms
2024-06-18T11:04:41.998Z checkout INFO reserve stock sku=88412 qty=1
-D - друкує заголовки відповіді, -o /dev/null відкидає тіло, grep залишає статус і ключі кореляції. У traceparent друге поле — 32-символьний trace id. Запит LogQL фільтрує потрібні сервіси за міткою app і шукає рядки з цим trace id. Унизу — три рядки в UTC, які пояснюють ланцюжок: резерв, очікування блокування, таймаут на gateway.-D - prints the response headers, -o /dev/null discards the body, grep keeps the status and the correlation keys. In traceparent the second field is the 32-character trace id. The LogQL query filters the relevant services by the app label and searches for lines with that trace id. At the bottom are three UTC lines that explain the chain: reserve, lock wait, gateway timeout.Що це. Багато багів відтворюються лише на конкретних даних: замовлення в певному статусі, користувач з двома адресами, товар з нульовим залишком. У репорті такі дані описують так, щоб їх можна було створити заново: SQL-запит, виклик API, фікстура або ідентифікатор у спільному наборі даних. Аркуш тестових даних (test data sheet) — це спільний документ або файл із переліком підготовлених акаунтів, карток, SKU та їхніх властивостей, щоб уся команда тестувала на однакових і відомих даних.
What it is. Many bugs reproduce only on specific data: an order in a certain status, a user with two addresses, a product with zero stock. The report describes such data so it can be recreated: an SQL query, an API call, a fixture or an id in a shared data set. A test data sheet is a shared document or file listing the prepared accounts, cards, SKUs and their properties, so the whole team tests on the same known data.
Навіщо. Тестові стенди регулярно перезаливають, а дані змінюють інші тестувальники. Якщо в багу написано лише «мій акаунт», через тиждень відтворити його неможливо. Скрипт або фікстура роблять баг відтворюваним будь-коли, а аркуш тестових даних прибирає питання «на якому юзері це перевіряти» і дає готові дані для автотестів.
Why it matters. Test environments get reset regularly and other testers change the data. If the bug only says "my account", nobody can reproduce it a week later. A script or fixture makes the bug reproducible at any time, and a test data sheet removes the "which user should I check this on" question and gives automated tests ready data.
Приклад з проєкту. У страховому сервісі перерахунок премії падав тільки для полісів, продовжених двічі з різними тарифами. Замість «поліс 55120 на staging» я додала SQL-запит, що знаходить такі поліси, і JSON-фікстуру для API, яка створює новий. Після перезаливки стенду розробник створив поліс за хвилину, а фікстура пізніше стала частиною регресійного набору. Сам поліс я додала в аркуш тестових даних з позначкою «двічі продовжений, тарифи A→B».
Example from a project. In an insurance service, premium recalculation failed only for policies renewed twice with different tariffs. Instead of "policy 55120 on staging" I added an SQL query that finds such policies and a JSON fixture for the API that creates a fresh one. After the environment was reset the developer created a policy in a minute, and the fixture later joined the regression suite. I added the policy itself to the test data sheet, marked "renewed twice, tariffs A→B".
Підводні камені. Використовувати копію продакшн-даних з реальними іменами, телефонами і номерами документів — це порушення GDPR і внутрішніх політик. Посилатися на ID, які зникнуть після перезаливки стенду. Тримати аркуш тестових даних в особистій таблиці, яку ніхто інший не оновлює. Ділити одні й ті самі тестові акаунти між паралельними автотестами — вони псують стан одне одному.
Pitfalls. Using a copy of production data with real names, phone numbers and document numbers — that breaches GDPR and internal policies. Referring to ids that will vanish when the environment is reset. Keeping the test data sheet in a personal spreadsheet nobody else updates. Sharing the same test accounts between parallel autotests — they corrupt each other's state.
Уточнююче питання. «Звідки брати реалістичні дані без персональних?» Генератори на кшталт Faker, маскування продакшн-дампу з заміною персональних полів, або seed-скрипти, які створюють набір даних з нуля для кожного стенду.
Follow-up question. "Where do you get realistic data without personal data?" Generators such as Faker, a masked production dump with personal fields replaced, or seed scripts that build the data set from scratch for every environment.
-- find policies renewed twice with a tariff change (staging-3)
SELECT p.id, p.status, array_agg(r.tariff ORDER BY r.renewed_at) AS tariffs
FROM policies p
JOIN renewals r ON r.policy_id = p.id
GROUP BY p.id, p.status
HAVING count(*) = 2 AND count(DISTINCT r.tariff) = 2
LIMIT 5;
// fixture: POST /api/test-data/policies (staging only)
{ "holder": "qa.synthetic+ren2@example.com",
"renewals": [ { "tariff": "A", "date": "2023-01-10" },
{ "tariff": "B", "date": "2024-01-10" } ] }
# test-data sheet row
id: POL-REN2-AB | env: staging-* | holder: qa.synthetic+ren2 | renewed twice, A->B | owner: QA
GROUP BY і HAVING: рівно два продовження з різними тарифами. JSON-фікстура створює такий поліс з нуля через службовий endpoint, доступний лише на тестових стендах. Останній рядок — запис в аркуші тестових даних зі стабільним ідентифікатором і синтетичним email.GROUP BY and HAVING: exactly two renewals with different tariffs. The JSON fixture creates such a policy from scratch through a service endpoint available only on test environments. The last line is the test data sheet entry with a stable id and a synthetic email.Що це. Життєвий цикл бага — це послідовність статусів у трекері. Типовий шлях: New/Open → Triaged (оцінено й призначено) → In Progress → In Review (код-рев’ю) → Ready for QA/Resolved → Verified → Closed. Відгалуження: Reopened, якщо ретест не пройшов, і закриття з резолюцією Duplicate, Cannot Reproduce, Works as Designed, Won’t Fix. У Jira важливо розрізняти статус (де зараз тікет) і резолюцію (чим він закінчився).
What it is. A bug's life cycle is the sequence of statuses in the tracker. A typical path: New/Open → Triaged (assessed and assigned) → In Progress → In Review (code review) → Ready for QA/Resolved → Verified → Closed. Branches: Reopened when the retest fails, and closure with a Duplicate, Cannot Reproduce, Works as Designed or Won't Fix resolution. In Jira it matters to tell the status (where the ticket is now) from the resolution (how it ended).
Навіщо. Статус показує, хто зараз відповідає за баг і що має статися далі. Без чіткого циклу баги застрягають: розробник вважає, що виправив, QA про це не знає, а в релізі баг досі є. Правильні резолюції також важливі для метрик: баг, закритий як Duplicate, не має рахуватися як виправлений дефект.
Why it matters. The status shows who owns the bug right now and what should happen next. Without a clear cycle bugs get stuck: the developer thinks it is fixed, QA does not know, and the bug is still in the release. Correct resolutions matter for metrics too: a bug closed as Duplicate should not count as a fixed defect.
Приклад з проєкту. На проєкті ми помітили, що в колонці «Resolved» висить сорок багів, які ніхто не перевіряв тижнями: розробники переводили їх туди, а QA не отримувала сповіщень. Ми додали в workflow статус «Ready for QA» з автоматичним призначенням на QA компонента і JQL-фільтр на дашборді. За спринт черга зменшилася до п’яти-шести тікетів.
Example from a project. On one project we noticed forty bugs sitting in "Resolved" that nobody had checked for weeks: developers moved them there, and QA got no notification. We added a "Ready for QA" status to the workflow with automatic assignment to the component's QA and a JQL filter on the dashboard. Within a sprint the queue shrank to five or six tickets.
Підводні камені. Закривати баг без ретесту, просто тому що розробник поставив Resolved. Використовувати статус замість резолюції: «Closed» без резолюції не пояснює, чи баг виправили, чи відхилили. Workflow із двадцятьма статусами, в якому ніхто не розуміє різниці між «Done», «Resolved» і «Closed».
Pitfalls. Closing a bug without a retest just because the developer set Resolved. Using a status instead of a resolution: "Closed" with no resolution does not say whether the bug was fixed or rejected. A workflow with twenty statuses where nobody knows the difference between "Done", "Resolved" and "Closed".
Уточнююче питання. «Хто переводить баг у Closed?» У більшості команд — QA після успішного ретесту, або автоматично після релізу, якщо баг перевірено. Автор бага також має погодитися з резолюціями на кшталт Works as Designed.
Follow-up question. "Who moves a bug to Closed?" In most teams QA does after a successful retest, or it happens automatically after release if the bug was verified. The bug's author should also agree with resolutions like Works as Designed.
New --> Triaged --> In Progress --> In Review --> Ready for QA --> Verified --> Closed
| | | (Fixed)
| | +--> Reopened --> In Progress
| +--> Closed (Won't Fix | Works as Designed | Duplicate)
+--> Need Info --> New (reporter adds missing details)
+--> Closed (Cannot Reproduce, after 14 days without an answer)
Що це. Перевірка на дубль — це пошук у трекері за ключовими словами симптому, модулем, текстом помилки і схожими кроками, включно із закритими тікетами. У Jira для цього є JQL з оператором ~ (повнотекстовий пошук) і фільтри за компонентом. Якщо схожий баг знайдено, ти не створюєш новий, а додаєш у наявний свої кроки, оточення і частоту — це теж цінні дані.
What it is. Checking for duplicates means searching the tracker for the symptom's keywords, the module, the error text and similar steps, closed tickets included. In Jira that means JQL with the ~ operator (full-text search) and component filters. If a similar bug turns up, you do not create a new one; you add your steps, environment and rate to the existing one — that is valuable data too.
Навіщо. Дублі розпорошують інформацію: один розробник виправляє баг, другий паралельно дебажить той самий у сусідньому тікеті. Вони також спотворюють метрики і захаращують тріаж. Коментар «відтворилося ще на Android 14 і в Safari» до наявного бага показує масштаб проблеми краще, ніж три окремі тікети.
Why it matters. Duplicates scatter information: one developer fixes the bug while another debugs the same thing in a neighbouring ticket. They also skew the metrics and clutter triage. A comment like "also reproduces on Android 14 and in Safari" on the existing bug shows the scale of the problem better than three separate tickets.
Приклад з проєкту. Перед репортом падіння при завантаженні аватарки я шукала за текстом помилки «Payload Too Large» і знайшла закритий тікет піврічної давності з резолюцією Fixed. Замість нового бага я перевідкрила його з коментарем: ліміт 5 МБ знову не працює, бо після міграції на новий gateway змінилося обмеження розміру тіла. Розробник одразу побачив попередній фікс і зрозумів, де шукати.
Example from a project. Before reporting a crash on avatar upload I searched for the error text "Payload Too Large" and found a ticket closed as Fixed six months earlier. Instead of a new bug I reopened it with a comment: the 5 MB limit is broken again, because the body size limit changed after the migration to a new gateway. The developer saw the earlier fix straight away and knew where to look.
Підводні камені. Шукати лише серед відкритих тікетів: баг міг бути закритий як Fixed і повернутися. Шукати за своїм формулюванням: інша людина могла назвати симптом по-іншому, тому варто пробувати текст помилки, URL endpoint або назву компонента. Позначати як дубль тікет, який лише схожий, але має іншу причину.
Pitfalls. Searching only open tickets: the bug may have been closed as Fixed and come back. Searching only with your own wording: someone else may have described the symptom differently, so try the error text, the endpoint URL or the component name. Marking a ticket as a duplicate when it is merely similar but has a different cause.
Уточнююче питання. «Що робити, якщо знайдений тікет лише схожий?» Створюю новий баг і зв’язую його з тим типом «relates to», пояснивши різницю. Тріаж вирішить, чи це одна причина.
Follow-up question. "What if the ticket you found is only similar?" I create a new bug and link it to the other one with "relates to", explaining the difference. Triage will decide whether it is the same cause.
# by error text, any status, last 12 months
project = SHOP AND issuetype = Bug AND text ~ "\"Payload Too Large\"" AND created >= -52w
# by component and endpoint, including closed ones
project = SHOP AND component = "Profile" AND text ~ "avatar upload" ORDER BY updated DESC
# recently fixed bugs in the same area (possible regression)
project = SHOP AND component = "Profile" AND resolution = Fixed AND resolved >= -90d
text ~ шукає по заголовку, опису й коментарях; екрановані лапки всередині роблять пошук точної фрази. created >= -52w обмежує період. Третій запит шукає нещодавно виправлені баги в тому ж компоненті: якщо симптом збігається, це, ймовірно, регресія.text ~ searches the summary, description and comments; the escaped quotes inside make it an exact phrase search. created >= -52w limits the period. The third query looks for recently fixed bugs in the same component: if the symptom matches, it is probably a regression.Що це. Тріаж — регулярний розбір нових багів, на якому команда для кожного вирішує: чи це справді баг, яка severity і priority, хто власник, у який спринт чи реліз він іде, чи бракує інформації. Зазвичай у ньому беруть участь QA, тимлід розробки і PO; тривалість — 15–30 хвилин, частота — від щоденної в передрелізний період до щотижневої.
What it is. Triage is a regular review of new bugs where the team decides for each one: is it really a bug, what are its severity and priority, who owns it, which sprint or release it goes into, is information missing. Participants are usually QA, the engineering lead and the PO; it lasts 15–30 minutes and runs anywhere from daily before a release to weekly.
Навіщо. Без тріажу баги накопичуються в New, пріоритети виставляє той, хто створив тікет, а критичні проблеми губляться серед косметики. Тріаж дає одну точку, де технічна оцінка зустрічається з бізнесовою, і кожен баг виходить з неї з власником і рішенням.
Why it matters. Without triage bugs pile up in New, priorities are set by whoever created the ticket, and critical problems get lost among cosmetics. Triage is the one place where the technical assessment meets the business one, and every bug leaves it with an owner and a decision.
Приклад з проєкту. На проєкті з двома командами ми проводили тріаж щовівторка й щоп’ятниці по 20 хвилин. Я готувала фільтр нових багів, відсортованих за severity, і заздалегідь перевіряла кожен: чи відтворюється на свіжому build, чи немає дубля, чи заповнені поля. Тікети з браком даних ішли в Need Info ще до зустрічі. Завдяки цьому на самій зустрічі ми встигали розібрати 25–30 багів замість 10.
Example from a project. On a project with two teams we ran triage every Tuesday and Friday for 20 minutes. I prepared a filter of new bugs sorted by severity and checked each one in advance: does it reproduce on the fresh build, is it a duplicate, are the fields filled in. Tickets missing data went to Need Info before the meeting. Thanks to that we got through 25–30 bugs per session instead of 10.
Підводні камені. Перетворювати тріаж на дебаг: якщо обговорення триває понад дві хвилини, баг отримує власника, а деталі обговорюють окремо. Приносити непідготовлені тікети, які доводиться читати вперше на зустрічі. Не фіксувати рішення в тікеті — через тиждень ніхто не пам’ятає, чому баг відклали.
Pitfalls. Turning triage into debugging: if a discussion runs over two minutes, the bug gets an owner and the details are discussed separately. Bringing unprepared tickets that people read for the first time in the meeting. Not recording the decision in the ticket — a week later nobody remembers why the bug was deferred.
Уточнююче питання. «Хто веде тріаж?» Часто QA або тимлід. Важливо, щоб на зустрічі був хтось із правом ухвалювати рішення про priority, інакше половина багів залишиться «обговорити з PO».
Follow-up question. "Who runs triage?" Often QA or the team lead. What matters is that someone in the room can decide on priority, otherwise half the bugs end up as "discuss with the PO".
# Triage queue: new bugs, not yet triaged (cf[10021] = Severity custom field)
project in (SHOP, PAY) AND issuetype = Bug AND status = New
ORDER BY cf[10021] ASC, created ASC
# Agenda (20 min)
# 1. Blockers/Critical first (2 min each max)
# 2. For every bug: valid? -> severity/priority -> owner -> fix version
# 3. Missing info -> status "Need Info" + comment to reporter
# 4. Decision recorded in the ticket as a comment:
# "Triage 2024-06-14: Major/High, team Payments, fixVersion 4.13.0"
cf[10021] — так у JQL звертаються до кастомного поля за його ID (тут це severity). Порядок денний задає ліміт часу на обговорення і формат коментаря з рішенням, щоб воно залишилось у тікеті.cf[10021] is how JQL refers to a custom field by its id (severity here). The agenda sets a time limit per discussion and a format for the decision comment so it stays in the ticket.Що це. Works as Designed (WAI, Not a Bug) — резолюція для тікета, де описана поведінка відповідає вимогам, дизайну або свідомому технічному рішенню. Коректне закриття з такою резолюцією завжди містить посилання на джерело: пункт вимоги, макет, ADR або рішення продукту. Без посилання це не резолюція, а думка.
What it is. Works as Designed (WAI, Not a Bug) is the resolution for a ticket whose described behaviour matches the requirements, the design or a deliberate technical decision. A proper closure with this resolution always links the source: a requirement item, a mockup, an ADR or a product decision. Without a link it is not a resolution, it is an opinion.
Навіщо. Такі закриття показують, де розходяться очікування QA і реальні вимоги, а це сигнал про прогалину в документації або в розумінні продукту. Якщо QA мовчки погоджується, та сама «проблема» повертається від клієнтів. Якщо сперечається без аргументів, псуються стосунки з розробкою.
Why it matters. These closures show where QA's expectations and the actual requirements diverge, which signals a gap in the documentation or in the understanding of the product. If QA silently agrees, the same "problem" comes back from customers. If QA argues without arguments, the relationship with engineering suffers.
Приклад з проєкту. Я репортила, що при видаленні проєкту його задачі не потрапляють у кошик, а зникають назавжди. Розробник закрив як Works as Designed. Я попросила посилання, і PO показав рішення в історії PRJ-88: видалення проєкту незворотне, користувач бачить підтвердження з назвою проєкту. Поведінка справді була задокументована, тож я погодилася, але створила improvement: у діалозі не було видно, скільки задач буде видалено, і в підтримці таких скарг було чимало.
Example from a project. I reported that deleting a project removed its tasks for good instead of moving them to the trash. The developer closed it as Works as Designed. I asked for the source, and the PO pointed to the decision in story PRJ-88: deleting a project is irreversible and the user sees a confirmation with the project name. The behaviour really was documented, so I agreed, but I filed an improvement: the dialog did not say how many tasks would be deleted, and support had plenty of complaints about it.
Підводні камені. Сприймати WAI як особисту поразку і перевідкривати тікет без нових фактів. Погоджуватися, коли посилання немає, а є лише «ми так вирішили» в особистих повідомленнях. Не оновити тест-кейси після рішення: якщо поведінка задумана, очікуваний результат у тестах треба змінити, інакше той самий «баг» зарепортять знову.
Pitfalls. Taking WAI as a personal defeat and reopening the ticket without new facts. Agreeing when there is no link, just "we decided so" in a private message. Not updating the test cases after the decision: if the behaviour is intended, the expected result in the tests must change, or the same "bug" will be reported again.
Уточнююче питання. «Що робиш, якщо не згодна з рішенням продукту?» Фіксую аргументи і дані в тікеті, пропоную improvement, але рішення продукту поважаю. Моє завдання — щоб рішення було ухвалене свідомо, а не щоб завжди виходило по-моєму.
Follow-up question. "What do you do if you disagree with the product decision?" I record the arguments and data in the ticket and propose an improvement, but I respect the product decision. My job is to make sure the decision is made consciously, not to always get my way.
# weak closure
Resolution: Works as Designed
Comment: "This is expected."
# good closure
Resolution: Works as Designed
Comment: Per PRJ-88 (AC #2) project deletion is irreversible by design;
the confirmation dialog shows the project name. See ADR-014 on why
tasks are not soft-deleted (storage and GDPR erasure).
# QA follow-up
Agreed, closing. Created PRJ-231 (Improvement): show the number of tasks
in the delete dialog - 17 support tickets in May mention lost tasks.
Updated TC-411 expected result to match PRJ-88.
Що це. Won’t Fix — свідоме рішення не виправляти підтверджений дефект. Воно виправдане, коли вартість виправлення перевищує шкоду: платформа виходить з підтримки, функцію скоро замінять, баг проявляється в нереалістичному сценарії, фікс несе більший ризик, ніж сам баг, або є прийнятний обхідний шлях. На відміну від Works as Designed, тут усі погоджуються, що поведінка неправильна.
What it is. Won't Fix is a deliberate decision not to fix a confirmed defect. It is justified when fixing costs more than the harm: the platform is going out of support, the feature will soon be replaced, the bug shows up only in an unrealistic scenario, the fix is riskier than the bug itself, or there is an acceptable workaround. Unlike Works as Designed, everyone agrees the behaviour is wrong.
Навіщо. Беклог без Won’t Fix розростається до тисяч тікетів, які ніхто не виправить, і реальні проблеми в ньому губляться. Водночас кожен Won’t Fix — це прийнятий ризик, і його треба зафіксувати так, щоб через рік можна було зрозуміти, хто, коли і чому його прийняв, і за якої умови рішення слід переглянути.
Why it matters. A backlog without Won't Fix grows to thousands of tickets nobody will ever fix, and real problems get lost in it. At the same time every Won't Fix is an accepted risk, and it must be recorded so that a year later anyone can see who accepted it, when and why, and under which condition the decision should be revisited.
Приклад з проєкту. У застосунку для складу старий модуль друку етикеток некоректно обрізав назви довші за 60 символів. Модуль мали замінити через два місяці, а фікс вимагав змін у сторонній бібліотеці. Ми закрили баг як Won’t Fix з обхідним шляхом (скорочені назви в довіднику), посиланням на епік заміни модуля і умовою перегляду: «якщо заміну відкладуть після Q3». Коли заміну справді відклали, фільтр нагадав про тікет, і його перевідкрили.
Example from a project. In a warehouse app, the legacy label printing module truncated names longer than 60 characters. The module was due to be replaced in two months, and the fix required changes in a third-party library. We closed the bug as Won't Fix with a workaround (shortened names in the catalogue), a link to the replacement epic and a review condition: "if the replacement slips past Q3". When it did slip, the filter surfaced the ticket and it was reopened.
Підводні камені. Використовувати Won’t Fix як спосіб «почистити» беклог перед звітом. Закривати без рішення відповідальної людини або без пояснення. Не записати обхідний шлях для підтримки. Не залишити тест на цей сценарій позначеним як відомий дефект — тоді автотест або падає щодня, або його видаляють і забувають про ризик.
Pitfalls. Using Won't Fix to "clean up" the backlog before a report. Closing without a decision from the accountable person or without an explanation. Not recording the workaround for support. Not marking the test for this scenario as a known defect — then the autotest either fails every day or gets deleted and the risk is forgotten.
Уточнююче питання. «Як переконатися, що Won’t Fix не забудуть?» Мітка на кшталт accepted-risk, дата або умова перегляду в тікеті, і збережений JQL-фільтр, який раз на квартал показує такі тікети на ревізію. Відомі дефекти також потрапляють у release notes як known issues.
Follow-up question. "How do you make sure a Won't Fix is not forgotten?" A label such as accepted-risk, a review date or condition in the ticket, and a saved JQL filter that brings these tickets up for review every quarter. Known defects also go into the release notes as known issues.
Resolution: Won't Fix
Labels: accepted-risk, known-issue
Comment:
Decision: @olena.pm (PO), 2024-04-09, triage
Reason: legacy label module is replaced by WMS-900 (planned 2024-06);
fix needs a patch in a vendor library, estimated 5 days
Impact: names > 60 chars truncated on labels, ~2% of SKUs
Workaround: use the "Short name" field (support article KB-212)
Review: reopen if WMS-900 moves beyond 2024-Q3
Tests: TC-733 marked as known defect, linked here
# quarterly review of accepted risks
project = WMS AND resolution = "Won't Fix" AND labels = accepted-risk
AND resolved <= -90d ORDER BY resolved ASC
accepted-risk і known-issue роблять тікет знаходжуваним, а JQL раз на квартал піднімає старі рішення на перегляд.accepted-risk and known-issue labels make the ticket findable, and the JQL brings old decisions up for review every quarter.Що це. Після фіксу QA робить ретест (confirmation testing): проходить оригінальні кроки на білді, де є виправлення, і перевіряє, що actual тепер збігається з expected. Далі — перевірка навколо: суміжні сценарії, інші дані, граничні значення, інші браузери з репорту, бо фікс міг зачепити сусідню логіку. Якщо все добре — баг переходить у Verified/Closed із коментарем, де і як перевірено; якщо ні — Reopened з новими фактами.
What it is. After a fix QA does a retest (confirmation testing): walks the original steps on the build containing the fix and checks that actual now matches expected. Then comes checking around it: neighbouring scenarios, other data, boundary values, the other browsers from the report, because the fix may have touched nearby logic. If all is well, the bug moves to Verified/Closed with a comment saying where and how it was checked; if not, it goes to Reopened with new facts.
Навіщо. Розробник перевіряє свій фікс на своїх даних і у своєму оточенні, і цього часто недостатньо. Ретест у тому оточенні, де баг знайшли, підтверджує, що виправлено саме той симптом. Перевірка навколо ловить регресії, які фікс міг спричинити. Коментар із build і переліком перевірок — доказ для релізу.
Why it matters. The developer checks the fix on their own data and in their own environment, which is often not enough. A retest in the environment where the bug was found confirms that exactly that symptom is fixed. Checking around it catches regressions the fix may have caused. A comment with the build and the list of checks is evidence for the release.
Приклад з проєкту. Баг: промокод не застосовувався до кошика з уцінованим товаром. Після фіксу оригінальний сценарій пройшов, але я перевірила суміжні: два промокоди поспіль, промокод з фіксованою сумою замість відсотка, кошик лише з уціненими товарами. Останній випадок дав знижку на повну ціну замість ціни зі знижкою, тобто подвійну знижку. Оригінальний баг я закрила, а новий створила й зв’язала з ним.
Example from a project. The bug: a promo code was not applied to a cart with a sale item. After the fix the original scenario passed, but I checked the neighbours: two promo codes in a row, a fixed-amount code instead of a percentage, a cart with only sale items. The last one applied the discount to the full price instead of the sale price, so a double discount. I closed the original bug and created a new one linked to it.
Підводні камені. Перевіряти на білді, де фіксу ще немає, — треба звірити build з полем fix version або з комітом у тікеті. Перевідкривати баг, коли знайдено іншу проблему в тому самому місці: це новий баг з посиланням. Закривати без коментаря — через місяць ніхто не знатиме, що саме перевірили.
Pitfalls. Checking on a build that does not contain the fix yet — compare the build with the fix version field or the commit in the ticket. Reopening a bug when a different problem shows up in the same place: that is a new bug with a link. Closing without a comment — a month later nobody will know what was checked.
Уточнююче питання. «Коли перевідкрити, а коли створити новий баг?» Перевідкриваю, якщо оригінальні кроки досі дають той самий неправильний результат. Якщо кроки проходять, але проявилася інша проблема, — новий баг, зв’язаний з оригінальним.
Follow-up question. "When do you reopen and when do you file a new bug?" I reopen if the original steps still produce the same wrong result. If the steps pass but a different problem appears, it is a new bug linked to the original.
# Verified
Verified on staging-2, web 4.12.3 (commit 5b7e0aa), Chrome 129 + Safari 17.5
[x] original steps: SPRING10 on mixed cart -> total 1,080 (was 1,200)
[x] fixed-amount code SAVE100 on mixed cart -> 1,100
[x] two codes in a row -> second rejected with "Only one promo per order"
[ ] cart with sale items only -> see SHOP-2207 (new bug, double discount)
Autotest added: cart-promo.spec.ts > "promo applies to mixed cart"
# Reopened
Still reproduces on web 4.12.3 (5b7e0aa) for guest checkout:
POST /api/cart/promo -> 422 PROMO_NOT_APPLICABLE (x-request-id 1c9f...e02).
Logged-in flow is fixed. Video: promo-guest-422.mp4
Що це. Регресійний баг — поломка поведінки, яка раніше працювала. Ознаки: функція не змінювалася за вимогами, але поводиться інакше, ніж у попередньому релізі; або вже був виправлений баг із тим самим симптомом. У репорті це показують явно: мітка regression, остання робоча версія і перша зламана, посилання на попередній баг, якщо він був, і, якщо можливо, запис чи скриншот зі старої версії.
What it is. A regression bug breaks behaviour that used to work. The signs: the feature has not changed in the requirements but behaves differently from the previous release, or a bug with the same symptom was already fixed once. The report shows this explicitly: a regression label, the last working version and the first broken one, a link to the earlier bug if there was one, and, if possible, a recording or screenshot from the old version.
Навіщо. Регресії зазвичай отримують вищий priority: користувачі вже звикли до робочої функції, і її поломка сприймається гірше, ніж відсутність нової. Діапазон версій дає розробнику короткий список змін для аналізу. Статистика регресій показує слабкі місця в автоматизованому регресійному наборі й у процесі рев’ю.
Why it matters. Regressions usually get higher priority: users are used to a working feature, and its breakage feels worse than a missing new one. The version range gives the developer a short list of changes to analyse. Regression statistics expose weak spots in the automated regression suite and in code review.
Приклад з проєкту. Після релізу 5.2 у мобільному застосунку перестав працювати вхід через Face ID для користувачів, що оновилися з 5.0. Я встановила 5.1 з архіву збірок і переконалася, що там усе працює, а в 5.2 — ні, і лише при оновленні, не при чистій установці. У репорті: мітка regression, «працює в 5.1.3, зламано в 5.2.0», «лише при оновленні з 5.x». Розробник одразу знайшов міграцію сховища ключів, яка не переносила прапорець біометрії.
Example from a project. After release 5.2 of a mobile app, Face ID login stopped working for users who upgraded from 5.0. I installed 5.1 from the build archive and confirmed it worked there but not in 5.2, and only after an upgrade, not on a clean install. The report said: regression label, "works in 5.1.3, broken in 5.2.0", "only when upgrading from 5.x". The developer immediately found a keychain migration that did not carry over the biometrics flag.
Підводні камені. Називати регресією будь-який баг у старій функції — якщо його просто раніше не помічали, це не регресія, а давній дефект. Не перевірити попередню версію, коли є така можливість. Не додати після фіксу автотест, і тоді регресія повертається через пів року.
Pitfalls. Calling any bug in an old feature a regression — if nobody had noticed it before, it is a long-standing defect, not a regression. Not checking the previous version when you can. Not adding an autotest after the fix, so the regression comes back six months later.
Уточнююче питання. «Як перевірити попередню версію, якщо стенд уже оновлено?» Архів збірок (TestFlight, Firebase App Distribution, артефакти CI), окремий стенд для попереднього релізу або локальний запуск потрібного тегу в Docker. Для вебу інколи достатньо feature flag, що вмикає старий код.
Follow-up question. "How do you check the previous version if the environment is already upgraded?" A build archive (TestFlight, Firebase App Distribution, CI artefacts), a separate environment for the previous release, or running the right tag locally in Docker. For web, sometimes a feature flag that turns the old code back on is enough.
Title: [iOS][Login] Face ID login fails after upgrading from 5.0/5.1 to 5.2.0
Labels: regression, biometrics
Affects version: 5.2.0 Last known good: 5.1.3
Scope: upgrade path only; clean install of 5.2.0 works
Steps:
1. Install 5.1.3 from TestFlight, log in, enable Face ID in Settings
2. Upgrade to 5.2.0 (TestFlight build 5.2.0 (812))
3. Kill the app, open it, tap "Log in with Face ID"
Expected: Face ID prompt, user logged in (as in 5.1.3, video attached)
Actual: "Biometric login is not set up", falls back to password
Related: MOB-1402 (Face ID fix in 4.9, same symptom after keychain change)
Affects version і Last known good задають діапазон, Scope звужує умову до сценарію оновлення. Expected посилається на відео з попередньої версії, а Related — на давній баг із тим самим симптомом, що підказує, де шукати.Affects version and Last known good fields set the range, and Scope narrows the condition to the upgrade path. Expected points to a video from the previous version, and Related points to an old bug with the same symptom, which hints where to look.Що це. «Не відтворюється» майже завжди означає, що оточення розробника і тестувальника відрізняються чимось, чого немає в репорті. Робота QA — знайти цю різницю: build, дані, роль, feature flags, кеш і service worker, мова, часовий пояс, мережа, розширення браузера, паралельні дії. Найшвидший шлях — відтворити разом: спільний екран на 10 хвилин або відтворення на стенді розробника за його участі.
What it is. "Cannot reproduce" almost always means the developer's and tester's environments differ in something the report does not mention. QA's job is to find that difference: build, data, role, feature flags, cache and service worker, language, time zone, network, browser extensions, parallel actions. The fastest route is to reproduce it together: a ten-minute screen share or a reproduction on the developer's environment with them watching.
Навіщо. Кожне повернення тікета з Cannot Reproduce коштує день-два, а баг тим часом залишається в продукті. Часто саме ці «невідтворювані» баги потім приходять від клієнтів. Систематичний пошук різниці перетворює суперечку «у тебе щось не так» на конкретний факт, який додають у репорт.
Why it matters. Every Cannot Reproduce bounce costs a day or two while the bug stays in the product. These "unreproducible" bugs are often the ones that later come in from customers. A systematic search for the difference turns the "something's wrong on your side" argument into a concrete fact that goes into the report.
Приклад з проєкту. Баг: після зміни мови на польську частина кнопок залишалася англійською. Розробник не відтворив. Я пройшла чек-лист і знайшла різницю: у мене був активний service worker зі старою версією файлів перекладу, а розробник завжди працював з вимкненим кешем у DevTools. Реальні користувачі теж мали service worker, тобто баг був справжнім: після деплою переклади не оновлювалися, доки не закриєш усі вкладки. Я додала в репорт кроки з кешем, і баг перейшов із Cannot Reproduce у High.
Example from a project. The bug: after switching the language to Polish, some buttons stayed in English. The developer could not reproduce it. I went through my checklist and found the difference: I had an active service worker with an old version of the translation files, while the developer always worked with the cache disabled in DevTools. Real users had the service worker too, so the bug was real: after a deploy the translations did not update until every tab was closed. I added the cache steps to the report, and the bug went from Cannot Reproduce to High.
Підводні камені. Сприймати відповідь як звинувачення і просто перевідкривати тікет тим самим текстом. Не перевірити, чи не виправили баг випадково на новішому build. Погоджуватися на Cannot Reproduce, не спробувавши в чистому профілі, — інколи причина справді в локальних налаштуваннях тестувальника, і тоді чесно закрити тікет теж правильно.
Pitfalls. Taking the reply as an accusation and just reopening the ticket with the same text. Not checking whether the bug was fixed by accident on a newer build. Accepting Cannot Reproduce without trying a clean profile — sometimes the cause really is the tester's local setup, and then closing the ticket honestly is the right thing too.
Уточнююче питання. «Коли погоджуватися закрити як Cannot Reproduce?» Коли я сама не можу відтворити на актуальному build після кількох спроб у різних оточеннях і немає слідів у логах. Тоді закриваю з описом, що перевірила, і з умовою: якщо з’явиться знову — перевідкрити з request id.
Follow-up question. "When do you agree to close as Cannot Reproduce?" When I cannot reproduce it myself on the current build after several attempts in different environments and there is no trace in the logs. Then I close it describing what I checked, with a condition: if it shows up again, reopen with a request id.
Same build? mine: web 6.3.1 (a91c2d0) dev: local main (f0e44b1)
Same data? mine: account pl-qa-03 (lang=pl, 2 workspaces) dev: fresh account
Same role/flags? mine: role=editor, flag new-i18n=on dev: admin, flag off
Cache / SW? mine: service worker active (sw.js v6.3.0) dev: "Disable cache" on
Locale / TZ? mine: pl-PL, Europe/Warsaw dev: en-US, UTC
Extensions? mine: none (clean profile checked) dev: n/a
Network? mine: office VPN dev: local
Timing? mine: switched language while page was loading dev: after full load
-> Difference found: service worker served old translations bundle.
Added to Preconditions: "service worker registered before deploy of 6.3.1".
Що це. Нестабільний (intermittent) баг репортять так само повно, як стабільний, але з трьома додатковими речами: частота у вигляді «N з M спроб» за описаних умов, що вже виключено (build, дані, браузер) і максимум слідів з кожного падіння: request id, час в UTC, логи, HAR. Частоту вимірюють, а не оцінюють: повторюють сценарій 10–20 разів, вручну або скриптом.
What it is. An intermittent bug is reported as fully as a stable one, plus three extras: the rate as "N out of M attempts" under the stated conditions, what has been ruled out (build, data, browser) and the most traces possible from every failure: request id, UTC time, logs, HAR. The rate is measured, not guessed: repeat the scenario 10–20 times, by hand or with a script.
Навіщо. Фраза «інколи падає» не дає розробнику ні з чого почати і часто закінчується Cannot Reproduce. «4 з 20, лише коли два запити йдуть паралельно» — вже гіпотеза про race condition. Сліди з кількох падінь дозволяють знайти спільне: той самий под, той самий шард бази, той самий час доби.
Why it matters. "Sometimes fails" gives the developer nothing to start with and often ends as Cannot Reproduce. "4 out of 20, only when two requests run in parallel" is already a hypothesis about a race condition. Traces from several failures let you find what they share: the same pod, the same database shard, the same time of day.
Приклад з проєкту. У сервісі бронювання подвійне списання коштів траплялося приблизно раз на двадцять оплат. Я написала невеликий скрипт, який 50 разів створював бронювання з повільним 3G, і зібрала request id усіх невдалих спроб. 3 з 50 давали два списання, і в усіх трьох клієнт повторював POST після таймауту 10 секунд, а в запиті не було ключа ідемпотентності. Репорт з цією статистикою і request id став основою для додавання ключа Idempotency-Key у платіжний API.
Example from a project. In a booking service, customers were occasionally charged twice, roughly once in twenty payments. I wrote a small script that created a booking 50 times over throttled 3G and collected the request ids of every failure. 3 out of 50 produced two charges, and in all three the client retried the POST after a 10-second timeout with no idempotency key in the request. The report with those statistics and request ids became the basis for adding an Idempotency-Key header to the payment API.
Підводні камені. Писати «рідко» чи «інколи» без чисел. Одна-дві спроби замість серії — з такої вибірки частоту не визначиш. Не зберегти сліди з першого ж падіння, бо «потім відтворю ще». Позначати баг як Minor лише через рідкість: подвійне списання грошей раз на двадцять оплат — Critical.
Pitfalls. Writing "rarely" or "sometimes" without numbers. One or two attempts instead of a series — you cannot tell a rate from that sample. Not saving the traces from the very first failure because "I'll reproduce it again later". Rating the bug Minor just because it is rare: a double charge once in twenty payments is Critical.
Уточнююче питання. «Як знизити шанс Cannot Reproduce для такого бага?» Дати розробнику спосіб підвищити частоту: повільна мережа, паралельні запити, навантаження. Якщо баг відтворюється 1 з 20 у звичайних умовах і 8 з 10 з throttling, це найцінніший рядок у репорті.
Follow-up question. "How do you lower the chance of Cannot Reproduce for such a bug?" Give the developer a way to raise the rate: slow network, parallel requests, load. If the bug reproduces 1 in 20 normally and 8 in 10 with throttling, that is the most valuable line in the report.
Title: [Payments] Customer charged twice when booking is retried after client timeout
Environment: staging-eu, booking-api 3.18.0 (c21f9e4), payments-api 2.31.4
Severity: Critical (double charge)
Reproduction rate:
- normal network: 1/20 manual attempts
- Chrome DevTools "Slow 3G": 3/50 (script repro-booking.sh, attached)
- DevTools offline toggle at 8 s after "Pay": 7/10
Steps to reproduce:
1. Log in as qa.booker+03@example.com, open hotel H-2291, dates 12-14 Sep
2. Set network to "Slow 3G", click "Pay 240 EUR"
3. Wait for "Something went wrong, retrying..." (client timeout 10 s)
Expected: exactly one charge of 240 EUR; retry returns the same payment
Actual: two charges of 240 EUR (pay_8Kx1..., pay_8Kx4...)
Ruled out: browser (Chrome, Firefox), test card type, specific hotel
Traces (UTC): 2024-07-02T09:14:07Z req 0b9e...a1, 09:14:17Z req 44c2...7f
Observation: retried POST /payments carries no Idempotency-Key header
Ruled out економить розробнику час, Traces дає request id кожного падіння, а Observation відділяє спостереження від висновку.Ruled out saves the developer time, Traces lists the request id of every failure, and Observation keeps what was seen apart from conclusions.Що це. Flaky-тест падає через проблему в самому тесті чи інфраструктурі: жорсткий таймаут, залежність від порядку тестів, спільні тестові дані, повільний CI-агент. Нестабільний баг — продукт справді інколи поводиться неправильно, і користувач теж це побачить. Розрізняють за доказами з падіння: trace або відео тесту, відповіді API, логи сервера. Якщо сервер повернув 500 або відповідь з неправильними даними — це кандидат у баг продукту; якщо тест не дочекався елемента, який з’явився через 50 мс після таймауту, — проблема тесту.
What it is. A flaky test fails because of a problem in the test itself or the infrastructure: a hard timeout, dependence on test order, shared test data, a slow CI agent. An intermittent bug means the product really does misbehave sometimes, and a user will see it too. You tell them apart from the evidence of the failure: the test's trace or video, the API responses, the server logs. If the server returned a 500 or a response with wrong data, it is a candidate product bug; if the test did not wait for an element that appeared 50 ms after the timeout, it is a test problem.
Навіщо. Команди, що звикли до flaky-тестів, натискають «перезапустити», і справжні race condition проходять у прод непоміченими. Протилежна крайність — заводити баг на кожне падіння автотесту, і розробники перестають вірити QA. Розбір падіння до кінця і правильний тип тікета (bug у продукті чи task на тест) тримають довіру до автоматизації.
Why it matters. Teams used to flaky tests press "rerun", and real race conditions slip into production unnoticed. The opposite extreme is filing a bug for every autotest failure, and developers stop trusting QA. Investigating the failure to the end and picking the right ticket type (a product bug or a task for the test) keeps trust in the automation.
Приклад з проєкту. У Playwright-наборі тест оформлення замовлення падав приблизно в 3% запусків і завжди проходив з повтором, тому звіт показував його як flaky. Я відкрила trace з першої невдалої спроби: у вкладці Network POST /orders повертав 409 «stock reserved by another session». Причина — паралельні воркери використовували той самий SKU. Для тесту ми зробили унікальні дані на воркер, але питання «чи бачить реальний користувач 409, коли хтось інший резервує останній товар» стало окремим багом продукту: повідомлення для користувача було технічним і незрозумілим.
Example from a project. In a Playwright suite the checkout test failed in about 3% of runs and always passed on retry, so the report showed it as flaky. I opened the trace from the first failed attempt: in the Network tab POST /orders returned 409 "stock reserved by another session". The cause was parallel workers using the same SKU. For the test we made the data unique per worker, but the question "does a real user see a 409 when someone else reserves the last item" became a separate product bug: the message shown to the user was technical and confusing.
Підводні камені. Вмикати повтори без trace, і тоді доказів першого падіння немає. Вважати тест flaky лише тому, що з повтором він проходить: баг продукту теж може проявлятися не щоразу. Залишати flaky-тест у наборі без тікета і власника — з часом таких стає десятки. Заводити баг продукту без підтвердження поза тестом.
Pitfalls. Enabling retries without traces, so there is no evidence from the first failure. Calling a test flaky just because it passes on retry: a product bug can also appear only some of the time. Leaving a flaky test in the suite with no ticket and no owner — over time there are dozens of them. Filing a product bug without confirming it outside the test.
Уточнююче питання. «Що ти робиш з тестом, поки розбираєшся?» Позначаю його як відомо нестабільний з посиланням на тікет (у Playwright — анотація або тег, у Jira — мітка flaky), щоб він не блокував злиття, але не видаляю і не ховаю. У тікеті — trace, частота і власник.
Follow-up question. "What do you do with the test while you investigate?" I mark it as known flaky with a link to the ticket (an annotation or tag in Playwright, a flaky label in Jira) so it does not block merges, but I neither delete nor hide it. The ticket has the trace, the rate and an owner.
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: "retain-on-failure", // trace of every failed attempt is kept, passes are dropped
video: "retain-on-failure",
screenshot: "only-on-failure",
},
});
// mark while investigating, with a ticket link
test("checkout creates an order", {
annotation: { type: "issue", description: "https://jira.example/browse/QA-318" },
tag: "@flaky",
}, async ({ page }) => { /* ... */ });
$ npx playwright show-trace test-results/checkout-creates-an-order-chromium/trace.zip
# Network tab: POST /api/orders -> 409 {"code":"STOCK_RESERVED"} => not a timing issue in the test
retries увімкнені лише в CI; trace: "retain-on-failure" записує trace кожної спроби і зберігає лише ті, що впали, тож доказ першого падіння залишається, навіть якщо повтор пройшов. annotation з типом issue і tag позначають тест у звіті. show-trace відкриває trace з вкладками Actions, Console і Network, де видно відповідь сервера.retries are enabled only in CI; trace: "retain-on-failure" records a trace for every attempt and keeps only the failed ones, so the evidence of the first failure survives even when the retry passes. The annotation of type issue and the tag mark the test in the report. show-trace opens the trace with the Actions, Console and Network tabs, where the server response is visible.Що це. Баг безпеки репортять в обмеженому доступі: окремий проєкт або security level у Jira, приватний Security Advisory у GitHub чи окремий канал до security-команди, а не в загальний беклог. Опис дає достатньо для відтворення, але без робочих секретів: токени, паролі, повні дані користувачів замінюють на позначки. Severity часто оцінюють за CVSS або внутрішньою шкалою, а в заголовку не описують техніку атаки для всіх, хто бачить список.
What it is. A security bug is filed with restricted access: a separate project or security level in Jira, a private Security Advisory on GitHub or a direct channel to the security team, not the general backlog. The description gives enough to reproduce but no working secrets: tokens, passwords and full user data are replaced with markers. Severity is often rated with CVSS or an internal scale, and the title does not spell out the attack technique for everyone who sees the list.
Навіщо. Загальний беклог бачать підрядники, стажери, інколи клієнти через інтеграції. Детальний опис вразливості з робочими кроками — готова інструкція для атаки, поки вона не виправлена. Обмежений доступ і відповідальне розкриття дають команді час виправити проблему, а security-команда контролює комунікацію і, за потреби, повідомлення регулятору.
Why it matters. The general backlog is visible to contractors, interns, sometimes customers through integrations. A detailed write-up of a vulnerability with working steps is a ready attack manual until it is fixed. Restricted access and responsible disclosure give the team time to fix the problem, and the security team controls the communication and, if needed, the regulator notification.
Приклад з проєкту. Я помітила, що GET /api/invoices/{id} повертав рахунки інших компаній, якщо змінити id в URL (IDOR). Я перевірила це лише на двох своїх тестових акаунтах, не перебирала чужі id на проді, і створила тікет у проєкті SEC з рівнем доступу Security. Заголовок нейтральний: «Access control issue in invoices API». Приклад нижче. Через день фікс був на staging, а security-команда перевірила логи доступу, щоб з’ясувати, чи хтось використовував вразливість раніше.
Example from a project. I noticed that GET /api/invoices/{id} returned other companies' invoices if you changed the id in the URL (IDOR). I verified it only with two of my own test accounts, did not enumerate other ids on production, and created a ticket in the SEC project with the Security access level. The title was neutral: "Access control issue in invoices API". The example is below. A day later the fix was on staging, and the security team checked the access logs to see whether anyone had exploited it earlier.
Підводні камені. Створити тікет у публічному проєкті або написати деталі в загальний канал Slack. Прикласти скриншот з реальними даними іншого клієнта. «Доводити» вразливість на проді, перебираючи чужі записи, — це вже може бути порушенням закону і політик компанії. Занизити severity, бо «це ж тільки тестовий стенд»: код той самий, що на проді.
Pitfalls. Filing the ticket in a public project or posting details in a general Slack channel. Attaching a screenshot with another customer's real data. "Proving" the vulnerability on production by enumerating other people's records — that may already break the law and company policy. Lowering severity because "it's only a test environment": the code is the same as in production.
Уточнююче питання. «Що робиш, якщо в компанії немає процесу для багів безпеки?» Пишу напряму тимліду або CTO в особисті повідомлення без деталей, «знайшла проблему з доступом до даних, де можу передати деталі безпечно?», і пропоную створити такий процес: окремий проєкт з обмеженим доступом і файл SECURITY.md.
Follow-up question. "What if the company has no process for security bugs?" I message the team lead or CTO directly without details — "I found a data access problem, where can I share the details safely?" — and suggest creating such a process: a separate restricted project and a SECURITY.md file.
Project: SEC Security level: Security team only
Title: Access control issue in invoices API
Severity: High (internal scale: cross-tenant data exposure)
CVSS 3.1: 6.5 Medium, AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Environment: staging-1, billing-api 5.4.0 (d8e21b3); same code on prod 5.4.0
Accounts used: two synthetic tenants, qa-tenant-a and qa-tenant-b (no real data)
Steps to reproduce:
1. Log in as user of qa-tenant-a, create invoice -> id INV-A (e.g. 70411)
2. Log in as user of qa-tenant-b, copy its session token
3. curl -s https://staging-1.billing.example/api/invoices/70411 \
-H "Authorization: Bearer <TENANT_B_TOKEN>"
Expected: 404 Not Found (or 403) for an invoice of another tenant
Actual: 200 OK with the full invoice of qa-tenant-a (amount, address, VAT id)
Not done: no enumeration of real ids, no prod requests
Attachments: response-redacted.json (tenant A fields masked)
Security level обмежує, хто бачить тікет. Заголовок нейтральний, а технічні деталі — лише в тілі. Внутрішня severity може бути вищою за якісну оцінку CVSS, бо враховує контекст: тут це доступ до даних іншого клієнта. CVSS-вектор пояснює базову оцінку: мережевий доступ, низька складність, потрібен звичайний логін, високий вплив на конфіденційність. Токен замінено на <TENANT_B_TOKEN>, а рядок Not done фіксує, чого тестувальник свідомо не робив.Security level restricts who can see the ticket. The title is neutral, and the technical detail lives only in the body. The internal severity can be higher than the CVSS qualitative rating because it adds context: here, access to another customer's data. The CVSS vector explains the base score: network access, low complexity, an ordinary login required, high impact on confidentiality. The token is replaced with <TENANT_B_TOKEN>, and the Not done line records what the tester deliberately did not do.Що це. Усе, що прикладається до бага, треба перевірити на персональні дані (імена, email, телефони, адреси, номери документів і карток, IP) та секрети (токени, cookie, API-ключі, паролі, підписані URL). Для тексту (логи, HAR, JSON) — автоматична заміна за шаблонами і ручна перевірка; для зображень — розмиття чи зафарбовування ще до завантаження, бо оригінал зберігається у вкладеннях. Найкращий варіант — від початку тестувати на синтетичних даних.
What it is. Everything attached to a bug must be checked for personal data (names, emails, phone numbers, addresses, document and card numbers, IPs) and secrets (tokens, cookies, API keys, passwords, signed URLs). For text (logs, HAR, JSON), use automatic pattern replacement plus a manual check; for images, blur or black out before uploading, because the original stays in the attachments. The best option is to test on synthetic data from the start.
Навіщо. Трекер — не захищене сховище: доступ мають десятки людей, підрядники, інтеграції, а вкладення живуть роками. Персональні дані в тікеті — потенційне порушення GDPR, а токен у лозі — робочий доступ, поки він не відкликаний. Витік через баг-репорт — реальний і доволі поширений сценарій.
Why it matters. A tracker is not secure storage: dozens of people, contractors and integrations have access, and attachments live for years. Personal data in a ticket is a potential GDPR breach, and a token in a log is working access until it is revoked. A leak through a bug report is a real and fairly common scenario.
Приклад з проєкту. Розбираючи баг у CRM, я мала прикласти лог сервісу нотифікацій на 2 000 рядків. Скрипт заміни прибрав email і Bearer-токени, але ручна перевірка знайшла ще телефони в полі to SMS-провайдера у форматі без плюса, який шаблон не ловив. Я доповнила шаблон, а в команді ми домовилися, що логи тестових стендів пишуться вже з маскуванням на стороні сервісу, і проблема зникла в корені.
Example from a project. While working on a CRM bug I needed to attach a 2,000-line log from the notification service. The replacement script removed emails and Bearer tokens, but the manual check found phone numbers in the SMS provider's to field, written without a plus sign, which the pattern missed. I extended the pattern, and the team agreed that test environment logs should be masked by the service itself, which solved the problem at the root.
Підводні камені. Розраховувати лише на автоматичну заміну: формати даних різні, завжди буде щось, що шаблон пропустить. Замазувати дані на скриншоті напівпрозорим пензлем, крізь який усе видно. Видалити токен з опису, але залишити в прикладеному HAR. Вважати, що на тестовому стенді даних немає: туди часто копіюють частину продакшн-бази.
Pitfalls. Relying on automatic replacement alone: data formats vary, and something will always slip past the pattern. Covering data on a screenshot with a semi-transparent brush you can see through. Removing a token from the description but leaving it in the attached HAR. Assuming a test environment has no real data: part of the production database is often copied there.
Уточнююче питання. «Якщо секрет уже потрапив у тікет — що робити?» Видалити вкладення чи відредагувати коментар недостатньо, бо історія змін і сповіщення на пошту зберігаються. Треба повідомити власника секрету, відкликати токен або ключ і лише потім прибрати його з тікета.
Follow-up question. "If a secret has already ended up in a ticket, what do you do?" Deleting the attachment or editing the comment is not enough, because the change history and email notifications remain. Tell the secret's owner, revoke the token or key, and only then remove it from the ticket.
$ sed -E \
-e 's/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/<EMAIL>/g' \
-e 's/(Bearer )[A-Za-z0-9._~+\/-]+=*/\1<TOKEN>/g' \
-e 's/("(phone|to|msisdn)": *")\+?[0-9 ()-]{9,}/\1<PHONE>/g' \
-e 's/\b[0-9]{4}( ?[0-9]{4}){3}\b/<CARD>/g' \
notifications.log > notifications.masked.log
# manual check: nothing that looks like an email, token or long number is left
$ grep -nE "@|Bearer [A-Za-z0-9]|[0-9]{9,}" notifications.masked.log | head
$ diff <(wc -l < notifications.log) <(wc -l < notifications.masked.log) # same line count
-e — окреме правило заміни: email, Bearer-токен, телефон у JSON-полях phone, to або msisdn з необов’язковим плюсом, номер картки з 16 цифр. \1 зберігає захоплену частину, наприклад назву поля. Після заміни grep шукає залишки, а diff кількості рядків перевіряє, що лог не обрізаний.-e is a separate replacement rule: email, Bearer token, a phone number in the JSON fields phone, to or msisdn with an optional plus, a 16-digit card number. \1 keeps the captured part, such as the field name. After the replacement, grep looks for leftovers and diff on the line counts checks the log was not truncated.Що це. Інцидент — подія в продакшні, що вже шкодить користувачам або бізнесу: сервіс недоступний, платежі не проходять, дані показуються неправильно. Його мета — відновити роботу якнайшвидше, будь-яким способом: відкат, вимкнення фічі, ручне виправлення даних. Баг — дефект у коді чи конфігурації, його мета — усунути причину. Один інцидент може мати кілька багів-причин, а один баг — спричинити кілька інцидентів. Для інцидентів є свій процес: on-call, severity SEV1–SEV4, канал інциденту, postmortem.
What it is. An incident is a production event that is already hurting users or the business: a service is down, payments fail, data is shown wrongly. Its goal is to restore service as fast as possible, by any means: rollback, switching a feature off, fixing data by hand. A bug is a defect in code or configuration; its goal is to remove the cause. One incident can have several causal bugs, and one bug can cause several incidents. Incidents have their own process: on-call, SEV1–SEV4 severity, an incident channel, a postmortem.
Навіщо. Змішування процесів шкодить обом: якщо інцидент оформити як звичайний баг, він чекатиме тріажу, поки клієнти не можуть платити. Якщо баг-причину не створити після інциденту, сервіс відновлять відкатом, але дефект повернеться з наступним релізом. Зв’язки в трекері показують, які баги реально дійшли до користувачів, і це основа для метрик на кшталт defect leakage.
Why it matters. Mixing the processes hurts both: if an incident is filed as an ordinary bug, it waits for triage while customers cannot pay. If no causal bug is created after the incident, service is restored by rollback but the defect returns with the next release. Links in the tracker show which bugs actually reached users, which is the basis for metrics like defect leakage.
Приклад з проєкту. У п’ятницю ввечері перестали надходити листи з підтвердженням замовлення. Інцидент INC-231 отримав SEV2, on-call за 40 хвилин відкотив конфіг поштового сервісу. На postmortem виявилося дві причини, і для кожної створили баг: SHOP-2290 — нова змінна оточення не потрапила в шаблон деплою, SHOP-2291 — моніторинг не мав алерту на падіння кількості відправлених листів. Я як QA додала в обидва баги кроки перевірки і тест у smoke-набір після деплою.
Example from a project. On a Friday evening, order confirmation emails stopped going out. Incident INC-231 was rated SEV2, and on-call rolled back the mail service config within 40 minutes. The postmortem found two causes, and a bug was created for each: SHOP-2290 — a new environment variable was missing from the deploy template, SHOP-2291 — monitoring had no alert on a drop in sent emails. As QA I added verification steps to both bugs and a check to the post-deploy smoke suite.
Підводні камені. Відкривати інцидент на кожен баг, знайдений на проді, — не кожен дефект у продакшні є інцидентом, якщо він нікому зараз не шкодить. Закривати інцидент без жодного бага-причини і дій із postmortem. Шукати винних замість причин: формат blameless postmortem існує саме для того, щоб люди чесно розповідали, що сталося.
Pitfalls. Opening an incident for every bug found in production — not every production defect is an incident if it is not hurting anyone right now. Closing an incident without a single causal bug or postmortem action. Looking for someone to blame instead of causes: the blameless postmortem format exists precisely so people talk honestly about what happened.
Уточнююче питання. «Яка роль QA в інциденті?» Швидко відтворити і звузити проблему, перевірити, що відкат або hotfix справді допоміг, а після — оформити баги-причини і додати перевірки в регресійний або smoke-набір, щоб інцидент не повторився.
Follow-up question. "What is QA's role in an incident?" Reproduce and narrow the problem quickly, verify that the rollback or hotfix actually helped, then file the causal bugs and add checks to the regression or smoke suite so the incident does not repeat.
INC-231 [SEV2] Order confirmation emails not sent (prod, 2024-06-21 18:05-18:47 UTC)
Mitigation: rollback of mail-service config 1.9.0 -> 1.8.4
Postmortem: PM-231 (blameless), 3 action items
|-- is caused by --> SHOP-2290 Bug: SMTP_POOL_SIZE missing from deploy template
|-- is caused by --> SHOP-2291 Bug: no alert when sent emails drop below baseline
|-- relates to --> QA-402 Task: add "confirmation email sent" check to post-deploy smoke
# production bugs linked to incidents in the last 90 days
project = SHOP AND issuetype = Bug AND issueFunction in linkedIssuesOf("project = INC", "is caused by")
AND created >= -90d
is caused by, дія QA — окремою задачею. Функція linkedIssuesOf у JQL доступна з додатком ScriptRunner; без нього ту саму вибірку роблять міткою на кшталт from-incident.is caused by link type, and the QA action is a separate task. The linkedIssuesOf function in JQL comes with the ScriptRunner app; without it, the same selection is done with a label such as from-incident.Що це. Стандартні поля бага в Jira: Summary (заголовок), Issue Type = Bug, Priority, Component/s (частина продукту, за яку відповідає конкретна команда), Labels (вільні мітки для зрізів: regression, customer-reported, a11y), Affects Version/s (де знайдено), Fix Version/s (куди виправлять, зазвичай ставить команда), Environment, Description, Attachment і Linked Issues. Severity у багатьох проєктах — кастомне поле.
What it is. The standard Jira fields for a bug: Summary (the title), Issue Type = Bug, Priority, Component/s (the part of the product a specific team owns), Labels (free-form tags for slicing: regression, customer-reported, a11y), Affects Version/s (where it was found), Fix Version/s (where it will be fixed, usually set by the team), Environment, Description, Attachment and Linked Issues. Severity is a custom field in many projects.
Навіщо. Поля перетворюють текст на дані. Component автоматично призначає баг потрібній команді й потрапляє на її дошку; labels дозволяють за секунду зібрати «усі регресії релізу» або «усі баги від клієнтів»; Affects Version показує, в яких релізах баг живе. Без них пошук і звіти спираються на пам’ять людей.
Why it matters. Fields turn text into data. Component assigns the bug to the right team automatically and puts it on their board; labels let you pull "all regressions in this release" or "all customer-reported bugs" in a second; Affects Version shows which releases the bug lives in. Without them, search and reports rely on people's memory.
Приклад з проєкту. На проєкті з шістьма командами баги без компонента падали в загальну чергу, і їх розбирав тимлід раз на тиждень. Ми зробили Component обов’язковим полем на екрані створення і налаштували component lead як виконавця за замовчуванням. Середній час від створення бага до початку роботи скоротився з чотирьох днів до одного. Паралельно ми почистили labels: з 140 варіантів залишили 15 узгоджених і описали їх у Confluence.
Example from a project. On a project with six teams, bugs without a component landed in a shared queue and the team lead went through it once a week. We made Component mandatory on the create screen and set the component lead as the default assignee. The average time from bug creation to work starting dropped from four days to one. In parallel we cleaned up labels: out of 140 variants we kept 15 agreed ones and documented them in Confluence.
Підводні камені. Labels як вільний текст без правил: regression, Regression, regresion і reg — чотири різні мітки, і фільтр знаходить лише частину. Ставити Fix Version самостійно, коли це рішення команди. Дублювати в labels те, що вже є в полях: android при наявному компоненті Android App.
Pitfalls. Labels as free text with no rules: regression, Regression, regresion and reg are four different labels, and a filter finds only some of them. Setting Fix Version yourself when it is the team's decision. Duplicating fields in labels: android when there is already an Android App component.
Уточнююче питання. «Чим component відрізняється від label?» Component — структурна частина продукту з власником, їх небагато і їх налаштовує адміністратор проєкту. Label — довільна мітка для зрізу, її може додати будь-хто, тому потрібні домовленості.
Follow-up question. "How does a component differ from a label?" A component is a structural part of the product with an owner; there are few of them and the project admin sets them up. A label is an arbitrary tag for slicing that anyone can add, which is why it needs conventions.
Issue Type: Bug
Summary: [Checkout] Promo code SPRING10 is rejected when cart has a sale item
Component/s: Checkout -> auto-assigned to Checkout team lead
Labels: regression, promo (from the agreed list of 15 labels)
Severity: Major (custom field, set by QA)
Priority: High (confirmed at triage)
Affects Version: web-4.12.0
Fix Version/s: web-4.12.3 (set by the team)
Environment: staging-2, Chrome 129 / macOS 14.6, commit a1b9e3f
Linked Issues: is caused by SHOP-2101; relates to PROMO-45
Attachment: promo-422.mp4, cart-promo.har
Що це. JQL (Jira Query Language) — мова запитів до Jira: поле, оператор, значення, з’єднані AND/OR, плюс ORDER BY. Щоденний набір QA: «чекають на мою перевірку», «нові баги в моїх компонентах», «перевідкриті за тиждень», «відкриті баги поточного релізу за severity», «баги без компонента чи severity». Збережені запити стають фільтрами, а фільтри — гаджетами дашборду (Filter Results, Pie Chart, Two Dimensional Filter Statistics).
What it is. JQL (Jira Query Language) is Jira's query language: field, operator, value, joined with AND/OR, plus ORDER BY. A QA's daily set: "waiting for my verification", "new bugs in my components", "reopened this week", "open bugs in the current release by severity", "bugs with no component or severity". Saved queries become filters, and filters become dashboard gadgets (Filter Results, Pie Chart, Two Dimensional Filter Statistics).
Навіщо. Інтерфейс пошуку Jira зручний для разового запиту, але щоденна робота — це ті самі питання щодня. Збережені фільтри з підпискою на пошту або дашборд відповідають на них за секунду і однаково для всієї команди. Уміння писати JQL також часто перевіряють на співбесіді як ознаку того, що кандидат реально працював з трекером.
Why it matters. Jira's search UI is fine for a one-off question, but daily work means the same questions every day. Saved filters with an email subscription or a dashboard answer them in a second, and the same way for the whole team. Writing JQL is also often checked in interviews as a sign the candidate has really worked with a tracker.
Приклад з проєкту. Для свого релізного дашборда я зробила п’ять фільтрів: відкриті баги fix version за severity (двовимірна таблиця severity × статус), перевідкриті за 14 днів, баги без компонента, «чекають QA» і створені за день. Тимлід перестав питати про статус релізу в чаті: дашборд був відкритий на щоденному стендапі, і за ним одразу було видно, де затор.
Example from a project. For my release dashboard I made five filters: open bugs in the fix version by severity (a severity × status two-dimensional table), reopened in the last 14 days, bugs without a component, "waiting for QA" and created today. The team lead stopped asking about release status in chat: the dashboard was on screen at the daily stand-up, and it was obvious where things were stuck.
Підводні камені. Використовувати status != Closed замість statusCategory != Done, і тоді баги в статусі «Done» чи «Won’t Fix» потрапляють у відкриті. Хардкодити ім’я користувача замість currentUser() у фільтрі, яким користуються інші. Шукати за назвою кастомного поля, яку перейменували, — надійніше за ID cf[10021].
Pitfalls. Using status != Closed instead of statusCategory != Done, so bugs in "Done" or "Won't Fix" show up as open. Hardcoding a user name instead of currentUser() in a filter others use. Searching by a custom field name that has since been renamed — the cf[10021] id is more reliable.
Уточнююче питання. «Як знайти баги, які перевідкривали?» Через історію статусів: status CHANGED TO Reopened. Оператори історії CHANGED, WAS і DURING працюють для status, assignee, priority, fixVersion та ще кількох системних полів.
Follow-up question. "How do you find bugs that were reopened?" Through status history: status CHANGED TO Reopened. The history operators CHANGED, WAS and DURING work for status, assignee, priority, fixVersion and a few other system fields.
# 1. Waiting for my verification
project = SHOP AND issuetype = Bug AND status = "Ready for QA" AND assignee = currentUser()
ORDER BY priority DESC, updated ASC
# 2. New bugs in my components, last 24 hours
project = SHOP AND issuetype = Bug AND component in (Checkout, Cart) AND created >= -1d
# 3. Reopened in the last 14 days
project = SHOP AND issuetype = Bug AND status CHANGED TO Reopened AFTER -14d
# 4. Open bugs in the release (dashboard: Two Dimensional Filter Statistics, Severity x Status)
project = SHOP AND issuetype = Bug AND fixVersion = "web-4.13.0" AND statusCategory != Done
# 5. Hygiene: bugs missing a component or severity
project = SHOP AND issuetype = Bug AND statusCategory != Done
AND (component IS EMPTY OR cf[10021] IS EMPTY)
currentUser() підставляє того, хто відкрив фільтр, тож фільтр 1 працює для кожного QA. -1d і -14d — відносні дати. CHANGED TO ... AFTER шукає за історією статусів. statusCategory != Done охоплює всі незавершені статуси, а IS EMPTY знаходить незаповнені поля.currentUser() substitutes whoever opens the filter, so filter 1 works for every QA. -1d and -14d are relative dates. CHANGED TO ... AFTER searches the status history. statusCategory != Done covers every unfinished status, and IS EMPTY finds empty fields.Що це. GitHub підтримує issue forms — YAML-файли в .github/ISSUE_TEMPLATE/, які перетворюють створення issue на форму з полями: textarea, input, dropdown, checkboxes, з обов’язковими полями через validations: required: true і автоматичними мітками. Файл config.yml у тій самій папці може вимкнути порожні issue і додати посилання, наприклад на приватний канал для багів безпеки.
What it is. GitHub supports issue forms — YAML files in .github/ISSUE_TEMPLATE/ that turn issue creation into a form with fields: textarea, input, dropdown, checkboxes, with required fields via validations: required: true and automatic labels. A config.yml file in the same folder can disable blank issues and add links, for example to a private channel for security bugs.
Навіщо. Шаблон — найдешевший спосіб підняти якість репортів від усіх, не лише від QA: розробників, продакту, зовнішніх контриб’юторів. Обов’язкові поля не дають створити issue без кроків і версії, мітки одразу ставлять issue в чергу тріажу, а dropdown для severity чи оточення робить дані однорідними для фільтрів.
Why it matters. A template is the cheapest way to raise report quality from everyone, not just QA: developers, product, external contributors. Required fields stop issues from being created without steps and a version, labels put the issue into the triage queue straight away, and dropdowns for severity or environment make the data consistent for filters.
Приклад з проєкту. У відкритому SDK, який підтримувала наша команда, дві третини issue від користувачів не мали версії SDK і кроків. Ми додали форму з обов’язковими полями «версія», «мінімальний приклад коду» і «очікувана/фактична поведінка», dropdown для платформи і чекбокс «я пошукав серед наявних issue». За два місяці частка issue, закритих як «потрібна інформація», впала приблизно вдвічі, а для вразливостей config.yml вів на приватне повідомлення через Security Advisory.
Example from a project. In an open-source SDK our team maintained, two thirds of user issues had no SDK version and no steps. We added a form with required "version", "minimal code sample" and "expected/actual behaviour" fields, a platform dropdown and a checkbox "I searched existing issues". Within two months the share of issues closed as "needs info" dropped by about half, and for vulnerabilities config.yml pointed to private reporting through a Security Advisory.
Підводні камені. Форма на двадцять полів — люди її кидають або заповнюють формально. Обов’язкові поля, які зовнішній користувач не може знати (наприклад, commit). Відсутній config.yml з blank_issues_enabled: false — тоді шаблон легко обійти. Забути, що форма публічна, і попросити вставити логи без попередження про токени.
Pitfalls. A form with twenty fields — people abandon it or fill it in carelessly. Required fields an external user cannot know (a commit, for instance). No config.yml with blank_issues_enabled: false, so the template is easy to bypass. Forgetting the form is public and asking people to paste logs without warning them about tokens.
Уточнююче питання. «Чим issue form відрізняється від markdown-шаблону?» Markdown-шаблон — просто попередньо заповнений текст, який можна стерти. Issue form — справжня форма з типами полів і валідацією, результат якої GitHub зберігає як markdown у тілі issue.
Follow-up question. "How is an issue form different from a markdown template?" A markdown template is just pre-filled text that can be deleted. An issue form is a real form with field types and validation, and GitHub stores its result as markdown in the issue body.
name: Bug report
description: Something does not work as documented
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
- type: input
id: version
attributes:
label: SDK version
placeholder: "e.g. 3.4.1"
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options: ["Android", "iOS", "Web", "Node.js"]
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce / minimal code sample
description: Remove tokens and personal data before pasting logs.
validations:
required: true
- type: textarea
id: expected-actual
attributes:
label: Expected vs actual behaviour
validations:
required: true
- type: checkboxes
id: searched
attributes:
label: Checks
options:
- label: I searched existing issues and found no duplicate
required: true
# .github/ISSUE_TEMPLATE/config.yml
# blank_issues_enabled: false
# contact_links:
# - name: Security vulnerability
# url: https://github.com/acme/sdk/security/advisories/new
# about: Report privately, not as a public issue
name, description, title, labels) описують шаблон і мітки, які GitHub поставить автоматично. У body кожен елемент має type, унікальний id, attributes з підписом і validations. У коментарях внизу — вміст окремого config.yml, що вимикає порожні issue і веде вразливості в приватний Security Advisory.name, description, title, labels) describe the template and the labels GitHub applies automatically. In body each element has a type, a unique id, attributes with a label and validations. The comments at the bottom show the separate config.yml that disables blank issues and routes vulnerabilities to a private Security Advisory.Що це. Тон баг-репорту — нейтральний, фактичний і безособовий: описуємо поведінку системи, а не людей. Замість «ви знову зламали» — «після релізу 4.12 кнопка неактивна». Без оцінок («жахливо», «очевидно»), без сарказму, без капслоку і знаків оклику. У коментарях — ті самі правила плюс конкретика: не «все ще не працює», а що саме, на якому build і з яким результатом.
What it is. A bug report's tone is neutral, factual and impersonal: we describe the system's behaviour, not people. Instead of "you broke it again", write "after release 4.12 the button is disabled". No judgements ("terrible", "obviously"), no sarcasm, no caps and no exclamation marks. Comments follow the same rules plus specifics: not "still doesn't work" but what exactly, on which build and with which result.
Навіщо. Баг-репорт читають люди, які цей код написали, і будь-який натяк на звинувачення запускає захист замість виправлення. Нейтральний тон тримає розмову на фактах і зберігає довіру між QA і розробкою, а це безпосередньо впливає на швидкість фіксів. До того ж тікети читають менеджери і клієнти через інтеграції — емоційний текст там виглядає непрофесійно.
Why it matters. Bug reports are read by the people who wrote the code, and any hint of blame triggers defensiveness instead of a fix. A neutral tone keeps the conversation on facts and preserves trust between QA and engineering, which directly affects how fast fixes arrive. Tickets are also read by managers and, through integrations, by customers — emotional text looks unprofessional there.
Приклад з проєкту. Колега-тестувальниця написала в перевідкритому тікеті: «Знову не працює, ви взагалі перевіряли?!». Розробник відповів різко, і обговорення розтягнулося на двадцять коментарів. Я переписала коментар: «Перевірила на 4.12.3: для гостьового користувача досі 422, для залогіненого виправлено. Відео і request id у вкладенні». Наступний фікс з’явився за дві години, а колега потім сама взяла цей формат за основу.
Example from a project. A fellow tester wrote on a reopened ticket: "Still broken, did you even test it?!". The developer replied sharply and the thread grew to twenty comments. I rewrote the comment: "Checked on 4.12.3: guest users still get 422, logged-in users are fixed. Video and request id attached." The next fix arrived within two hours, and my colleague later adopted that format herself.
Підводні камені. Писати «очевидно», «елементарно», «як завжди» — це звинувачення, навіть якщо не задумувалось. Згадувати ім’я розробника в заголовку чи описі проблеми. Надмірна обережність теж шкодить: «можливо, мені здається, але якщо вам не важко...» розмиває факти. Писати емоційно у спільному каналі, де баг обговорюють при всіх.
Pitfalls. Writing "obviously", "trivially", "as usual" — that is blame, even if unintended. Naming the developer in the title or the problem description. Excessive caution hurts too: "maybe it's just me, but if it's not too much trouble..." blurs the facts. Writing emotionally in a shared channel where the bug is discussed in front of everyone.
Уточнююче питання. «Що робити, якщо розробник відповідає грубо?» Не відповідати в тому ж тоні, повернути розмову до фактів і, якщо конфлікт не минає, обговорити голосом або з тимлідом наодинці, а не в коментарях до тікета.
Follow-up question. "What if the developer replies rudely?" Do not answer in the same tone, steer the conversation back to facts, and if the conflict does not settle, talk it through by voice or with the team lead in private, not in the ticket comments.
# before
"Still broken!!! Did anyone actually test this before moving it to QA?"
# after
"Checked on 4.12.3 (5b7e0aa): guest checkout still returns 422 PROMO_NOT_APPLICABLE,
logged-in checkout is fixed. Video and x-request-id 1c9f...e02 attached."
# before
"Obviously the backend ignores the discount, as usual."
# after
"POST /api/cart/promo responds 422 for carts with SKU 88412 (sale item).
Carts without sale items get 200 and the correct total."
# before
"Sorry to bother, maybe it's me, but the button might not work sometimes?"
# after
"Pay button stays disabled after adding a card via Apple Pay: 4/5 attempts, iOS 17.5."
Що це. Якість репортів видно з кількох метрик по тікетах: rejected rate — частка багів, закритих як Duplicate, Cannot Reproduce або Works as Designed; need-info rate — скільки повертали автору за даними; reopen rate — скільки перевідкривали після фіксу; час від створення до початку роботи. Defect leakage — частка дефектів, які знайшли після релізу: знайдені в проді / (знайдені до релізу + знайдені в проді) × 100%. Вона залежить не лише від тестування, а й від того, чи потрапили знайдені баги в репорт і в рішення про реліз.
What it is. Report quality shows up in a few ticket metrics: rejected rate — the share of bugs closed as Duplicate, Cannot Reproduce or Works as Designed; need-info rate — how many went back to the author for data; reopen rate — how many were reopened after a fix; time from creation to work starting. Defect leakage is the share of defects found after release: found in production / (found before release + found in production) × 100%. It depends not only on testing but on whether the bugs found made it into a report and into the release decision.
Навіщо. Метрики перетворюють «наші репорти нормальні» на перевірюване твердження і показують, куди вкладати зусилля: високий rejected rate — проблема з пошуком дублів або з розумінням вимог; високий reopen rate — ретест поверхневий або фікси неповні. Leakage показує, чи працює вся система якості, а розбір конкретних «витеклих» багів відповідає на питання, чому їх не спіймали або не зарепортили.
Why it matters. Metrics turn "our reports are fine" into a checkable claim and show where to invest: a high rejected rate points to duplicate searching or misread requirements; a high reopen rate means shallow retests or incomplete fixes. Leakage shows whether the whole quality system works, and going through the specific "leaked" bugs answers why they were not caught or not reported.
Приклад з проєкту. На проєкті rejected rate був 28%, і розробники перестали довіряти новим багам. Ми розібрали вибірку: половина — дублі, третина — Cannot Reproduce без build і даних. Після впровадження шаблону з обов’язковими полями і правила «пошук дублів перед створенням» за квартал rejected rate впав до 11%. Leakage за той самий період знизилася з 18% до 12%, і розбір показав, що три з п’яти «витеклих» багів раніше лежали в беклозі як Cannot Reproduce.
Example from a project. On one project the rejected rate was 28%, and developers stopped trusting new bugs. We went through a sample: half were duplicates, a third were Cannot Reproduce with no build or data. After introducing a template with required fields and the "search for duplicates before creating" rule, the rejected rate fell to 11% within a quarter. Leakage over the same period dropped from 18% to 12%, and the analysis showed that three of the five "leaked" bugs had earlier sat in the backlog as Cannot Reproduce.
Підводні камені. Використовувати метрики для оцінки окремих людей: тестувальники почнуть не репортити сумнівне, щоб не псувати rejected rate, і leakage зросте. Рахувати leakage без однакового правила, що таке «знайдено в проді»: баги від клієнтів, з моніторингу, від внутрішніх користувачів. Дивитися на число без розбору причин.
Pitfalls. Using metrics to rate individuals: testers will stop reporting borderline issues to protect their rejected rate, and leakage will rise. Counting leakage without a consistent rule for what "found in production" means: customer reports, monitoring, internal users. Looking at the number without analysing the causes.
Уточнююче питання. «Яка нормальна defect leakage?» Універсальної норми немає, вона залежить від домену і того, як рахують. Важливіша динаміка по релізах і розбір кожного критичного «витеклого» бага: не знайшли, знайшли але не зарепортили, чи зарепортили але свідомо випустили.
Follow-up question. "What is a normal defect leakage?" There is no universal norm; it depends on the domain and on how it is counted. What matters more is the trend across releases and an analysis of every critical leaked bug: not found, found but not reported, or reported but knowingly shipped.
# rejected rate = rejected / all bugs resolved in the period
project = SHOP AND issuetype = Bug AND resolved >= -90d
AND resolution in (Duplicate, "Cannot Reproduce", "Works as Designed")
project = SHOP AND issuetype = Bug AND resolved >= -90d
# reopen rate = reopened at least once / all resolved
project = SHOP AND issuetype = Bug AND resolved >= -90d AND status WAS Reopened
# defect leakage for release 4.12 (label "found-in-prod" set by support and on-call)
project = SHOP AND issuetype = Bug AND affectedVersion = "web-4.12.0" AND labels = found-in-prod
project = SHOP AND issuetype = Bug AND affectedVersion = "web-4.12.0"
# leakage = prod / (pre-release + prod) = 6 / (44 + 6) = 12%
status WAS Reopened знаходить баги, які хоч раз були в цьому статусі. Для leakage потрібна узгоджена мітка found-in-prod, інакше знайдені в продакшні баги не відрізнити від знайдених під час тестування.status WAS Reopened finds bugs that were in that status at least once. Leakage needs an agreed found-in-prod label, otherwise bugs found in production cannot be told apart from those found in testing.Що це. Тест-кейс — детальний сценарій: ID, назва, передумови (стан системи, дані, роль), кроки з тестовими даними й очікуваний результат для перевірки. Чек-лист — короткий перелік того, що перевірити, без кроків, наприклад «промокод відсотковий», «промокод на суму», «прострочений промокод». Передумови відокремлюють стан від дій: вони фіксують, з чого починається перевірка, щоб кейс давав однаковий результат на різних стендах. Добра практика — одна перевірка на кейс: коли він падає, одразу зрозуміло, що саме зламалося.
What it is. A test case is a detailed scenario: an id, a title, preconditions (system state, data, role), steps with test data and an expected result to check. A checklist is a short list of things to check with no steps, for example "percentage promo code", "fixed-amount promo code", "expired promo code". Preconditions separate state from actions: they fix where the check starts, so the case gives the same result on different environments. A good practice is one check per case: when it fails, it is immediately clear what broke.
Навіщо. Вибір залежить від ризику і команди. Тест-кейси потрібні там, де важлива відтворюваність і аудит: платежі, медицина, регуляторні вимоги, передача роботи новим людям, основа для автоматизації. Чек-листи швидші в підтримці і підходять для досвідченої команди, яка добре знає продукт. Без передумов кейс стає нестабільним: він проходить на одному стенді й падає на іншому, де немає потрібних даних.
Why it matters. The choice depends on risk and the team. Test cases are needed where reproducibility and audit matter: payments, healthcare, regulatory requirements, handing work to new people, a basis for automation. Checklists are cheaper to maintain and suit an experienced team that knows the product well. Without preconditions a case becomes unstable: it passes on one environment and fails on another that lacks the data.
Приклад з проєкту. На проєкті платежів ми тримали тест-кейси для оплати і повернень, бо аудитори вимагали доказів виконання, а для адмінки — чек-листи на 10–15 пунктів. Коли тест-кейс TC-204 «повернення частини суми» став нестабільним, виявилося, що в ньому не було передумови «оплата пройшла settlement»: на одних стендах settlement займав секунди, на інших — годину. Додали передумову і службовий виклик для примусового settlement — кейс стабілізувався.
Example from a project. On a payments project we kept test cases for payments and refunds because auditors required evidence of execution, and 10–15 item checklists for the admin panel. When test case TC-204 "partial refund" became unstable, it turned out to lack the precondition "payment has been settled": on some environments settlement took seconds, on others an hour. We added the precondition and a service call to force settlement, and the case stabilised.
Підводні камені. Писати тест-кейс з десятьма перевірками в одному: коли він падає, незрозуміло, яка з них. Копіювати в тест-кейс усі кроки логіну замість передумови «залогінений як менеджер». Чек-лист з пунктами на кшталт «перевірити оплату», які кожен розуміє по-своєму. Не оновлювати кейси після зміни вимог — через рік половина з них бреше.
Pitfalls. Writing a test case with ten checks in one: when it fails, nobody knows which one. Copying every login step into a test case instead of the precondition "logged in as manager". A checklist with items like "check payment" that everyone reads differently. Not updating cases after requirements change — a year later half of them lie.
Уточнююче питання. «Коли з чек-листа варто зробити тест-кейс?» Коли пункт регулярно пропускають або перевіряють по-різному, коли він іде в автоматизацію або коли його виконуватимуть нові люди. Тоді кроки й дані мають бути записані явно.
Follow-up question. "When should a checklist item become a test case?" When it is regularly skipped or checked in different ways, when it is going into automation, or when new people will run it. Then the steps and data must be written out explicitly.
TC-204 Partial refund returns the requested amount to the card
Preconditions:
- logged in as role "Support manager" (qa.support+01@example.com)
- payment pay_T204 of 120.00 EUR exists and is SETTLED
(POST /api/test-data/payments/pay_T204/settle on staging)
Steps:
1. Open Payments > pay_T204 > Refund
2. Enter 20.00, reason "Damaged item", click "Refund"
Expected:
- refund re_* created with amount 20.00 and status SUCCEEDED
- payment shows "Partially refunded, 100.00 EUR remaining"
Linked: REQ-PAY-31, autotest refunds.spec.ts > "partial refund"
Checklist: refunds (admin panel)
[ ] full refund [ ] partial refund [ ] refund > remaining amount rejected
[ ] second partial [ ] refund of unsettled payment blocked
[ ] refund reason required [ ] audit log entry created
SETTLED разом зі способом цей стан отримати, кроки містять конкретні значення, а очікуваний результат перевіряє одну поведінку з двох боків. Чек-лист нижче покриває ту саму фічу ширше, але без кроків — він розрахований на людину, яка знає продукт.SETTLED state together with how to reach it, the steps hold concrete values, and the expected result checks one behaviour from two sides. The checklist below covers the same feature more broadly but without steps — it is meant for someone who knows the product.Що це. Трасованість — це зв’язки між вимогою, тестами, що її перевіряють, результатами виконання й багами. Матриця трасованості (RTM) показує ці зв’язки таблицею: рядок — вимога, стовпці — ручні кейси, автотести, останній результат, відкриті баги. На практиці її рідко ведуть в Excel: у Jira з Xray або Zephyr вона будується із зв’язків «tests» і «is tested by», а автотести прив’язують до ID вимоги через анотації.
What it is. Traceability is the set of links between a requirement, the tests that check it, the execution results and the bugs. A requirements traceability matrix (RTM) shows those links as a table: a row per requirement, columns for manual cases, autotests, the latest result and open bugs. In practice it is rarely kept in Excel: in Jira with Xray or Zephyr it is built from the "tests" and "is tested by" links, and autotests are tied to the requirement id through annotations.
Навіщо. RTM відповідає на три питання перед релізом: чи кожна вимога покрита хоча б одним тестом, які вимоги мають падіння або відкриті баги, і що доведеться перетестувати, якщо вимога змінилася. Зв’язок бага з тестом і вимогою також показує, який тест мав би цей баг спіймати, — це основа для розбору «витеклих» дефектів.
Why it matters. An RTM answers three questions before a release: is every requirement covered by at least one test, which requirements have failures or open bugs, and what needs retesting if a requirement changes. Linking a bug to a test and a requirement also shows which test should have caught it — the basis for analysing leaked defects.
Приклад з проєкту. Перед релізом модуля рефандів ми побудували матрицю з Xray для 14 вимог. Дві вимоги не мали жодного тесту: обмеження на кількість часткових повернень і повідомлення клієнту листом. Для однієї з них тест за день знайшов баг — можна було зробити шосте часткове повернення попри ліміт у п’ять. Без матриці ці прогалини залишилися б непоміченими до першої скарги.
Example from a project. Before releasing the refunds module we built a matrix in Xray for 14 requirements. Two requirements had no tests at all: the limit on the number of partial refunds and the customer notification email. For one of them a test found a bug within a day — a sixth partial refund went through despite the limit of five. Without the matrix those gaps would have stayed invisible until the first complaint.
Підводні камені. Вести RTM вручну в таблиці, яка застаріває після першої зміни вимог. Вважати, що «покрито» означає «перевірено»: тест може бути прив’язаний, але ніколи не запускатися або падати місяцями. Дрібнити вимоги до рівня кнопок — матриця стає нечитабельною. Не зв’язувати баги з тестами, і тоді не видно, які вимоги реально мають проблеми.
Pitfalls. Keeping the RTM by hand in a spreadsheet that goes stale after the first requirement change. Assuming "covered" means "verified": a test may be linked but never run, or failing for months. Splitting requirements down to individual buttons — the matrix becomes unreadable. Not linking bugs to tests, so nobody sees which requirements actually have problems.
Уточнююче питання. «Коли RTM справді потрібна?» Завжди корисна, але обов’язкова в регульованих доменах (медицина, фінанси, авіація), де аудитор вимагає довести, що кожна вимога перевірена. В інших командах вистачає зв’язків у трекері й звіту покриття за епіком.
Follow-up question. "When is an RTM truly required?" It is always useful, but mandatory in regulated domains (healthcare, finance, aviation), where an auditor demands proof that every requirement was verified. Other teams can make do with tracker links and a coverage report per epic.
Requirement | Manual cases | Autotests | Last run | Open bugs
----------------+------------------+----------------------------------+------------+----------
REQ-PAY-31 | TC-204, TC-205 | refunds.spec.ts: partial refund | PASS 06-18 | -
REQ-PAY-32 | - | - | NOT RUN | - <- gap
REQ-PAY-33 | TC-210 | refunds.spec.ts: refund limit | FAIL 06-18 | PAY-877
REQ-PAY-34 | TC-212 | - | PASS 06-17 | -
// autotest tied to the requirement (Playwright annotation, read by the reporter)
test("refund limit: sixth partial refund is rejected", {
annotation: [{ type: "requirement", description: "REQ-PAY-33" },
{ type: "issue", description: "PAY-877" }],
}, async ({ request }) => { /* ... */ });
Що це. Acceptance criteria (AC) — перевірювані умови, за яких історія вважається виконаною, часто у форматі Given/When/Then. Definition of Ready (DoR) — домовленість команди, коли історію можна брати в спринт: є AC, макети, відомі залежності, узгоджені тестові дані, оцінка. QA бере участь у доопрацюванні історій (refinement, three amigos) і ставить питання про граничні випадки ще до початку розробки.
What it is. Acceptance criteria (AC) are testable conditions under which a story counts as done, often in Given/When/Then form. A Definition of Ready (DoR) is the team's agreement on when a story can enter a sprint: AC exist, designs are ready, dependencies are known, test data is agreed, it is estimated. QA takes part in story refinement (three amigos) and asks about edge cases before development starts.
Навіщо. Найдешевший баг — той, який не написали. Розмиті AC («користувач може повернути кошти») породжують баги, що насправді є різним розумінням вимоги, і суперечки «баг чи так задумано». Чіткі AC стають expected result у баг-репортах і тест-кейсах, а DoR не пускає в роботу історію, на яку тестувальнику не буде з чим звіряти поведінку.
Why it matters. The cheapest bug is the one never written. Vague AC ("the user can get a refund") breed bugs that are really different readings of the requirement, and "bug or by design" arguments. Clear AC become the expected result in bug reports and test cases, and the DoR keeps out of development any story a tester would have nothing to check behaviour against.
Приклад з проєкту. На refinement історії «часткове повернення коштів» я поставила чотири питання: що буде, якщо сума більша за залишок; скільки часткових повернень дозволено; чи можна повертати оплату, яка ще не пройшла settlement; чи отримує клієнт лист. Жодне не було описане. Відповіді стали чотирма AC, два з них потім спіймали баги ще на code review, бо розробник написав тести саме за ними. А в DoR ми додали пункт «граничні випадки обговорені з QA».
Example from a project. At refinement of the "partial refund" story I asked four questions: what happens if the amount exceeds the remainder; how many partial refunds are allowed; can an unsettled payment be refunded; does the customer get an email. None were described. The answers became four AC, and two of them later caught bugs as early as code review, because the developer wrote tests straight from them. We also added "edge cases discussed with QA" to the DoR.
Підводні камені. AC, які неможливо перевірити: «швидко», «зручно», «як у конкурентів» — без чисел і умов. DoR як бюрократичний чек-лист, який заповнюють формально. Писати AC лише для happy path. QA, яка вперше бачить історію, коли вона вже в статусі «готово до тестування».
Pitfalls. AC nobody can verify: "fast", "convenient", "like competitors" — no numbers, no conditions. A DoR treated as a bureaucratic checklist that gets ticked off formally. AC written only for the happy path. QA seeing the story for the first time once it is already "ready for testing".
Уточнююче питання. «Чим DoR відрізняється від DoD?» Definition of Ready — вхід: коли історію можна почати. Definition of Done — вихід: коли її можна вважати завершеною (код, рев’ю, тести, документація, деплой на стенд).
Follow-up question. "How does a DoR differ from a DoD?" The Definition of Ready is the entry gate: when a story can start. The Definition of Done is the exit gate: when it can count as finished (code, review, tests, documentation, deployed to an environment).
Story PAY-120: Support manager issues a partial refund
AC1 Given a settled payment of 120.00 EUR
When the manager refunds 20.00
Then a refund of 20.00 is created and 100.00 remains refundable
AC2 Given 100.00 remains refundable
When the manager enters 150.00
Then the refund is rejected with "Amount exceeds remaining 100.00"
AC3 Given 5 partial refunds already exist for a payment
When the manager tries a 6th
Then it is rejected with "Refund limit reached (5)"
AC4 Given an unsettled payment
Then the Refund button is disabled with a tooltip "Available after settlement"
Definition of Ready (team agreement)
[x] AC are testable, including at least one negative case
[x] edge cases discussed with QA (three amigos)
[x] designs linked; test data available on staging
[x] dependencies and feature flag named
Що це. Charter — коротка місія дослідницької сесії. Популярний шаблон з книжки Елізабет Гендріксон «Explore It!»: Explore (що досліджуємо) with (з якими ресурсами чи техніками) to discover (яку інформацію шукаємо). Сесію обмежують за часом (зазвичай 60–90 хвилин) і ведуть нотатки: що перевірено, що знайдено, які питання й ідеї для наступних сесій. Цей підхід відомий як session-based test management.
What it is. A charter is the short mission of an exploratory session. A popular template from Elisabeth Hendrickson's book "Explore It!": Explore (what we examine) with (which resources or techniques) to discover (what information we are after). The session is timeboxed (usually 60–90 minutes) and notes are kept: what was covered, what was found, questions and ideas for the next sessions. This approach is known as session-based test management.
Навіщо. Дослідницьке тестування без рамок легко перетворюється на випадкові кліки, про які нічого не можна звітувати. Charter дає фокус і межі, а нотатки роблять сесію видимою: видно, що покрито, скільки знайдено, де залишилися ризики. Для баг-репортів це означає, що кожна знахідка має контекст і кроки, записані в момент, коли їх ще пам’ятаєш.
Why it matters. Exploratory testing without a frame easily turns into random clicking nobody can report on. A charter gives focus and boundaries, and the notes make the session visible: what was covered, how much was found, where risks remain. For bug reports it means every finding has context and steps written down while you still remember them.
Приклад з проєкту. Перед запуском нового фільтра доставки я провела сесію за charter «Explore фільтр часу доставки with різними часовими поясами і змінами дати в браузері to discover помилки в доступних слотах». За 75 хвилин у нотатках з’явилося три баги, два питання до продукту і одна ідея для автотесту. Баги я оформила одразу після сесії, копіюючи кроки з нотаток: слоти на «завтра» зникали, якщо браузер був у поясі UTC−5 і час переходив за північ за UTC.
Example from a project. Before a new delivery time filter launched, I ran a session with the charter "Explore the delivery time filter with different time zones and browser date changes to discover errors in available slots". In 75 minutes my notes held three bugs, two questions for product and one idea for an autotest. I filed the bugs right after the session, copying steps from the notes: "tomorrow" slots disappeared when the browser was in UTC−5 and the time crossed midnight UTC.
Підводні камені. Писати charter настільки широким («Explore весь застосунок»), що він нічого не обмежує, або настільки вузьким, що це вже тест-кейс. Не вести нотаток і відновлювати кроки з пам’яті через день. Оформлювати баги посеред сесії й губити фокус — краще коротко занотувати й оформити після. Не проводити розбору сесії, де знахідки перетворюються на тікети і нові charters.
Pitfalls. A charter so broad ("Explore the whole app") it constrains nothing, or so narrow it is already a test case. Keeping no notes and reconstructing steps from memory the next day. Filing bugs mid-session and losing focus — better to jot them down briefly and file them afterwards. Skipping the debrief where findings become tickets and new charters.
Уточнююче питання. «Як звітувати про дослідницьке тестування?» Список сесій з charter, тривалістю, відсотком часу на тестування, налаштування й оформлення багів, а також знахідки з посиланнями на тікети. Це дає керованість без покрокових кейсів.
Follow-up question. "How do you report on exploratory testing?" A list of sessions with the charter, duration, the share of time spent on testing, setup and bug filing, plus findings linked to tickets. That gives control without step-by-step cases.
Charter: Explore the delivery time filter
with time zones UTC-5 / UTC+9 and browser date changes around midnight
to discover wrong or missing delivery slots
Timebox: 75 min Tester: O.K. Build: web 6.4.0 Env: staging-2
Notes
10:05 UTC-5, 18:50 local -> "Tomorrow" shows 6 slots, OK
10:12 UTC-5, 19:05 local (00:05 UTC) -> "Tomorrow" empty BUG-1
10:31 UTC+9 -> slots shown in UTC, not local time BUG-2
10:44 changing date in the picker keeps old slots for ~2 s BUG-3 (minor)
10:58 Q: should same-day slots close 2 h or 3 h before start? -> ask PO
11:10 idea: autotest with page.clock set to 23:59 UTC
Debrief: 3 bugs (DLV-501..503), 2 questions, 1 automation idea
Time split: testing 70% / setup 15% / bug notes 15%
Що це. Test summary report — підсумковий звіт про тестування релізу чи етапу для людей, які ухвалюють рішення. Він містить: обсяг (що тестували і що ні), результати виконання, відкриті баги за severity з ризиками, статус exit criteria і рекомендацію QA. Exit criteria — заздалегідь узгоджені умови завершення тестування, наприклад: немає відкритих Blocker і Critical, 100% smoke пройдено, не менше 95% регресії пройдено, усі Major мають рішення.
What it is. A test summary report is the closing report on testing a release or phase, written for decision makers. It contains: scope (what was and was not tested), execution results, open bugs by severity with their risks, the status of the exit criteria and QA's recommendation. Exit criteria are conditions agreed in advance for finishing testing, for example: no open Blocker or Critical bugs, 100% of smoke passed, at least 95% of regression passed, every Major has a decision.
Навіщо. Власник релізу не читатиме сотню тікетів — йому потрібна сторінка, з якої видно, чи можна випускати і з якими ризиками. Exit criteria роблять висновок перевірюваним, а не суб’єктивним «здається, все нормально». Список відкритих багів з обхідними шляхами одразу стає розділом known issues у release notes і підказкою для підтримки.
Why it matters. The release owner will not read a hundred tickets — they need one page that shows whether it can ship and with which risks. Exit criteria make the conclusion checkable rather than a subjective "seems fine". The list of open bugs with workarounds becomes the known issues section of the release notes and a heads-up for support.
Приклад з проєкту. Для релізу 4.13 мій звіт займав одну сторінку: 412 тест-кейсів, 96% пройдено, три відкриті Major з рішеннями і обхідними шляхами, два exit criteria виконано і один ні — не все з регресії мобільного вебу було виконано через недоступний пристрій iOS 16. Я не писала «реліз заблоковано», а описала ризик: частка iOS 16 — 4% сесій, останній успішний прогін на цій версії був для 4.12. Власник релізу вирішив випускати з пріоритетною перевіркою iOS 16 наступного ранку.
Example from a project. For release 4.13 my report was one page: 412 test cases, 96% passed, three open Major bugs with decisions and workarounds, two exit criteria met and one not — part of the mobile web regression was not run because the iOS 16 device was unavailable. I did not write "release blocked"; I described the risk: iOS 16 is 4% of sessions, and the last successful run on that version was for 4.12. The release owner decided to ship with a priority iOS 16 check the next morning.
Підводні камені. Звіт на десять сторінок з графіками, де не видно висновку. Тільки цифри «пройдено/впало» без переліку ризиків. Замовчувати, що не тестували — невидима прогалина небезпечніша за відомий ризик. Змінювати exit criteria заднім числом, щоб вони «виконались». Рекомендація QA, подана як остаточне рішення.
Pitfalls. A ten-page report full of charts where the conclusion is nowhere to be seen. Only pass/fail numbers with no list of risks. Staying silent about what was not tested — an invisible gap is more dangerous than a known risk. Rewriting the exit criteria after the fact so they are "met". QA's recommendation presented as the final decision.
Уточнююче питання. «Хто визначає exit criteria?» Їх узгоджують QA, розробка і продукт на етапі планування тестування і записують у тест-план. QA звітує, чи вони виконані, а рішення про відхилення ухвалює власник релізу.
Follow-up question. "Who defines the exit criteria?" QA, engineering and product agree on them during test planning and record them in the test plan. QA reports whether they are met, and the release owner decides on any deviation.
Test Summary Report - web 4.13.0 (staging-2, builds 4.13.0-rc1..rc3), 10-19 Jun
Scope: checkout, refunds (new), promo engine, mobile web; NOT tested: partner API v1 (frozen)
Execution: 412 cases - 396 passed / 9 failed / 7 not run (iOS 16 device unavailable)
Automation: 1,184 tests, 99.2% pass, 3 known flaky (QA-318, QA-322, QA-329)
Open bugs Blocker 0 | Critical 0 | Major 3 | Minor 11 | Trivial 6
PAY-877 Major 6th partial refund accepted fix in 4.13.1, limit enforced by support
SHOP-2207 Major Double discount, sale-only carts promo SPRING10 disabled for sale items
DLV-501 Major "Tomorrow" slots empty for UTC-5 known issue in release notes
Exit criteria
[x] no open Blocker/Critical
[x] smoke 100% passed on rc3
[ ] all regression cases for supported platforms executed: 405/412
(7 mobile web cases on iOS 16 not run)
[x] every Major has an owner and a decision
Recommendation: GO with risk - iOS 16 (4% of sessions) re-checked on 20 Jun 09:00;
decision owner: release manager