# Gaffer Documentation (Full) > Complete documentation for Gaffer, a test analytics platform. > Blog posts are listed as links; fetch any URL with an `.md` suffix for its markdown source. # Documentation --- ## Analytics & Metrics Source: https://gaffer.sh/docs/analytics/ Gaffer provides analytics to help you understand your test suite's health and identify problems before they slow your team down. Our metrics are designed around one principle: **helping developers stay productive**. ## The Problem with Test Suites As test suites grow, they often become a source of friction rather than confidence. Tests that randomly pass or fail waste developer time. Slow feedback loops block PRs. And without visibility into trends, problems compound silently until the whole suite becomes a burden. Gaffer's analytics give you the visibility to catch these issues early and keep your test suite working *for* you, not against you. ## Health Score The Health Score is a single number (0-100) that summarizes your test suite's overall condition. It's designed to answer the question: *"How much can I trust my tests right now?"* The score is calculated from three factors: - **Pass Rate** (60%) - Higher pass rates contribute positively - **Flaky Test Percentage** (30%) - Fewer flaky tests means a higher score - **Trend Direction** (10%) - Improving trends boost the score; declining trends reduce it Pass rate carries the most weight because it's the most direct signal of whether your tests are doing their job. A failing test suite needs attention regardless of other factors. Flaky tests still have significant impact at 30% because they erode trust and waste developer time even when the overall pass rate looks healthy. ### Score Ranges - **90-100** - Excellent. Your test suite is reliable and trustworthy. - **75-89** - Healthy. Minor issues to address, but tests are useful. - **50-74** - Needs Attention. Flaky tests or failures are impacting productivity. - **25-49** - At Risk. Significant issues undermining test value. - **0-24** - Critical. Tests may be causing more harm than good. ## Flaky Test Detection A test is marked as "flaky" when it flip-flops between passing and failing without any code changes. These tests are particularly harmful because they: - Force developers to re-run CI pipelines, wasting time and compute - Erode trust in the test suite ("it's probably just flaky, merge anyway") - Hide real failures in noise ### How We Calculate It We use a **flip rate** algorithm. For each test, we track the sequence of pass/fail results across runs. A "flip" occurs when a test changes from pass to fail (or vice versa) between consecutive runs. The flip rate is calculated as: ``` flip_rate = number_of_flips / (total_runs - 1) ``` For example, if a test has results [pass, fail, pass, pass, fail] over 5 runs, that's 3 flips in 4 transitions = 75% flip rate. By default, tests with a flip rate of 10% or higher are flagged as flaky. You can adjust this threshold in Settings > Analytics based on your team's tolerance. ### Minimum Sample Size To avoid false positives, we require at least **5 runs** before flagging a test as flaky. A single failure in 2 runs might be a real bug; a pattern across 5+ runs is a signal. ### A Note on "Flaky" vs "Unreliable" There's ongoing debate in the testing community about what "flaky" really means. Is it a race condition? A test environment issue? A genuine intermittent bug in production code? **Our perspective: it doesn't matter for developer productivity.** Whether a test fails randomly due to timing issues, external dependencies, or cosmic rays, the impact is the same - developers waste time investigating, re-running, and eventually ignoring it. Gaffer flags these tests so you can decide what to do: fix them, quarantine them, or delete them. The goal is to keep your test suite providing reliable signal, not noise. ## Pass Rate Pass rate is the percentage of tests that passed, calculated as: ``` pass_rate = passed_tests / (passed_tests + failed_tests) ``` Skipped tests are excluded from this calculation since they don't represent actual test execution. We track pass rate over 30 days and show the trend direction (improving, stable, or declining) to help you spot regressions early. ## Analytics Settings You can configure analytics behavior in Settings > Analytics: - **Flaky Threshold** - Adjust the flip rate percentage that triggers flaky detection (default: 10%) - **Manual Recompute** - Trigger an immediate analytics refresh instead of waiting for the next scheduled computation ## Data Freshness Analytics are pre-computed every 4 hours to ensure fast dashboard loading. When you upload new test reports, the data will be reflected in the next computation cycle. If you need immediate results (e.g., after uploading a batch of historical reports), use the "Compute now" button in Settings > Analytics. ## Health Alerts Gaffer can notify your team when your test suite's health changes. Alerts are evaluated every 4 hours alongside the regular analytics computation. ### Health Degradation Alerts When your health score drops across a label boundary (e.g., from "Healthy" to "Needs Attention"), Gaffer sends a notification with the previous and current scores. This catches regressions before they compound. ### New Flaky Test Alerts When new tests are detected as flaky for the first time, Gaffer sends a notification listing the newly flaky tests. This lets your team investigate while the cause is still fresh. ### Cooldown To avoid notification fatigue, each alert type has a 24-hour cooldown per project. If your health score degrades multiple times within 24 hours, you'll only receive the first notification. ### Notification Destinations Health alerts are sent to your configured notification destinations. Gaffer supports [Slack](/docs/integrations/slack/) and [Webhooks](/docs/integrations/webhooks/) for health alerts. Configure destinations in Settings > Notifications. ## Next Steps Analytics work best when you have consistent test data flowing in. If you haven't already: - [Set up the GitHub Action](/docs/guides/github-actions/) to automatically upload reports from CI - [Integrate the Upload API](/docs/upload-api/) into your custom pipeline --- ## README Badges Source: https://gaffer.sh/docs/badges/ import { Aside } from '@astrojs/starlight/components' Showcase your test health with dynamic badges powered by [shields.io](https://shields.io). Badges update automatically with each test run. ## Available Badges Gaffer provides three badge types: | Badge | Example | Description | |-------|---------|-------------| | **Tests** | ![Tests badge](https://img.shields.io/badge/tests-95%25%20passing-brightgreen) | Shows your test pass rate percentage | | **Coverage** | ![Coverage badge](https://img.shields.io/badge/coverage-87%25-green) | Shows your code coverage percentage | | **Flaky** | ![Flaky badge](https://img.shields.io/badge/flaky-none-brightgreen) | Shows the count of flaky tests detected | ## How It Works Badges use the [shields.io endpoint badge](https://shields.io/badges/endpoint-badge) format. Gaffer provides public JSON endpoints that shields.io fetches to render your badges dynamically. ### Badge Endpoints | Badge | Endpoint | |-------|----------| | Tests | `/api/badges/:projectId/health.json` | | Coverage | `/api/badges/:projectId/coverage.json` | | Flaky | `/api/badges/:projectId/flaky.json` | ## Getting Your Badge Code The easiest way to get badge embed code is from your project settings: 1. Go to your project in the Gaffer dashboard 2. Navigate to **Settings** → **Badges** 3. Choose your preferred badge style 4. Copy the Markdown or HTML code 5. Paste into your README ## Manual Setup You can also construct badge URLs manually. Replace `YOUR_PROJECT_ID` with your Gaffer project ID: ### Markdown ```markdown [![Tests](https://img.shields.io/endpoint?url=https://app.gaffer.sh/api/badges/YOUR_PROJECT_ID/health.json)](https://app.gaffer.sh/projects/YOUR_PROJECT_ID) [![Coverage](https://img.shields.io/endpoint?url=https://app.gaffer.sh/api/badges/YOUR_PROJECT_ID/coverage.json)](https://app.gaffer.sh/projects/YOUR_PROJECT_ID) [![Flaky](https://img.shields.io/endpoint?url=https://app.gaffer.sh/api/badges/YOUR_PROJECT_ID/flaky.json)](https://app.gaffer.sh/projects/YOUR_PROJECT_ID) ``` ### HTML ```html Tests ``` ## Badge Styles Shields.io supports multiple badge styles. Add the `style` parameter to customize: | Style | Example | Parameter | |-------|---------|-----------| | Flat (default) | ![Flat style](https://img.shields.io/badge/tests-95%25-brightgreen?style=flat) | `style=flat` | | Flat Square | ![Flat square style](https://img.shields.io/badge/tests-95%25-brightgreen?style=flat-square) | `style=flat-square` | | Plastic | ![Plastic style](https://img.shields.io/badge/tests-95%25-brightgreen?style=plastic) | `style=plastic` | | For the Badge | ![For the badge style](https://img.shields.io/badge/tests-95%25-brightgreen?style=for-the-badge) | `style=for-the-badge` | ## Badge Colors Badge colors automatically reflect your project's health using a 5-level scale: ### Tests Badge | Pass Rate | Color | |-----------|-------| | ≥90% | ![brightgreen](https://img.shields.io/badge/tests-95%25-brightgreen) | | ≥75% | ![green](https://img.shields.io/badge/tests-80%25-green) | | ≥50% | ![yellow](https://img.shields.io/badge/tests-60%25-yellow) | | ≥25% | ![orange](https://img.shields.io/badge/tests-30%25-orange) | | {'<'}25% | ![red](https://img.shields.io/badge/tests-10%25-red) | ### Coverage Badge | Coverage | Color | |----------|-------| | ≥80% | ![brightgreen](https://img.shields.io/badge/coverage-85%25-brightgreen) | | ≥60% | ![green](https://img.shields.io/badge/coverage-70%25-green) | | ≥40% | ![yellow](https://img.shields.io/badge/coverage-50%25-yellow) | | ≥20% | ![orange](https://img.shields.io/badge/coverage-25%25-orange) | | {'<'}20% | ![red](https://img.shields.io/badge/coverage-10%25-red) | ### Flaky Badge | Flaky Tests | Color | |-------------|-------| | 0 | ![brightgreen](https://img.shields.io/badge/flaky-none-brightgreen) | | 1-3 | ![yellow](https://img.shields.io/badge/flaky-2%20tests-yellow) | | 4-10 | ![orange](https://img.shields.io/badge/flaky-7%20tests-orange) | | >10 | ![red](https://img.shields.io/badge/flaky-15%20tests-red) | When no data is available (e.g., no test runs yet), badges display "N/A" with a neutral gray color. ## Caching Badge data is cached for 5 minutes to ensure fast loading times and reduce API load. After uploading new test results, badges will update within 5 minutes. ## Example Here's how badges might look in a README: **my-awesome-project** ![Tests](https://img.shields.io/badge/tests-98%25%20passing-brightgreen) ![Coverage](https://img.shields.io/badge/coverage-84%25-green) ![Flaky](https://img.shields.io/badge/flaky-none-brightgreen) A well-tested project with great coverage and no flaky tests! ## Next Steps - [Getting Started](/docs/getting-started/) - Set up your first project - [Analytics & Metrics](/docs/analytics/) - Understand your test health data - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack --- ## CLI Reference Source: https://gaffer.sh/docs/cli/ import { Aside } from '@astrojs/starlight/components' ## Installation Install via Homebrew (macOS, Linux): ```bash brew install gaffer-sh/tap/gaffer ``` Or via the install script (macOS, Linux): ```bash curl -fsSL https://app.gaffer.sh/install.sh | sh ``` The install script places the `gaffer` binary in `~/.local/bin`. Both methods support Linux (x86_64, aarch64) and macOS (Apple Silicon, Intel). For a guided walkthrough, see the [Getting Started guide](/docs/getting-started/). ## `gaffer test` Run a test command and analyze results. ```bash gaffer test -- npm test gaffer test -- pytest -x gaffer test -- go test ./... gaffer test -- cargo test ``` ### Flags | Flag | Env var | Description | |------|---------|-------------| | `--token ` | `GAFFER_TOKEN` | API token for cloud sync | | `--report ` / `-r ` | — | Report file path(s) to parse (repeatable) | | `--root ` | — | Project root directory (default: `.`) | | `--format ` | — | Output format (default: `human`) | | `--show-errors` | — | Show full error messages, stack traces, and context files for failed tests | | `--compare ` | — | Compare against the latest run on a branch (e.g. `--compare=main`) | | `--fail-on ` | — | Override exit code based on failure classification. `new` exits 0 when only pre-existing or flaky failures exist | | `--affected` | — | Derive the wrapped command from `affected-tests`. Use with `--files`. The trailing `-- ` is ignored when set | | `--files ` | — | Changed source files. Only meaningful with `--affected` (repeatable) | | `--no-graph` | — | With `--affected`, disable the import-graph strategy and fall back to naming + proximity heuristics | | `--no-cache` | — | With `--affected`, force an in-memory graph build instead of using `.gaffer/graph.db` | | `--on-empty ` | — | With `--affected`, behavior when no tests are affected. `auto` (default) exits 0 only when all signals were available; `skip` always exits 0; `fail` always exits non-zero | | `--api-url ` | `GAFFER_API_URL` | Override API endpoint | ### Behavior 1. Runs your command as a child process, passing through stdout/stderr 2. Discovers report files via glob patterns (config or defaults) 3. Parses test results and coverage reports 4. Computes health score, flaky tests, failure clusters, duration analysis 5. Classifies each failure as `new`, `pre_existing`, `flaky`, or `unknown` (auto-compares against the default branch) 6. Prints enriched summary to stderr 7. Syncs results to cloud (if token configured) 8. Exits with the child process's exit code (or overrides via `--fail-on`) ### Example output ``` gaffer 40 passed 2 failed 3 skipped 12.4s Health: 87 (good) ^ Slow: p95 245.3ms Flaky: 2 tests src/auth.test.ts > login — 40% flip rate (4/10 runs) src/api.test.ts > timeout handler — 20% flip rate (2/10 runs) Clusters: 1 pattern (3 tests) "Connection refused" — 3 tests New failures: 1 src/billing.test.ts > charge card Pre-existing: 1 src/db.test.ts > connection timeout Coverage: 78.5% lines (1234/1572) Synced: 1 run uploaded ``` ### Branch comparison Compare the current run against a baseline branch: ```bash gaffer test --compare=main -- npm test ``` ``` vs main: 2 new failures, 1 fixed, 3 pre-existing pass rate -5.0% duration +1.2s NEW src/auth.test.ts > login > OAuth redirect NEW src/billing.test.ts > charge card FIX src/api.test.ts > timeout handler ``` ### Failure classification Every failure is automatically classified by comparing against your default branch: | Classification | Meaning | |---------------|---------| | `new` | Failed now, passed on the baseline branch. Likely caused by your changes. | | `pre_existing` | Already failing on the baseline branch. Not your fault. | | `flaky` | Known flaky test (high flip rate in historical data). | | `unknown` | No baseline data available (first run or no runs on the default branch). | Classification runs automatically on every `gaffer test` invocation. The default branch is detected via git (falls back to `main`, then `master`). ### Smart exit codes Use `--fail-on=new` to exit 0 when only pre-existing or flaky failures exist: ```bash gaffer test --fail-on=new -- npm test ``` This is useful in CI to avoid blocking PRs on failures that existed before your changes. If a failure is classified as `unknown` (no baseline), it's treated as `new` for safety. Signal exits (e.g. SIGTERM killing the test process) always propagate regardless of `--fail-on`. ### JSON output Use `--format=json` to get machine-readable output on stdout: ```bash gaffer test --format=json -- npm test | jq .health.score ``` The JSON output includes a `classification` object with each failure's type: ```bash gaffer test --format=json -- npm test | jq '.classification.classified_failures[] | {name, classification}' ``` ### Run only affected tests `--affected` collapses the `affected-tests` + `gaffer test` agentic loop into one invocation. Pass the changed source files; Gaffer maps them to test files, scopes the runner, and parses results as usual. ```bash gaffer test --affected --files src/auth.ts src/api.ts ``` Use `--on-empty=auto` (default) to exit 0 only when all detection signals were available. When some signals were unavailable (degraded mode), `auto` exits non-zero so CI doesn't silently green-light a partial run. Use `--on-empty=skip` to always exit 0 or `--on-empty=fail` to always exit non-zero. The CLI currently reports `coverage_history` and `failure_history` as unavailable on every run (those signals require a future Gaffer-history connection), so `auto` will exit non-zero on any empty result today. If you want silent-skip on empty, pass `--on-empty=skip` explicitly. ## `gaffer affected-tests` Map changed source files to relevant test specs. Returns test files and a suggested run command. ```bash gaffer affected-tests --files src/auth.ts src/api.ts ``` ### Flags | Flag | Description | |------|-------------| | `--files ` | Source files that changed (required, repeatable) | | `--root ` | Project root directory (default: `.`) | | `--format ` | Output format (default: `json`) | | `--pretty` | Human-readable output to stderr. Equivalent to `--format human` | | `--no-graph` | Disable the import-graph strategy. Falls back to naming + proximity heuristics only. Faster on huge codebases at the cost of missing indirect dependencies | | `--no-cache` | Force an in-memory graph build instead of using `.gaffer/graph.db`. Useful for ephemeral CI runs and read-only filesystems | | `--print-cmd` | Print only the bare `run_command` string to stdout. Exit 1 when no command is available so `gaffer test -- $(gaffer affected-tests --files X --print-cmd)` fails fast on the empty case | ### Detection strategies | Strategy | Example | |----------|---------| | Naming convention | `src/auth.ts` finds `src/auth.test.ts`, `src/auth.spec.ts`, `src/__tests__/auth.test.ts` | | Directory proximity | `src/utils.ts` finds test files in sibling `__tests__/` or `tests/` directories | | Import graph | Reverse-reachability over the static import graph. First call walks the project; subsequent calls incrementally update files whose mtime has changed, persisted to `.gaffer/graph.db` | The import graph runs by default. Pass `--no-graph` to opt out. Results are deduplicated across strategies; the JSON payload reports which signals were attempted and which were unavailable so callers can detect degraded runs. ### Example output ```json { "affected": [ { "test_file": "src/auth.test.ts", "source_file": "src/auth.ts", "confidence": 0.97, "strategy": "naming_convention", "signals": [ { "strategy": "naming_convention", "confidence": 0.9 }, { "strategy": "import_graph", "confidence": 0.7 } ] }, { "test_file": "src/__tests__/api.test.ts", "source_file": "src/api.ts", "confidence": 0.3, "strategy": "directory_proximity", "signals": [ { "strategy": "directory_proximity", "confidence": 0.3 } ] } ], "run_command": "pnpm vitest src/auth.test.ts src/__tests__/api.test.ts", "framework": "vitest", "signals": { "attempted": ["naming_convention", "directory_proximity", "import_graph"], "unavailable": ["coverage_history", "failure_history"] } } ``` Per-test fields: `confidence` is the noisy-OR combination across signals, and `strategy` is the highest-confidence individual signal (kept flat for legacy consumers). The `signals` array carries every signal that selected the test with its per-signal confidence. The run command auto-detects your framework and package manager (pnpm, yarn, bun, or npm from lock files). `signals.unavailable` lists detection sources that weren't reachable on this run; when `affected` is empty and this list is non-empty, the result is degraded rather than confirmed-empty. ### Use with AI agents The integrated `gaffer test --affected` flag is the simplest path. To pipe through `affected-tests` directly, use `--print-cmd`: ```bash gaffer test -- $(gaffer affected-tests --files $(git diff --name-only main) --print-cmd) ``` `--print-cmd` exits 1 when no command is available, so the `gaffer test` invocation never runs with an empty wrapped command. ## `gaffer doctor` Diagnose common setup issues. Checks config, database, token validity, report discovery, framework detection, and CLI version. ```bash gaffer doctor ``` ``` gaffer doctor OK Config .gaffer/config.toml OK Database .gaffer/data.db (48KB), has data OK Token gaf_...x4f2 (valid, API reachable) OK Reports 12 files match current patterns OK Frameworks vitest (vitest.config.ts), playwright (playwright.config.ts) OK Version gaffer 0.1.0 ``` Useful as a first diagnostic step when tests fail to sync or report files aren't detected. Each check outputs OK, WARN, or FAIL with actionable detail. ## `gaffer init` Interactive project setup. ```bash gaffer init ``` Steps: 1. Detects test frameworks (Vitest, Playwright, Jest, pytest, Go, RSpec, .NET, Cargo, PHPUnit, Mocha) 2. Shows reporter setup instructions for each detected framework 3. Optionally authenticates via browser (creates API token) 4. Writes `.gaffer/config.toml` 5. Adds `.gaffer/` to `.gitignore` ## `gaffer query` Query local test intelligence without running tests. Output is JSON by default — use `--pretty` for human-readable. AI agents can access the same data via the [MCP server](/docs/mcp/). ### `gaffer query health` Health score, trend, and label. ```bash gaffer query health gaffer query health --pretty gaffer query health | jq .score ``` ### `gaffer query flaky` Flaky tests ranked by composite score. ```bash gaffer query flaky gaffer query flaky | jq '.[].test_name' ``` ### `gaffer query slowest` Top N slowest tests by duration. ```bash gaffer query slowest gaffer query slowest --limit 5 ``` ### `gaffer query runs` Recent test runs with pass/fail counts. ```bash gaffer query runs gaffer query runs --limit 5 ``` ### `gaffer query history ""` Pass/fail history for a specific test (name matched with LIKE). ```bash gaffer query history "login" gaffer query history "auth > login" --limit 10 ``` ### `gaffer query failures ""` Search failures across runs by test name or error message. ```bash gaffer query failures "timeout" gaffer query failures "connection refused" --limit 10 ``` ## `gaffer sync` Force-sync pending uploads. Use when a previous `gaffer test` run was interrupted before syncing, or to retry failed uploads. To upload reports without the CLI, see the [Upload API](/docs/upload-api/). ```bash gaffer sync gaffer sync --token gaf_xxx ``` ## Configuration Config file: `.gaffer/config.toml` (or `gaffer.toml` at project root) ```toml [project] token = "gaf_..." api_url = "https://app.gaffer.sh" [test] report_patterns = [ "**/.gaffer/reports/**/*.xml", "**/.gaffer/reports/**/*.json", "**/junit*.xml", "**/test-results/**/*.xml", "**/test-reports/**/*.xml", "**/target/nextest/**/*.xml", "**/ctrf/**/*.json", "**/ctrf-report.json", "**/coverage/lcov.info", "**/lcov.info", ] ``` **Resolution order:** CLI flags > environment variables > config file > defaults. **Config discovery:** Walks up from the working directory looking for `.gaffer/config.toml` or `gaffer.toml`. The directory containing the config becomes the project root. ## Environment variables | Variable | Purpose | |----------|---------| | `GAFFER_TOKEN` | API token for cloud sync (overridden by `--token`) | | `GAFFER_API_URL` | API endpoint URL (overridden by `--api-url`) | ## Default report patterns When no `--report` flag or `report_patterns` config is set, Gaffer auto-discovers: - `**/.gaffer/reports/**/*.xml` — Gaffer's own report directory - `**/.gaffer/reports/**/*.json` — Gaffer's own report directory - `**/junit*.xml` — JUnit XML reports - `**/test-results/**/*.xml` — Common test result directories - `**/test-reports/**/*.xml` — Common test report directories - `**/target/nextest/**/*.xml` — Cargo nextest JUnit output - `**/ctrf/**/*.json` — CTRF JSON reports - `**/ctrf-report.json` — Default CTRF output - `**/coverage/lcov.info` — Default coverage output - `**/lcov.info` — Root-level coverage --- ## Coverage Reports (Beta) Source: https://gaffer.sh/docs/coverage/ import { Aside } from '@astrojs/starlight/components' Gaffer can track your code coverage metrics alongside your test results. Upload coverage reports to see coverage trends, identify gaps, and monitor improvements over time. ## Supported Formats | Format | Status | File Extension | |--------|--------|---------------| | **LCOV** | Supported | `lcov.info`, `.lcov` | | **Cobertura XML** | Supported | `coverage.xml`, `cobertura.xml` | | **JaCoCo XML** | Supported | `jacoco.xml` | | **Clover XML** | Supported | `clover.xml` | ## Upload API Use the standard [Upload API](/docs/upload-api/) to send coverage files. The only difference is the file type being uploaded. | Field | Required | Description | |-------|----------|-------------| | `files` | Yes | Your coverage file (e.g., `coverage/lcov.info`, `coverage.xml`) | | `tags.commitSha` | Recommended | Git commit SHA for tracking coverage per commit | | `tags.branch` | Recommended | Git branch name for filtering coverage by branch | | `tags.type` | Optional | Set to `"coverage"` for clarity | ### Example Request ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@coverage/lcov.info" \ -F 'tags={"commitSha":"abc123","branch":"main","type":"coverage"}' ``` ## Generating LCOV Reports Most test frameworks support LCOV output. Here's how to configure common tools: ### Vitest ```typescript title="vitest.config.ts" import { defineConfig } from 'vitest/config' export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['lcov', 'text'], reportsDirectory: './coverage' } } }) ``` Run tests with: `pnpm vitest --coverage` ### Jest ```javascript title="jest.config.js" module.exports = { collectCoverage: true, coverageReporters: ['lcov', 'text'], coverageDirectory: './coverage' } ``` Run tests with: `pnpm jest --coverage` ### pytest Install pytest-cov: `pip install pytest-cov` ```bash title="pyproject.toml" [tool.pytest.ini_options] addopts = "--cov=src --cov-report=lcov:coverage/lcov.info" ``` Run tests with: `pytest` ## CI Integration ### GitHub Actions ```yaml title=".github/workflows/test.yml" name: Tests with Coverage on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests with coverage run: pnpm test --coverage - name: Upload coverage to Gaffer run: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: ${{ secrets.GAFFER_TOKEN }}" \ -F "files=@coverage/lcov.info" \ -F 'tags={"commitSha":"${{ github.sha }}","branch":"${{ github.ref_name }}","type":"coverage"}' ``` ### GitLab CI ```yaml title=".gitlab-ci.yml" test: stage: test script: - pnpm test --coverage - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_TOKEN" \ -F "files=@coverage/lcov.info" \ -F "tags={\"commitSha\":\"$CI_COMMIT_SHA\",\"branch\":\"$CI_COMMIT_REF_NAME\",\"type\":\"coverage\"}" artifacts: reports: coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml ``` ## Viewing Coverage Once you've uploaded coverage reports, you can view them in the Gaffer dashboard: - **Project Analytics** - See coverage trends over time in the analytics tab - **Test Run Details** - View coverage associated with specific test runs - **File Breakdown** - Drill down into per-file coverage metrics ## GitHub Commit Status Gating Gaffer can report coverage as a GitHub commit status on pull requests, optionally failing the status if coverage doesn't meet your criteria. This requires the Gaffer GitHub App installed and a repository linked to your project (configured under Project Settings > GitHub). Configure this at **Project Settings > GitHub > Coverage Status Settings**. Three modes are available: | Mode | Behavior | |------|----------| | **Always Pass** (default) | Posts coverage as an informational status. The check always passes regardless of the coverage value. | | **Threshold** | Fails the status if coverage is below a configured percentage (e.g., 80%). Useful for enforcing a minimum coverage floor. | | **Delta** | Fails the status if coverage drops by more than a configured percentage compared to the default branch (e.g., fails if coverage decreases by more than 5%). Useful for preventing regressions without requiring a fixed minimum. | You can choose which metric to gate on: **line coverage** or **branch coverage**. Delta mode compares the PR's coverage against the most recent coverage data from the repository's default branch. If no default branch coverage exists yet, it falls back to the most recent coverage report from any branch. If no previous coverage data exists at all, the delta check passes. ## Metrics Tracked | Metric | Description | |--------|-------------| | **Line Coverage** | Percentage of executable lines covered by tests | | **Branch Coverage** | Percentage of code branches (if/else) covered | | **Function Coverage** | Percentage of functions called during tests | ## Best Practices - **Upload with every test run** - Consistent uploads enable accurate trend tracking - **Include commit SHA** - Links coverage to specific code changes - **Set a baseline** - Track coverage over time to catch regressions - **Focus on meaningful coverage** - High coverage doesn't always mean quality tests ## Troubleshooting ### No coverage data showing - Verify the coverage file exists and contains data - Check that the file uses a supported format (LCOV, Cobertura, JaCoCo, or Clover) - Ensure the upload request succeeded (check for 201 response) ### Coverage seems incorrect - Some tools report different metrics - Gaffer uses the LCOV-reported values - Verify source maps are correctly configured if using transpiled code ## Next Steps - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Analytics & Metrics](/docs/analytics/) - Understanding your test metrics - [Live Demo](https://app.gaffer.sh/demo/) - See coverage tracking in action --- ## Getting Started Source: https://gaffer.sh/docs/getting-started/ import { Aside, LinkCard, CardGrid } from '@astrojs/starlight/components' ## Install the CLI Homebrew (macOS, Linux): ```bash brew install gaffer-sh/tap/gaffer ``` Or via the install script (macOS, Linux): ```bash curl -fsSL https://app.gaffer.sh/install.sh | sh ``` The script installs the `gaffer` binary to `~/.local/bin` and detects your OS and architecture automatically. ## Initialize your project Run `gaffer init` in your project root: ```bash gaffer init ``` The setup wizard will: 1. **Detect your test frameworks** — Vitest, Playwright, Jest, pytest, Go, RSpec 2. **Show reporter setup instructions** — so your framework outputs a format Gaffer can parse (JUnit XML, CTRF, or native) 3. **Authenticate via browser** (optional) — creates an API token for syncing results to the dashboard 4. **Write `.gaffer/config.toml`** — stores your token and report patterns 5. **Add `.gaffer/` to `.gitignore`** ## Run your tests Wrap your existing test command with `gaffer test`: ```bash gaffer test -- npm test ``` Gaffer runs your command, parses the results, and prints an enriched summary: ``` gaffer 40 passed 2 failed 3 skipped 12.4s Health: 87 (good) ^ Slow: p95 245.3ms Flaky: 2 tests src/auth.test.ts > login — 40% flip rate (4/10 runs) src/api.test.ts > timeout handler — 20% flip rate (2/10 runs) Coverage: 78.5% lines (1234/1572) Synced: 1 run uploaded ``` Works with any test command — `pytest`, `go test ./...`, `cargo test`, `pnpm test`, etc. ## Sync to the dashboard If you authenticated during `gaffer init`, results sync automatically after each run. If you skipped that step: 1. [Sign up](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) and create a project 2. Copy the API token from your project settings 3. Add it to your config: ```bash gaffer init # re-run to authenticate via browser ``` Or set the token directly: ```toml # .gaffer/config.toml [project] token = "gaf_..." ``` Once synced, your team can view test history, trends, and analytics in the [dashboard](https://app.gaffer.sh/demo). ## Add to CI Install the CLI and set `GAFFER_TOKEN` as a secret. Example for GitHub Actions (see the full [GitHub Actions guide](/docs/guides/github-actions/) for more options): ```yaml - name: Install Gaffer CLI run: curl -fsSL https://app.gaffer.sh/install.sh | sh - name: Run tests run: gaffer test -- npm test env: GAFFER_TOKEN: ${{ secrets.GAFFER_TOKEN }} ``` ## Next steps --- ## Azure DevOps Source: https://gaffer.sh/docs/guides/azure-devops/ import { Aside } from '@astrojs/starlight/components'; Azure DevOps Pipelines is Microsoft's CI/CD solution for cloud and on-premises deployments. With Gaffer, you can automatically upload and share test reports from your Azure Pipelines. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - An Azure DevOps project with a pipeline configured ## Setup ### 1. Add your project token as a pipeline variable 1. Go to your Azure DevOps project 2. Navigate to **Pipelines** → select your pipeline → **Edit** 3. Click **Variables** → **New variable** 4. Name: `GAFFER_PROJECT_TOKEN` 5. Value: Your Gaffer project token 6. Check **Keep this value secret** 7. Click **OK** and **Save** ### 2. Add the upload step to your pipeline ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: NodeTool@0 inputs: versionSpec: '20.x' - script: npm ci displayName: 'Install dependencies' - script: npm test displayName: 'Run tests' - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)"}' displayName: 'Upload to Gaffer' condition: always() ``` ## Environment Variables Azure DevOps provides these predefined variables: | Variable | Description | Example | |----------|-------------|---------| | `$(Build.SourceVersion)` | Full commit SHA | `abc123def456...` | | `$(Build.SourceBranchName)` | Branch name (short) | `main`, `feature/login` | | `$(Build.SourceBranch)` | Full branch ref | `refs/heads/main` | | `$(System.PullRequest.SourceBranch)` | PR source branch | `refs/heads/feature/login` | | `$(Build.BuildNumber)` | Build number | `20231215.1` | | `$(Build.Repository.Name)` | Repository name | `my-app` | ## Examples ### Playwright ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: NodeTool@0 inputs: versionSpec: '20.x' - script: npm ci displayName: 'Install dependencies' - script: npx playwright install --with-deps displayName: 'Install Playwright browsers' - script: npx playwright test displayName: 'Run Playwright tests' - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)","test_framework":"playwright","test_suite":"e2e"}' displayName: 'Upload to Gaffer' condition: always() - publish: playwright-report artifact: playwright-report condition: always() ``` ### Jest with JUnit Reporter ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: NodeTool@0 inputs: versionSpec: '20.x' - script: npm ci displayName: 'Install dependencies' - script: npm test -- --reporters=default --reporters=jest-junit displayName: 'Run Jest tests' env: JEST_JUNIT_OUTPUT_DIR: ./test-results - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)","test_framework":"jest"}' displayName: 'Upload to Gaffer' condition: always() - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' testResultsFiles: '**/junit.xml' condition: always() ``` ### pytest ```yaml trigger: - main pool: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 inputs: versionSpec: '3.11' - script: pip install pytest pytest-html displayName: 'Install dependencies' - script: pytest --html=report.html --self-contained-html displayName: 'Run pytest' - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@report.html" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)","test_framework":"pytest"}' displayName: 'Upload to Gaffer' condition: always() - publish: report.html artifact: pytest-report condition: always() ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```yaml # Install the CTRF reporter for your framework: # npm install --save-dev jest-ctrf-json-reporter # npm install --save-dev playwright-ctrf-json-reporter # npm install --save-dev vitest-ctrf-json-reporter - script: npm install --save-dev jest-ctrf-json-reporter displayName: 'Install CTRF reporter' - script: npm test -- --reporter=jest-ctrf-json-reporter displayName: 'Run tests with CTRF' - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)"}' displayName: 'Upload CTRF to Gaffer' condition: always() ``` ## Pull Request Pipelines For PR pipelines, use the source branch: ```yaml trigger: none pr: - main steps: - script: npm ci && npm test displayName: 'Run tests' - script: | # Extract clean branch name from PR source BRANCH_NAME=$(echo "$(System.PullRequest.SourceBranch)" | sed 's|refs/heads/||') curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"'"$BRANCH_NAME"'"}' displayName: 'Upload to Gaffer' condition: always() ``` ## Troubleshooting ### Report not uploading - Verify `GAFFER_PROJECT_TOKEN` is set as a secret variable - Ensure `condition: always()` is set on the upload step - Check the file path is correct ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the variable name matches exactly (case-sensitive) ### Variable syntax Azure DevOps uses `$(VariableName)` syntax, not `$VARIABLE_NAME`. Make sure to use the correct format. ### Branch name includes refs/heads/ Use `$(Build.SourceBranchName)` instead of `$(Build.SourceBranch)` for clean branch names. ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitHub Actions](/docs/guides/github-actions/) · [GitLab CI](/docs/guides/gitlab-ci/) · [CircleCI](/docs/guides/circleci/) · [Jenkins](/docs/guides/jenkins/) · [Bitbucket](/docs/guides/bitbucket-pipelines/) --- ## Bitbucket Pipelines Source: https://gaffer.sh/docs/guides/bitbucket-pipelines/ import { Aside } from '@astrojs/starlight/components'; Bitbucket Pipelines is Atlassian's integrated CI/CD solution for Bitbucket Cloud. With Gaffer, you can automatically upload and share test reports from your pipelines. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - A Bitbucket repository with a `bitbucket-pipelines.yml` file ## Setup ### 1. Add your project token as a repository variable 1. Go to your Bitbucket repository 2. Navigate to **Repository settings** → **Pipelines** → **Repository variables** 3. Add a new variable: - Name: `GAFFER_PROJECT_TOKEN` - Value: Your Gaffer project token - Check **Secured** to hide it in logs ### 2. Add the upload step to your pipeline ```yaml image: node:20 pipelines: default: - step: name: Test caches: - node script: - npm ci - npm test after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'"}' ``` ## Environment Variables Bitbucket Pipelines provides these variables: | Variable | Description | Example | |----------|-------------|---------| | `$BITBUCKET_COMMIT` | Full commit SHA | `abc123def456...` | | `$BITBUCKET_BRANCH` | Branch name | `main`, `feature/login` | | `$BITBUCKET_PR_ID` | Pull request ID (if applicable) | `42` | | `$BITBUCKET_PR_DESTINATION_BRANCH` | PR target branch | `main` | | `$BITBUCKET_REPO_SLUG` | Repository slug | `my-app` | | `$BITBUCKET_BUILD_NUMBER` | Build number | `123` | ## Examples ### Playwright ```yaml image: mcr.microsoft.com/playwright:v1.40.0-jammy pipelines: default: - step: name: Playwright Tests script: - npm ci - npx playwright test after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'","test_framework":"playwright","test_suite":"e2e"}' artifacts: - playwright-report/** ``` ### Jest with JUnit Reporter ```yaml image: node:20 pipelines: default: - step: name: Jest Tests caches: - node script: - npm ci - npm test -- --reporters=default --reporters=jest-junit after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@junit.xml" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'","test_framework":"jest"}' ``` ### pytest ```yaml image: python:3.11 pipelines: default: - step: name: pytest script: - pip install pytest pytest-html - pytest --html=report.html --self-contained-html after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@report.html" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'","test_framework":"pytest"}' artifacts: - report.html ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```yaml - step: name: Test with CTRF script: - npm ci # Install the CTRF reporter for your framework: # npm install --save-dev jest-ctrf-json-reporter # npm install --save-dev playwright-ctrf-json-reporter # npm install --save-dev vitest-ctrf-json-reporter - npm install --save-dev jest-ctrf-json-reporter - npm test -- --reporter=jest-ctrf-json-reporter after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'"}' ``` ## Pull Request Pipelines For pull request pipelines, include the PR information: ```yaml pipelines: pull-requests: '**': - step: name: PR Tests script: - npm ci - npm test after-script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$BITBUCKET_COMMIT"'","branch":"'"$BITBUCKET_BRANCH"'","pr":"'"$BITBUCKET_PR_ID"'"}' ``` ## Troubleshooting ### Report not uploading - Verify `GAFFER_PROJECT_TOKEN` is set in repository variables - Check the variable is marked as **Secured** - Ensure `after-script` is used (runs even when tests fail) ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the token is correctly copied (no extra spaces) ### Variable not available Some variables like `$BITBUCKET_PR_ID` are only available in pull request pipelines. Check the [Bitbucket variables documentation](https://support.atlassian.com/bitbucket-cloud/docs/variables-and-secrets/) for availability. ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitHub Actions](/docs/guides/github-actions/) · [GitLab CI](/docs/guides/gitlab-ci/) · [CircleCI](/docs/guides/circleci/) · [Jenkins](/docs/guides/jenkins/) · [Azure DevOps](/docs/guides/azure-devops/) --- ## CircleCI Source: https://gaffer.sh/docs/guides/circleci/ import { Aside } from '@astrojs/starlight/components'; CircleCI is a popular CI/CD platform with powerful caching and parallelism features. With Gaffer, you can automatically upload and share test reports from your CircleCI pipelines. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - A CircleCI project with a `config.yml` file ## Setup ### 1. Add your project token as an environment variable 1. Go to your CircleCI project settings 2. Navigate to **Environment Variables** 3. Click **Add Environment Variable** 4. Name: `GAFFER_PROJECT_TOKEN` 5. Value: Your Gaffer project token ### 2. Add the upload step to your config ```yaml version: 2.1 jobs: test: docker: - image: cimg/node:20.0 steps: - checkout - run: name: Install dependencies command: npm ci - run: name: Run tests command: npm test - run: name: Upload to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'"}' - store_test_results: path: test-results workflows: test: jobs: - test ``` ## Environment Variables CircleCI provides these variables for your jobs: | Variable | Description | Example | |----------|-------------|---------| | `$CIRCLE_SHA1` | Full commit SHA | `abc123def456...` | | `$CIRCLE_BRANCH` | Branch name | `main`, `feature/login` | | `$CIRCLE_PR_NUMBER` | Pull request number (if applicable) | `42` | | `$CIRCLE_PROJECT_REPONAME` | Repository name | `my-app` | | `$CIRCLE_BUILD_NUM` | Build number | `123` | ## Examples ### Playwright ```yaml version: 2.1 jobs: playwright: docker: - image: mcr.microsoft.com/playwright:v1.40.0-jammy steps: - checkout - run: name: Install dependencies command: npm ci - run: name: Run Playwright tests command: npx playwright test - run: name: Upload to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'","test_framework":"playwright","test_suite":"e2e"}' - store_artifacts: path: playwright-report ``` ### Jest with JUnit Reporter ```yaml version: 2.1 jobs: test: docker: - image: cimg/node:20.0 steps: - checkout - run: name: Install dependencies command: npm ci - run: name: Run Jest tests command: npm test -- --reporters=default --reporters=jest-junit environment: JEST_JUNIT_OUTPUT_DIR: ./test-results - run: name: Upload to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'","test_framework":"jest"}' - store_test_results: path: test-results ``` ### pytest ```yaml version: 2.1 jobs: pytest: docker: - image: cimg/python:3.11 steps: - checkout - run: name: Install dependencies command: pip install pytest pytest-html - run: name: Run pytest command: pytest --html=report.html --self-contained-html - run: name: Upload to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@report.html" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'","test_framework":"pytest"}' - store_artifacts: path: report.html ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```yaml - run: name: Install CTRF reporter # Choose the reporter for your framework: # npm install --save-dev jest-ctrf-json-reporter # npm install --save-dev playwright-ctrf-json-reporter # npm install --save-dev vitest-ctrf-json-reporter command: npm install --save-dev jest-ctrf-json-reporter - run: name: Run tests with CTRF command: npm test -- --reporter=jest-ctrf-json-reporter - run: name: Upload CTRF to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'"}' ``` ## Parallel Test Runs When using CircleCI's parallelism feature, you can upload from each container: ```yaml jobs: test: parallelism: 4 steps: - run: name: Run tests command: | circleci tests glob "**/*.spec.js" | circleci tests split | xargs npm test -- - run: name: Upload to Gaffer when: always command: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$CIRCLE_SHA1"'","branch":"'"$CIRCLE_BRANCH"'","container":"'"$CIRCLE_NODE_INDEX"'"}' ``` ## Troubleshooting ### Report not uploading - Verify `GAFFER_PROJECT_TOKEN` is set in project environment variables - Ensure `when: always` is set so the step runs even when tests fail - Check the file path is correct relative to the working directory ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the token is correctly copied (no extra spaces) ### Missing branch name - Make sure you're using `$CIRCLE_BRANCH` (not `$CIRCLE_PR_BRANCH`) - For forks, the branch may be available in different variables ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitHub Actions](/docs/guides/github-actions/) · [GitLab CI](/docs/guides/gitlab-ci/) · [Jenkins](/docs/guides/jenkins/) · [Bitbucket](/docs/guides/bitbucket-pipelines/) · [Azure DevOps](/docs/guides/azure-devops/) --- ## CTRF JSON Reporter Setup: Fix ENOENT Errors & Upload Reports Source: https://gaffer.sh/docs/guides/ctrf/ import { Aside } from '@astrojs/starlight/components'; CTRF (Common Test Report Format) is a universal JSON schema for test results. It provides a standardized way to report test outcomes across any testing tool, framework, or language. ## Why Use CTRF? - **Universal**: Works with any test framework that has a CTRF reporter - **Consistent**: Same format across Jest, Playwright, pytest, RSpec, and more - **Rich data**: Includes timing, retries, flaky test detection, and metadata - **Open standard**: Community-driven, MIT-licensed specification at [ctrf.io](https://ctrf.io) ## Supported Frameworks CTRF has reporters for most popular test frameworks: | Framework | Package | Language | |-----------|---------|----------| | Playwright | [`playwright-ctrf-json-reporter`](https://www.npmjs.com/package/playwright-ctrf-json-reporter) | JavaScript/TypeScript | | Jest | [`jest-ctrf-json-reporter`](https://www.npmjs.com/package/jest-ctrf-json-reporter) | JavaScript/TypeScript | | Vitest | [`vitest-ctrf-json-reporter`](https://www.npmjs.com/package/vitest-ctrf-json-reporter) | JavaScript/TypeScript | | Cypress | [`cypress-ctrf-json-reporter`](https://www.npmjs.com/package/cypress-ctrf-json-reporter) | JavaScript/TypeScript | | Mocha | [`mocha-ctrf-json-reporter`](https://www.npmjs.com/package/mocha-ctrf-json-reporter) | JavaScript/TypeScript | | pytest | [`pytest-ctrf`](https://pypi.org/project/pytest-ctrf/) | Python | | Go | [`ctrf-go-json-reporter`](https://github.com/ctrf-io/go-ctrf-json-reporter) | Go | | JUnit | [`junit-json-reporter-ctrf`](https://github.com/ctrf-io/junit-json-reporter-ctrf) | Java | See the full list at [ctrf.io](https://ctrf.io). ## Installation ### Playwright ```bash npm install playwright-ctrf-json-reporter --save-dev ``` ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ reporter: [ ['playwright-ctrf-json-reporter', { outputFile: 'ctrf-report.json' }], ['list'], // Also show in console ], }); ``` ### Jest ```bash npm install jest-ctrf-json-reporter --save-dev ``` ```javascript // jest.config.js module.exports = { reporters: [ 'default', ['jest-ctrf-json-reporter', { outputFile: 'ctrf-report.json' }], ], }; ``` ### Vitest ```bash npm install vitest-ctrf-json-reporter --save-dev ``` ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { reporters: ['default', 'vitest-ctrf-json-reporter'], }, }); ``` ### pytest ```bash pip install pytest-ctrf ``` ```bash pytest --ctrf ctrf-report.json ``` ## Troubleshooting ### `ENOENT: no such file or directory` writing the CTRF report ``` ENOENT: no such file or directory, open 'ctrf/coverage/report.json' ``` The fix depends on which reporter you're using, because the CTRF packages don't share an option shape. **Jest (`jest-ctrf-json-reporter`)** uses two separate options: `outputDir` (the directory) and `outputFile` (the filename only, default `ctrf-report.json`). The reporter calls `mkdirSync(outputDir, { recursive: true })` for you, so a nested `outputDir` works as long as it's actually passed via that option. The common mistake is collapsing both into `outputFile`: ```javascript // jest.config.js // ❌ Wrong: outputFile is filename-only; the path separators are not understood as a directory tree ['jest-ctrf-json-reporter', { outputFile: 'ctrf/coverage/report.json' }] // ✅ Correct: outputDir for the directory (will be created), outputFile for the filename ['jest-ctrf-json-reporter', { outputDir: 'ctrf/coverage', outputFile: 'report.json' }] ``` **Playwright (`playwright-ctrf-json-reporter`)** takes a single `outputFile` that is a full path, and does **not** create the parent directory. Either ensure the directory exists in CI before the run, or write to a flat path: ```yaml # Option A: pre-create the directory in CI - name: Ensure CTRF output directory exists run: mkdir -p ctrf/coverage - name: Run tests run: npx playwright test ``` ```typescript // Option B: use a flat path // playwright.config.ts reporter: [ ['playwright-ctrf-json-reporter', { outputFile: 'ctrf-report.json' }], ['list'], ], ``` If you're using a different CTRF reporter, check its README for whether the directory is created automatically, or default to the pre-`mkdir -p` step. ### `tests` array missing or empty If the report is generated but Gaffer reports zero tests parsed, the reporter probably wasn't invoked. - **Default reporter overridden.** The `reporters` array in `jest.config.js` (or the equivalent in your framework) replaces the default. Make sure both `'default'` and the CTRF reporter are listed, otherwise tests run but no CTRF file is written. - **Tests crashed before reporting.** If the suite throws during setup (a `beforeAll` that fails to connect to a service, for example), some reporters bail before writing the file. Check the CI logs for the underlying error before assuming the reporter is broken. ## Uploading CTRF Reports Once you have a CTRF JSON file, upload it to Gaffer: ```bash curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"abc123","branch":"main"}' ``` ### GitHub Actions ```yaml - name: Run tests run: npm test - name: Upload CTRF report to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./ctrf-report.json commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` ### GitLab CI ```yaml test: script: - npm ci - npm test after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ## CTRF Report Structure A CTRF report contains standardized test result data: ```json { "results": { "tool": { "name": "playwright" }, "summary": { "tests": 42, "passed": 40, "failed": 1, "pending": 0, "skipped": 1, "other": 0, "start": 1703520000000, "stop": 1703520060000 }, "tests": [ { "name": "should login successfully", "status": "passed", "duration": 1234, "retries": 0, "flaky": false }, { "name": "should display dashboard", "status": "failed", "duration": 5678, "message": "Element not found: #dashboard" } ] } } ``` ## Benefits with Gaffer When you upload CTRF reports to Gaffer, you get: - **Structured analytics**: Pass rates, duration trends, flaky test detection - **Cross-framework comparison**: Compare results from different test suites - **Failure patterns**: See which tests fail most frequently - **Historical tracking**: See how tests perform over time ## Next Steps **CI Provider Guides:** - [GitHub Actions](/docs/guides/github-actions/) - Use the official Gaffer Action - [GitLab CI](/docs/guides/gitlab-ci/) - GitLab pipeline integration - [CircleCI](/docs/guides/circleci/) - CircleCI workflow integration - [Jenkins](/docs/guides/jenkins/) - Jenkins pipeline integration - [Bitbucket Pipelines](/docs/guides/bitbucket-pipelines/) - Bitbucket integration - [Azure DevOps](/docs/guides/azure-devops/) - Azure Pipelines integration **Reference:** - [Upload API](/docs/upload-api/) - Full API documentation - [cURL Guide](/docs/guides/curl/) - Manual uploads and debugging ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Upload your first CTRF report in under 5 minutes. [Start Uploading CTRF Reports - Free](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) --- ## cURL Guide Source: https://gaffer.sh/docs/guides/curl/ import { Aside } from '@astrojs/starlight/components' This guide shows how to upload test reports to Gaffer using cURL from the command line. This is useful for quick uploads, debugging, or integrating with custom CI/CD systems. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - cURL installed (available by default on macOS and most Linux distributions) ## Basic Upload The simplest upload with just a file: ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@path/to/test-report.html" ``` Replace `YOUR_PROJECT_TOKEN` with your actual project token, and update the file path to point to your test report. ## Upload with Tags Add metadata tags to help organize your test runs. We strongly recommend including `commitSha` and `branch`: ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"abc123def456","branch":"main","test_framework":"playwright","test_suite":"e2e"}' ``` ## Upload Multiple Files Upload multiple files by repeating the `-F "files=..."` flag: ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F "files=@playwright-report/data/report.json" \ -F "files=@playwright-report/screenshots/failed-test.png" \ -F 'tags={"commitSha":"abc123","branch":"feature/login"}' ``` ## Dynamic Git Information You can use shell command substitution to automatically include git information: ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$(git rev-parse HEAD)"'","branch":"'"$(git branch --show-current)"'"}' ``` ## Common Issues ### 401 Unauthorized Check that your project token is correct and properly formatted. The token should start with `gfr_`. ### 400 Bad Request - No files provided Ensure you're using `-F "files=@..."` with the `@` symbol before the file path. The `@` tells cURL to read the file contents rather than using the path as a string value. ### 400 Bad Request - File too large The `/api/upload` endpoint is capped at 100 MB total per request by the Cloudflare edge. For uploads above that, use the [`gaffer` CLI](/docs/cli/) or [`gaffer-uploader@v2` GitHub Action](/docs/guides/github-actions/), which automatically route through Gaffer's multipart endpoints and support files up to 5 GB. ## Next Steps - [Upload API Reference](/docs/upload-api/) - Full API documentation - [GitHub Action](/docs/guides/github-actions/) - Automate uploads in CI --- ## GitHub Actions Source: https://gaffer.sh/docs/guides/github-actions/ import { Aside } from '@astrojs/starlight/components'; The official [Gaffer Uploader](https://github.com/gaffer-sh/gaffer-uploader) GitHub Action makes it easy to automatically upload test reports from your CI workflows. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - A GitHub repository with a test workflow ## Setup ### 1. Add your project token as a secret 1. Go to your GitHub repository 2. Navigate to **Settings** → **Secrets and variables** → **Actions** 3. Click **New repository secret** 4. Name: `GAFFER_PROJECT_TOKEN` 5. Value: Your Gaffer project token ### 2. Add the upload step to your workflow Add the Gaffer uploader step after your test step. Use `if: always()` to ensure reports are uploaded even when tests fail. ## Inputs | Input | Required | Description | |-------|----------|-------------| | `gaffer_api_key` | Yes | Your Gaffer project token | | `report_path` | Yes | Path to the report file or directory to upload | | `api_endpoint` | No | Custom API endpoint URL (for staging/preview environments) | | `commit_sha` | No | Git commit SHA to associate with the test run | | `branch` | No | Git branch name to associate with the test run | | `test_framework` | No | Test framework used (e.g., `playwright`, `jest`, `pytest`) | | `test_suite` | No | Name of the test suite (e.g., `unit`, `e2e`) | ## Examples ### Basic Usage ```yaml name: Tests on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Upload to Gaffer uses: gaffer-sh/gaffer-uploader@v1 if: always() with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` ### Playwright Example A complete example for Playwright tests: ```yaml name: Playwright Tests on: push jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v3 with: version: 9 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm' - name: Install dependencies run: pnpm install - name: Install Playwright browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests run: pnpm exec playwright test - name: Upload Playwright Report to Gaffer uses: gaffer-sh/gaffer-uploader@v1 if: always() with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./playwright-report commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} test_framework: playwright test_suite: e2e ``` ### Pull Request Workflow For pull request workflows, use `github.head_ref` to get the correct branch name: ```yaml name: PR Tests on: pull_request: branches: [main] env: branch_name: ${{ github.head_ref || github.ref_name }} jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: npm test - name: Upload to Gaffer uses: gaffer-sh/gaffer-uploader@v1 if: always() with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results commit_sha: ${{ github.event.pull_request.head.sha }} branch: ${{ env.branch_name }} test_framework: jest test_suite: unit ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```yaml - name: Install CTRF reporter run: npm install --save-dev jest-ctrf-json-reporter # or playwright-ctrf-json-reporter, vitest-ctrf-json-reporter - name: Run tests with CTRF reporter run: npm test -- --reporter=jest-ctrf-json-reporter - name: Upload CTRF report to Gaffer uses: gaffer-sh/gaffer-uploader@v1 if: always() with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./ctrf-report.json commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` ## Environment Variables GitHub Actions provides these variables for your workflow: | Variable | Description | Example | |----------|-------------|---------| | `${{ github.sha }}` | Full commit SHA | `abc123def456...` | | `${{ github.ref_name }}` | Branch or tag name | `main`, `feature/login` | | `${{ github.head_ref }}` | PR source branch (PR workflows only) | `feature/login` | | `${{ github.event.pull_request.head.sha }}` | PR head commit SHA | `def456abc789...` | ## Troubleshooting ### Report not uploading - Verify the `report_path` points to the correct file or directory - Check that your project token secret is named exactly `GAFFER_PROJECT_TOKEN` - Ensure `if: always()` is set so the step runs even when tests fail ### Wrong branch showing For pull request workflows, use `github.head_ref || github.ref_name` pattern to get the correct source branch name. ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the token is correctly copied (no extra spaces) - Make sure the secret is accessible to the workflow ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [cURL Guide](/docs/guides/curl/) - Manual uploads for debugging - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitLab CI](/docs/guides/gitlab-ci/) · [CircleCI](/docs/guides/circleci/) · [Jenkins](/docs/guides/jenkins/) · [Bitbucket](/docs/guides/bitbucket-pipelines/) · [Azure DevOps](/docs/guides/azure-devops/) --- ## GitLab CI Source: https://gaffer.sh/docs/guides/gitlab-ci/ import { Aside } from '@astrojs/starlight/components'; GitLab CI/CD makes it easy to run tests on every push. With Gaffer, you can automatically upload and share test reports from your pipelines. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - A GitLab repository with a `.gitlab-ci.yml` file ## Setup ### 1. Add your project token as a CI/CD variable 1. Go to your GitLab project 2. Navigate to **Settings** → **CI/CD** → **Variables** 3. Click **Add variable** 4. Key: `GAFFER_PROJECT_TOKEN` 5. Value: Your Gaffer project token 6. Check **Mask variable** to hide it in logs ### 2. Add the upload step to your pipeline ```yaml test: stage: test script: - npm ci - npm test after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' artifacts: when: always reports: junit: test-results/junit.xml ``` ## Environment Variables GitLab CI provides these variables for your pipeline: | Variable | Description | Example | |----------|-------------|---------| | `$CI_COMMIT_SHA` | Full commit SHA | `abc123def456...` | | `$CI_COMMIT_REF_NAME` | Branch or tag name | `main`, `feature/login` | | `$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | Source branch in MR pipelines | `feature/login` | | `$CI_PROJECT_NAME` | Project name | `my-app` | ## Examples ### Playwright ```yaml playwright: stage: test image: mcr.microsoft.com/playwright:v1.40.0-jammy script: - npm ci - npx playwright install --with-deps - npx playwright test after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"playwright","test_suite":"e2e"}' artifacts: when: always paths: - playwright-report/ ``` ### Jest with JUnit Reporter ```yaml jest: stage: test script: - npm ci - npm test -- --reporters=default --reporters=jest-junit after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@junit.xml" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"jest"}' artifacts: when: always reports: junit: junit.xml ``` ### pytest ```yaml pytest: stage: test image: python:3.11 script: - pip install pytest pytest-html - pytest --html=report.html --self-contained-html after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@report.html" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'","test_framework":"pytest"}' artifacts: when: always paths: - report.html ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```yaml test: script: - npm ci # Install the CTRF reporter for your framework: # npm install --save-dev jest-ctrf-json-reporter # npm install --save-dev playwright-ctrf-json-reporter # npm install --save-dev vitest-ctrf-json-reporter - npm test -- --reporter=jest-ctrf-json-reporter after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ## Merge Request Pipelines For merge request pipelines, use the source branch name: ```yaml test: rules: - if: $CI_MERGE_REQUEST_IID script: - npm test after_script: - | BRANCH="${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME:-$CI_COMMIT_REF_NAME}" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$BRANCH"'"}' ``` ## Troubleshooting ### Report not uploading - Verify `GAFFER_PROJECT_TOKEN` is set in CI/CD variables - Check the variable is not marked "Protected" if running on unprotected branches - Ensure `after_script` is used (runs even when tests fail) ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the token is correctly copied (no extra spaces) ### Wrong branch showing For merge request pipelines, use `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` instead of `CI_COMMIT_REF_NAME`. ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitHub Actions](/docs/guides/github-actions/) · [CircleCI](/docs/guides/circleci/) · [Jenkins](/docs/guides/jenkins/) · [Bitbucket](/docs/guides/bitbucket-pipelines/) · [Azure DevOps](/docs/guides/azure-devops/) --- ## CI Provider Guides Source: https://gaffer.sh/docs/guides/index/ import { LinkCard, CardGrid } from '@astrojs/starlight/components' import { Aside } from '@astrojs/starlight/components' Choose your CI provider below to get started with automatic test report uploads. ## Universal Format: CTRF CTRF (Common Test Report Format) provides a standardized JSON format that works with any CI provider and test framework. Use it for consistent analytics across your entire test suite. [Learn about CTRF →](/docs/guides/ctrf/) ## CI Providers ## Other Methods - [cURL Guide](/docs/guides/curl/) — Upload manually or from any CI system using curl - [Upload API Reference](/docs/upload-api/) — Build custom integrations with the full API documentation --- ## Jenkins Source: https://gaffer.sh/docs/guides/jenkins/ import { Aside } from '@astrojs/starlight/components'; Jenkins is a widely-used open-source automation server. With Gaffer, you can automatically upload and share test reports from your Jenkins pipelines. ## Prerequisites - A [Gaffer account](https://app.gaffer.sh/register?utm_source=gaffer&utm_medium=website&utm_campaign=docs) with a project - Your [project token](/docs/upload-api/) - A Jenkins instance with a pipeline configured ## Setup ### 1. Add your project token as a credential 1. Go to **Manage Jenkins** → **Credentials** 2. Select the appropriate domain (or global) 3. Click **Add Credentials** 4. Kind: **Secret text** 5. Secret: Your Gaffer project token 6. ID: `gaffer-project-token` 7. Description: `Gaffer Project Token` ### 2. Add the upload step to your Jenkinsfile ```groovy pipeline { agent any environment { GAFFER_PROJECT_TOKEN = credentials('gaffer-project-token') } stages { stage('Test') { steps { sh 'npm ci' sh 'npm test' } post { always { sh ''' curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$GIT_COMMIT"'","branch":"'"$GIT_BRANCH"'"}' ''' junit 'test-results/junit.xml' } } } } } ``` ## Environment Variables Jenkins provides these variables for your pipeline: | Variable | Description | Example | |----------|-------------|---------| | `$GIT_COMMIT` | Full commit SHA | `abc123def456...` | | `$GIT_BRANCH` | Branch name (may include `origin/`) | `origin/main`, `origin/feature/login` | | `$BRANCH_NAME` | Branch name (multibranch pipelines) | `main`, `feature/login` | | `$BUILD_NUMBER` | Build number | `123` | | `$JOB_NAME` | Job name | `my-app/main` | ## Examples ### Playwright ```groovy pipeline { agent { docker { image 'mcr.microsoft.com/playwright:v1.40.0-jammy' } } environment { GAFFER_PROJECT_TOKEN = credentials('gaffer-project-token') } stages { stage('Install') { steps { sh 'npm ci' } } stage('Test') { steps { sh 'npx playwright test' } post { always { sh ''' curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"'"$GIT_COMMIT"'","branch":"'"$BRANCH_NAME"'","test_framework":"playwright","test_suite":"e2e"}' ''' archiveArtifacts artifacts: 'playwright-report/**' } } } } } ``` ### Jest with JUnit Reporter ```groovy pipeline { agent any environment { GAFFER_PROJECT_TOKEN = credentials('gaffer-project-token') } stages { stage('Test') { steps { sh 'npm ci' sh 'npm test -- --reporters=default --reporters=jest-junit' } post { always { sh ''' curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@junit.xml" \ -F 'tags={"commitSha":"'"$GIT_COMMIT"'","branch":"'"$BRANCH_NAME"'","test_framework":"jest"}' ''' junit 'junit.xml' } } } } } ``` ### Declarative with Scripted Post ```groovy pipeline { agent any stages { stage('Test') { steps { sh 'npm ci && npm test' } } } post { always { withCredentials([string(credentialsId: 'gaffer-project-token', variable: 'GAFFER_PROJECT_TOKEN')]) { sh ''' # Clean branch name (remove origin/ prefix) CLEAN_BRANCH="${GIT_BRANCH#origin/}" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/junit.xml" \ -F 'tags={"commitSha":"'"$GIT_COMMIT"'","branch":"'"$CLEAN_BRANCH"'"}' ''' } } } } ``` ## Using CTRF Format For a standardized format across all your test frameworks, consider using [CTRF](/docs/guides/ctrf/): ```groovy stage('Test') { steps { // Install the CTRF reporter for your framework: // npm install --save-dev jest-ctrf-json-reporter // npm install --save-dev playwright-ctrf-json-reporter // npm install --save-dev vitest-ctrf-json-reporter sh 'npm install --save-dev jest-ctrf-json-reporter' sh 'npm test -- --reporter=jest-ctrf-json-reporter' } post { always { sh ''' curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$GIT_COMMIT"'","branch":"'"$BRANCH_NAME"'"}' ''' } } } ``` ## Troubleshooting ### Report not uploading - Verify the credential ID matches (`gaffer-project-token`) - Ensure the upload is in a `post { always { } }` block - Check the file path is correct relative to the workspace ### 401 Unauthorized - Check your project token starts with `gfr_` - Verify the credential is accessible to the pipeline ### Branch name includes "origin/" Use `${GIT_BRANCH#origin/}` to strip the prefix: ```groovy sh ''' CLEAN_BRANCH="${GIT_BRANCH#origin/}" curl ... -F 'tags={"branch":"'"$CLEAN_BRANCH"'"}' ''' ``` ### curl not found Ensure curl is installed on your Jenkins agent or use a Docker image that includes it. ## Next Steps - [CTRF Guide](/docs/guides/ctrf/) - Use the universal test format - [Upload API Reference](/docs/upload-api/) - Full API documentation - [Slack Integration](/docs/integrations/slack/) - Get test results in Slack **Other CI Providers:** [GitHub Actions](/docs/guides/github-actions/) · [GitLab CI](/docs/guides/gitlab-ci/) · [CircleCI](/docs/guides/circleci/) · [Bitbucket](/docs/guides/bitbucket-pipelines/) · [Azure DevOps](/docs/guides/azure-devops/) --- ## Docs Source: https://gaffer.sh/docs/ import { LinkCard } from '@astrojs/starlight/components' Redirecting to [Getting Started](/docs/getting-started/)... --- ## OpenTelemetry Source: https://gaffer.sh/docs/integrations/opentelemetry/ import { Aside } from '@astrojs/starlight/components' Gaffer can export test run and coverage metrics as OpenTelemetry (OTLP) data to your existing observability stack. Track pass rates, failure counts, and coverage trends alongside your service metrics in Datadog, Grafana Cloud, or any OTLP-compatible backend. ## Features - **Provider presets** — built-in support for Datadog and Grafana Cloud, plus a generic OTLP option for any compatible endpoint - **Branch filtering** — limit metric export to specific branches using glob patterns - **Test run metrics** — pass rate, total/passed/failed/skipped counts per run - **Coverage metrics** — line and branch coverage percentages per upload - **Test connectivity** — verify your configuration from the dashboard before waiting for real data - **Encrypted API keys** — credentials are encrypted at rest with AES-256-GCM ## Setup by Provider ### Datadog 1. In Datadog, go to **Organization Settings > API Keys** and create (or copy) an API key 2. In Gaffer, navigate to **Settings > OpenTelemetry** and click **"Add destination"** 3. Select **Datadog** as the provider 4. Paste your API key and select your Datadog site (e.g., `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`, `us5.datadoghq.com`, `ap1.datadoghq.com`) 5. Choose which event types to export (test runs, coverage, or both) 6. Click **"Create"** 7. Click **"Test"** to verify connectivity 8. Verify in Datadog: go to **Metrics > Explorer** and search for `gaffer.tests.pass_rate` ### Grafana Cloud 1. In Grafana Cloud, go to your stack's **OpenTelemetry** section to get your OTLP gateway credentials (instance ID, region, API token) 2. In Gaffer, navigate to **Settings > OpenTelemetry** and click **"Add destination"** 3. Select **Grafana** as the provider 4. Enter your Grafana Cloud region, instance ID, and API token 5. Choose event types and click **"Create"** 6. Click **"Test"** to verify connectivity 7. Verify in Grafana: go to **Explore** and query for `gaffer_tests_pass_rate` ### Generic OTLP Any backend that accepts OTLP over HTTP can be used as a destination. 1. In Gaffer, navigate to **Settings > OpenTelemetry** and click **"Add destination"** 2. Select **Generic OTLP** as the provider 3. Enter your OTLP HTTP endpoint URL (must be HTTPS) 4. Enter an API key or bearer token for authentication 5. Choose event types and click **"Create"** 6. Click **"Test"** to verify connectivity ## Configuration Options ### Event Types Choose which events export metrics: - **Test runs** — exports pass rate, total/passed/failed/skipped counts when a test run completes - **Coverage** — exports line and branch coverage percentages when a coverage report is uploaded At least one event type must be enabled. ### Branch Filters Restrict which branches trigger metric export using glob patterns: - `main` — exact match - `release/*` — matches `release/v1.0`, `release/2024-01`, etc. - `feature-*` — matches `feature-auth`, `feature-dashboard`, etc. Leave branch filters empty to export metrics for all branches. ### Enabled Toggle Each destination can be enabled or disabled independently. Disabled destinations remain configured but do not export metrics. ## Available Metrics ### Test Run Metrics | Metric | Type | Unit | Description | |--------|------|------|-------------| | `gaffer.tests.pass_rate` | Gauge | % | Pass rate of the test run | | `gaffer.tests.total` | Gauge | test | Total test count | | `gaffer.tests.passed` | Gauge | test | Passed test count | | `gaffer.tests.failed` | Gauge | test | Failed test count | | `gaffer.tests.skipped` | Gauge | test | Skipped test count | ### Coverage Metrics | Metric | Type | Unit | Description | |--------|------|------|-------------| | `gaffer.coverage.line_percent` | Gauge | % | Line coverage percentage | | `gaffer.coverage.branch_percent` | Gauge | % | Branch coverage percentage | ### Dimensions (Attributes) Metrics include these attributes for filtering and grouping: | Attribute | Included On | Description | |-----------|-------------|-------------| | `gaffer.project.id` | All metrics | Project identifier | | `gaffer.project.name` | All metrics | Project name | | `gaffer.branch` | All metrics | Git branch name (when available) | | `gaffer.commit_sha` | All metrics | Git commit SHA (when available) | | `gaffer.report_format` | Test run metrics only | Report format (e.g., `ctrf`) | ## Dashboard Templates Get started faster with a pre-built Datadog dashboard. See [Datadog Test Metrics Dashboard](/solutions/datadog-test-metrics/) for a downloadable JSON template and import instructions. ## Testing Each destination has a **"Test"** button in the dashboard. Clicking it sends a test metric payload to your configured endpoint and reports the HTTP status code. Use this to verify connectivity and authentication before waiting for real test data. ## Troubleshooting ### Test button returns an error? - **Check your API key** — make sure the key is correct and has the necessary permissions - **Verify the site/region** — Datadog and Grafana Cloud require the correct region to be selected - **Check endpoint URL** — for generic OTLP, ensure the URL is correct and reachable - **Network issues** — if your backend is behind a firewall, ensure it accepts connections from Gaffer's servers ### Metrics not appearing in your backend? - **Check the destination is enabled** — disabled destinations do not export metrics - **Verify branch filters** — if configured, only matching branches trigger export - **Check event types** — make sure the relevant event type (test runs or coverage) is enabled - **Check "Last Sent"** — the destinations list shows the last delivery time and any errors - **Allow propagation time** — some backends take a few minutes to index new metrics ## Security - **Encrypted API keys** — credentials are encrypted at rest using AES-256-GCM - **HTTPS recommended** — Datadog and Grafana Cloud presets use HTTPS endpoints; for generic OTLP, use HTTPS to protect data in transit --- ## Slack Integration Source: https://gaffer.sh/docs/integrations/slack/ Gaffer's Slack integration sends notifications to your team when test runs complete. Stay informed about test failures without leaving Slack, and quickly navigate to detailed reports when issues arise. ## Features - **Real-time notifications** - Get notified immediately when test runs finish - **Smart message updates** - Multiple test runs for the same commit update a single message instead of spamming your channel - **Branch filtering** - Only receive notifications for branches you care about (e.g., `main`, `release/*`) - **Configurable triggers** - Choose to be notified on all runs, only failures, or after consecutive failures - **Quick links** - Jump directly to the full test report from the notification ## Connecting Slack 1. Navigate to **Settings > Slack** in your Gaffer dashboard 2. Click **"Connect Slack"** to start the OAuth flow 3. Select the Slack workspace you want to connect 4. Authorize Gaffer to post messages 5. You'll be redirected back to Gaffer with Slack connected After connecting, you'll need to configure which channels receive notifications and customize your notification preferences. ## Configuration Options ### Channels Select one or more channels where Gaffer should post notifications. The Gaffer bot must be a member of any private channels you want to use. **Tip:** Consider creating a dedicated `#test-results` channel to keep notifications organized without cluttering general channels. ### Branch Filters By default, Gaffer sends notifications for all branches. You can restrict notifications to specific branches using patterns: - `main` - Exact match for the main branch - `release/*` - All release branches (e.g., `release/v1.0`, `release/2024-01`) - `feature/*` - All feature branches Leave the branch filters empty to receive notifications for all branches. ### Notification Triggers Control when notifications are sent: - **All test runs** - Notify on every completed test run (good for high-visibility branches like `main`) - **Failures only** - Only notify when tests fail (reduces noise while keeping you informed of problems) - **Consecutive failures** - Only notify after multiple consecutive failures (useful for catching real issues vs. one-off flakes) For the consecutive failures option, you can set the threshold (e.g., notify after 3 consecutive failures). ## Notification Format Slack notifications include: - Project name and branch - Test summary (passed, failed, skipped counts) - Commit SHA (when available) - List of failed test names (up to 5, with "and X more" if there are additional failures) - Direct link to the full report in Gaffer When multiple test runs complete for the same commit (e.g., Vitest unit tests and Playwright E2E tests), Gaffer updates the existing message instead of posting a new one. This keeps your channel tidy and gives you a consolidated view of all test results for that commit. ## Disconnecting Slack To disconnect Slack: 1. Go to **Settings > Slack** 2. Click **"Disconnect"** 3. Confirm the disconnection This removes the Slack integration and deletes all notification history. You can reconnect at any time by going through the OAuth flow again. ## Troubleshooting ### Not receiving notifications? - **Check channel membership** - For private channels, ensure the Gaffer bot has been added to the channel - **Verify branch filters** - If you have branch filters configured, make sure your branch matches one of the patterns - **Check notification trigger** - If set to "failures only", you won't receive notifications for passing test runs - **Confirm notifications are enabled** - The toggle in Settings > Slack must be enabled ### Token expired or revoked? If your Slack token is revoked (e.g., if someone removes the Gaffer app from your workspace), notifications will stop. You'll need to reconnect Slack by going through the OAuth flow again. ## Security Gaffer requests minimal Slack permissions: - `chat:write` - Post messages to channels - `channels:read` - List public channels for selection - `groups:read` - List private channels for selection OAuth tokens are encrypted at rest using AES-256-GCM. Gaffer uses Slack's token rotation feature, automatically refreshing tokens before they expire. --- ## Webhooks Source: https://gaffer.sh/docs/integrations/webhooks/ Webhooks send HTTP POST requests to your endpoint when events occur in Gaffer. Use them to integrate test results with your monitoring tools, trigger deployments, update dashboards, or build custom notification flows. ## Features - **Test run and coverage events** — receive notifications for either or both event types - **Branch filtering** — only trigger for specific branches using glob patterns - **Configurable triggers** — fire on all events, failures only, or after consecutive failures - **HMAC-SHA256 signing** — verify that payloads are from Gaffer - **Test delivery** — send a test payload from the dashboard to verify your endpoint ## Creating a Webhook 1. Navigate to **Settings > Webhooks** in your Gaffer dashboard 2. Click **"Add Webhook"** 3. Enter a name and HTTPS endpoint URL 4. Configure notification settings (event types, trigger mode, branch filters) 5. Click **"Create"** Gaffer generates an HMAC signing secret automatically when you create a webhook. The signing secret is displayed once at creation — store it securely, as it cannot be retrieved later. You can regenerate the secret from the webhook settings if needed. ## Configuration ### Event Types Choose which events trigger the webhook: - **Test runs** — fires when a test run completes - **Coverage reports** — fires when a coverage report is uploaded At least one event type must be enabled. ### Notification Triggers Control when test run webhooks fire: - **All test runs** — fire on every completed test run - **Failures only** — only fire when tests fail - **Consecutive failures** — only fire after N consecutive failures (configurable, 1-10). Useful for filtering out one-off flakes Coverage events always fire when enabled — trigger settings only apply to test runs. ### Branch Filters Restrict which branches trigger the webhook using glob patterns: - `main` — exact match - `release/*` — matches `release/v1.0`, `release/2024-01`, etc. - `feature-*` — matches `feature-auth`, `feature-dashboard`, etc. Leave branch filters empty to receive events for all branches. ### Project Scope Webhooks can be scoped to a specific project or apply to all projects in your organization. Organization-wide webhooks receive events from every project. ## Payload Format ### Test Run Event ```json title="test_run event" { "event": "test_run", "timestamp": "2026-01-25T14:30:00.000Z", "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "data": { "test_run_id": "tr_abc123", "upload_id": "upl_xyz789", "project_id": "prj_def456", "project_name": "my-project", "organization_id": "org_ghi789", "branch": "main", "commit_sha": "a1b2c3d4e5f6", "report_format": "ctrf", "passed_count": 142, "failed_count": 3, "skipped_count": 2, "total_count": 147, "status": "failed", "dashboard_url": "https://app.gaffer.sh/projects/prj_def456/runs/tr_abc123", "hosted_report_url": "https://app.gaffer.sh/reports/upl_xyz789/index.html", "url": "https://app.gaffer.sh/projects/prj_def456/runs/tr_abc123" } } ``` ### Coverage Event ```json title="coverage event" { "event": "coverage", "timestamp": "2026-01-25T14:30:00.000Z", "delivery_id": "660e8400-e29b-41d4-a716-446655440000", "data": { "coverage_report_id": "cov_jkl012", "upload_id": "upl_xyz789", "project_id": "prj_def456", "project_name": "my-project", "organization_id": "org_ghi789", "branch": "main", "commit_sha": "a1b2c3d4e5f6", "format": "lcov", "lines": { "covered": 1200, "total": 1500, "percent": 80.0 }, "branches": { "covered": 300, "total": 500, "percent": 60.0 }, "functions": { "covered": 95, "total": 120, "percent": 79.2 }, "dashboard_url": "https://app.gaffer.sh/projects/prj_def456/coverage", "hosted_report_url": null, "url": "https://app.gaffer.sh/projects/prj_def456/coverage" } } ```

Field Reference

| Field | Type | Notes | |-------|------|-------| | `upload_id` | string | Parent upload ID (`upl_*`). Stable across schema changes. Useful for correlating the webhook to dashboard views and to the `hosted_report_url`. | | `dashboard_url` | string | Deep link to the Gaffer dashboard for this run or coverage report. Opening this link in a browser authenticates via session or prompts sign-in. | | `hosted_report_url` | string or null | Link to the hosted HTML report (e.g., Playwright `index.html`) when the upload includes one. `null` when the upload is a raw report file (JUnit XML, lcov, etc.) with no browsable HTML. Opening requires sign-in or a valid share — the link is designed to drop recipients straight into Gaffer. Coverage events may return `null` for every upload today. | | `url` | string | **Deprecated.** Alias of `dashboard_url`, retained for backwards compatibility. Switch to `dashboard_url`. Will be removed on **Sat, 01 Aug 2026**. Outbound requests include `Deprecation` and `Link: rel="successor-version"` headers pointing here. | ### Test Event When you click "Test" in the dashboard, Gaffer sends a test payload: ```json title="test event" { "event": "test", "timestamp": "2026-01-25T14:30:00.000Z", "delivery_id": "770e8400-e29b-41d4-a716-446655440000", "test": true, "data": { "message": "This is a test webhook from Gaffer", "webhook_id": "whk_abc123", "webhook_name": "My Webhook", "organization_id": "org_ghi789" } } ``` ## HTTP Headers Every webhook request includes these headers: | Header | Description | |--------|-------------| | `Content-Type` | `application/json` | | `X-Gaffer-Event` | Event type: `test_run`, `coverage`, or `test` | | `X-Gaffer-Signature` | HMAC-SHA256 signature of the request body | | `X-Gaffer-Delivery` | Unique delivery ID (UUID) for idempotency | | `User-Agent` | `Gaffer-Webhooks/1.0` | | `Deprecation` | Present on `test_run` and `coverage` events. HTTP-date of the sunset for the deprecated `url` field. See [Field Reference](#v2-payload). | | `Link` | Present on `test_run` and `coverage` events. Points at this documentation section with `rel="successor-version"`. | ## Verifying Signatures Every webhook is signed with your webhook's secret using HMAC-SHA256. Always verify the signature before processing the payload to confirm it came from Gaffer. ### Node.js ```javascript title="verify-signature.js" const crypto = require('crypto'); function verifySignature(payload, signature, secret) { const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // In your request handler: const payload = JSON.stringify(req.body); const signature = req.headers['x-gaffer-signature']; if (!verifySignature(payload, signature, process.env.GAFFER_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } ``` ### Python ```python title="verify_signature.py" import hmac import hashlib def verify_signature(payload: bytes, signature: str, secret: str) -> bool: expected = 'sha256=' + hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) # In your request handler: payload = request.get_data() signature = request.headers.get('X-Gaffer-Signature') if not verify_signature(payload, signature, os.environ['GAFFER_WEBHOOK_SECRET']): abort(401) ``` ## Retry Behavior Gaffer retries webhook deliveries when your endpoint returns a 5xx status code or a network error occurs. Deliveries that receive a 4xx response are not retried — these indicate a permanent issue with the request (e.g., your endpoint rejected it). Each delivery has a unique `X-Gaffer-Delivery` ID. Use this for idempotency — if you receive the same delivery ID twice, you can safely skip the duplicate. ## Testing Webhooks 1. Create your webhook in **Settings > Webhooks** 2. Click the **"Test"** button next to your webhook 3. Gaffer sends a test payload to your endpoint 4. Check the result — the dashboard shows whether the delivery succeeded or failed, including the HTTP status code For local development, use a tunnel service like [ngrok](https://ngrok.com) to expose your local server to the internet. ## Plan Limits | Plan | Webhooks | |------|----------| | Free | 1 | | Pro | 5 | | Team | Unlimited | ## Troubleshooting ### Not receiving webhooks? - **Check the webhook is enabled** — disabled webhooks don't fire - **Verify branch filters** — if configured, only matching branches trigger the webhook - **Check notification trigger** — "failures only" won't fire for passing test runs - **Check event types** — make sure the relevant event type (test runs or coverage) is enabled - **Check "Last Sent" and error status** — the webhooks list shows the last delivery time and any errors ### Receiving a 401 from signature verification? - **Use the raw request body** — verify against the exact bytes received, not a re-serialized version - **Check the secret** — make sure you're using the correct secret for this webhook - **Regenerate if needed** — edit the webhook and check "Regenerate secret" to get a new one ### Endpoint URL rejected? - **HTTPS required** — webhook URLs must use HTTPS - **No private IPs** — localhost, 127.0.0.1, and private IP ranges (10.x, 172.16-31.x, 192.168.x) are blocked for security ## Security - **HTTPS only** — webhook URLs must use HTTPS to protect payloads in transit - **SSRF protection** — private IP ranges and localhost are blocked - **Encrypted secrets** — signing secrets are encrypted at rest using AES-256-GCM - **HMAC-SHA256 signing** — every payload is signed so you can verify authenticity --- ## Introduction Source: https://gaffer.sh/docs/introduction/ Gaffer wraps your test command and extracts flaky tests, failure clusters, health scores, and coverage trends from raw results. Same data in three places: - **[CLI](/docs/cli/)** — `gaffer test` enriches your terminal output after every run - **Dashboard** — your team sees history, trends, and share links at [app.gaffer.sh](https://app.gaffer.sh) - **[MCP server](/docs/mcp/)** — AI coding agents (Claude Code, Cursor, Windsurf) query your test data and act on it ## What you get - **[Flaky test detection](/solutions/flaky-test-detection/)** — flip rates, composite scores, and history for every test that alternates between pass and fail - **Failure clusters** — groups failing tests by shared error patterns so you fix root causes, not symptoms - **Health scores** — a single 0–100 number combining pass rate, flakiness, and trend direction - **[Coverage tracking](/docs/coverage/)** — line, branch, and function coverage with GitHub commit status gating - **Duration analysis** — P50/P75/P90/P95/P99 percentiles and slowest test identification - **Works with everything** — Playwright, Jest, Vitest, pytest, Go, RSpec, JUnit XML, CTRF, and any CI provider ## How it works ```bash # Install curl -fsSL https://app.gaffer.sh/install.sh | sh # Wrap your test command gaffer test -- npm test ``` Gaffer runs your tests, discovers report files, parses results, computes analytics, prints an enriched summary, and syncs to the cloud — all in one command. Your test command's exit code passes through unchanged. ## Next steps Head to the [Getting Started](/docs/getting-started/) guide to install the CLI and run your first test. --- ## MCP Server Source: https://gaffer.sh/docs/mcp/ The Gaffer MCP server (`@gaffer-sh/mcp`) connects AI coding assistants to your test history. Ask about test health, investigate flaky tests, and get context about failures without leaving your editor. ## What is MCP? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that lets AI assistants access external tools and data sources. By installing the Gaffer MCP server, your AI assistant gains direct access to your test analytics. ## Features - **Test health insights** - Ask about pass rates, trends, and overall test suite health - **Flaky test detection** - Identify tests with inconsistent behavior - **Test history lookup** - Check the history of specific tests to understand stability - **Cross-project access** - Query all your projects from a single API key - **Report file access** - Get links to HTML reports, coverage files, and other artifacts - **Slowest test analysis** - Find tests that are slowing down your CI pipeline ## Prerequisites 1. A [Gaffer account](https://app.gaffer.sh) with test results uploaded 2. An API Key from **Account Settings > API Keys** ## Setup ### Claude Code Add to your Claude Code settings (`~/.claude.json` or project `.claude/settings.json`): ```json { "mcpServers": { "gaffer": { "command": "npx", "args": ["-y", "@gaffer-sh/mcp"], "env": { "GAFFER_API_KEY": "gaf_your_api_key_here" } } } } ``` ### Cursor Add to `.cursor/mcp.json` in your project: ```json { "mcpServers": { "gaffer": { "command": "npx", "args": ["-y", "@gaffer-sh/mcp"], "env": { "GAFFER_API_KEY": "gaf_your_api_key_here" } } } } ``` ## What tools does the Gaffer MCP server expose? Three. The server runs in **code mode**: rather than one MCP tool per API call, it exposes three tools plus a `codemode` namespace of 16 analytics functions that you call from JavaScript. That keeps 16 tool definitions out of the context window, and it lets one execution chain several calls instead of paying a round-trip each. | MCP tool | What it does | |----------|--------------| | `execute_code` | Runs JavaScript against `codemode.()`. Max 20 API calls, 30s timeout. | | `search_tools` | Finds available functions by keyword. An empty query lists all of them. | | `list_projects` | Lists projects. Registered only when your token is a user API Key (`gaf_`). | You rarely write this code yourself. You ask a question, and the assistant writes the call: ```javascript const health = await codemode.get_project_health({ projectId: "proj_abc" }); if (health.flakyTestCount > 0) { const flaky = await codemode.get_flaky_tests({ projectId: "proj_abc" }); return { health, flaky }; } return { health }; ``` ## Which functions can the assistant call? All 16 are reachable through `execute_code`. Ask `search_tools` with an empty query to list them at runtime. | Function | Category | Returns | |----------|----------|---------| | `get_project_health` | health | Health score (0-100), pass rate, run count, flaky count, trend | | `get_test_history` | testing | Pass/fail history for one test, with branch, commit, and errors | | `get_flaky_tests` | testing | Flip rates, transition counts, and last-seen timestamps | | `list_test_runs` | testing | Recent runs, filterable by commit, branch, or status | | `get_test_run_details` | testing | Individual results for one run, with stack traces | | `get_failure_clusters` | testing | Failed tests grouped by error similarity | | `get_slowest_tests` | testing | Slowest tests by average and P95 duration | | `compare_test_metrics` | testing | Before/after metrics across two commits or runs | | `search_failures` | testing | Past failures matching an error or test-name pattern | | `get_coverage_summary` | coverage | Line, branch, and function coverage, plus trend | | `get_coverage_for_file` | coverage | Coverage for an exact or partial file path | | `get_untested_files` | coverage | Files below a coverage threshold | | `find_uncovered_failure_areas` | coverage | Files with low coverage and test failures | | `get_report` | reports | Report file URLs for a test run | | `get_report_browser_url` | reports | Signed browser URL, valid 30 minutes | | `get_upload_status` | uploads | Whether CI results are uploaded and processed | With a project token (`gfr_`), omit `projectId` on every function. It resolves automatically. ## Example Prompts Once the MCP server is connected, try asking your AI assistant: - "What projects do I have in Gaffer?" - "What's the health of my test suite?" - "Which tests are flaky in my project?" - "Is the login test flaky? Check its history" - "What tests failed in the last commit?" - "Show me test runs on the main branch" - "Which tests are slowing down my CI pipeline?" - "Get the Playwright report for the latest test run" ## Environment Variables | Variable | Required | Description | |----------|----------|-------------| | `GAFFER_API_KEY` | Yes | Your Gaffer API Key (starts with `gaf_`) | | `GAFFER_API_URL` | No | API base URL (default: `https://app.gaffer.sh`) | ## Authentication The MCP server uses **User API Keys** (`gaf_` prefix) which provide read-only access to all projects across your organizations. Get your API Key from **Account Settings > API Keys** in the Gaffer dashboard. **Note:** Project Tokens (`gfr_` prefix) are designed for uploading test results from CI and only provide access to a single project. For the MCP server, use a User API Key instead. --- ## Upload API Source: https://gaffer.sh/docs/upload-api/ Gaffer's Upload API allows you to upload test reports from any CI/CD system or local environment. The API accepts multipart form data containing your test report files and optional metadata. ## Endpoint `POST https://app.gaffer.sh/api/upload` ## Authentication All requests must include your project token in the `X-API-Key` header. You can find your project token in your project settings. | Header | Description | |--------|-------------| | `X-API-Key` | Your project token (required) | | `Content-Type` | `multipart/form-data` (set automatically by most HTTP clients) | When using the [Gaffer CLI](/docs/cli/), the canonical environment variable is `GAFFER_PROJECT_TOKEN`. `GAFFER_UPLOAD_TOKEN` (the previous name) and `GAFFER_TOKEN` remain supported as fallbacks for backward compatibility. ## Request Body The request body must be `multipart/form-data` with the following fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | `files` | File(s) | Yes | One or more test report files to upload. Include multiple files by repeating the field. | | `tags` | JSON string | No | Metadata tags as a JSON object (see below). | ## Tags Tags are optional metadata that help you organize and filter your test runs. Pass them as a JSON string in the `tags` field. | Tag | Recommended | Description | |-----|-------------|-------------| | `commitSha` | Strongly recommended | The git commit SHA associated with this test run. | | `branch` | Recommended | The git branch name (e.g., `main`, `feature/auth`). | | `test_framework` | Optional | The test framework used (e.g., `playwright`, `jest`, `pytest`). | | `test_suite` | Optional | A label for the test suite (e.g., `unit`, `integration`, `e2e`). | You can also include any custom tags you like. All tag values must be strings. ## File Size Limits The single-POST endpoint documented here is capped by the Cloudflare edge at **100 MB total per request**. For files larger than 100 MB (Playwright traces, large videos, full HTML reports), use the [`gaffer` CLI](/docs/cli/) or the [`gaffer-uploader@v2` GitHub Action](/docs/guides/github-actions/) instead. Both transparently route uploads through Gaffer's multipart endpoints, supporting individual files up to **5 GB**. Your plan's storage cap is the only practical ceiling. ## Supported Report Formats Gaffer supports a variety of test report formats: - **Playwright HTML** - Full HTML report with embedded data - **Vitest HTML** - Vitest HTML reporter output - **Jest JSON** - Native Jest JSON output (`--json`) - **Jest HTML** - jest-html-reporter output - **pytest HTML** - pytest-html plugin output - **JUnit XML** - Standard JUnit XML format - **Vitest JSON** - Vitest JSON reporter output ## Example Request ### Single File Upload ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F 'tags={"commitSha":"abc123def456","branch":"main","test_framework":"playwright"}' ``` ### Multiple Files Upload ```bash title="curl" curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: YOUR_PROJECT_TOKEN" \ -F "files=@playwright-report/index.html" \ -F "files=@playwright-report/data/test-results.json" \ -F 'tags={"commitSha":"abc123","branch":"feature/auth"}' ``` ## Response A successful upload returns HTTP `201 Created` with the following JSON response: ```json title="Response" { "testRun": { "id": "abc123xyz", "uniqueId": "1701234567890", "projectId": "proj_abc123", "commitSha": "abc123def456", "branch": "main", "tags": { "commitSha": "abc123def456", "branch": "main", "test_framework": "playwright" }, "createdAt": "2024-01-15T10:30:00.000Z" }, "files": [ { "filename": "index.html", "size": 245678, "path": "org_123/proj_456/1701234567890/index.html", "contentType": "text/html" } ] } ``` | Field | Description | |-------|-------------| | `testRun.id` | Unique identifier for the test run. | | `testRun.uniqueId` | Timestamp-based identifier. | | `testRun.projectId` | The project this test run belongs to. | | `testRun.commitSha` | Git commit SHA (if provided in tags). | | `testRun.branch` | Git branch name (if provided in tags). | | `testRun.tags` | All tags provided in the request. | | `testRun.createdAt` | When the test run was created. | | `files` | Array of uploaded file details. | | `warnings` | Optional array of warning messages (e.g., if parsing couldn't be queued). | ## Error Responses | Status | Meaning | Description | |--------|---------|-------------| | `400` | Bad Request | No files provided, invalid tags format, or file too large. | | `401` | Unauthorized | Missing or invalid project token. | | `402` | Payment Required | Storage limit exceeded. Upgrade your plan for more storage. | | `503` | Service Unavailable | Temporary upload failure. Retry the request. | ## Next Steps - [cURL Guide](/docs/guides/curl/) - Quick examples for uploading with cURL - [GitHub Action](/docs/guides/github-actions/) - Automate uploads from GitHub Actions # Solutions --- ## AI Test Memory: Give Agents Test History via MCP Source: https://gaffer.sh/solutions/ai-agent-test-memory/

Your AI agent just "fixed" a test that's been flipping between pass and fail for two weeks. It spent tokens debugging a 40% flip-rate flaky test that no human would touch. The agent doesn't know that because it has no memory of what happened before this session.

## The Problem AI coding agents operate without test history. Each session starts from zero. This leads to three failure modes that waste time and CI minutes: **Wasted cycles on flaky tests.** An agent sees a test failure and tries to fix it. But the test has a 40% flip rate — it was going to pass on the next run anyway. The agent doesn't know because it can't see historical pass/fail patterns. **Duplicate work on shared root causes.** CI reports 14 failures. The agent attempts 14 separate fixes. In reality, 11 of those failures share one root cause — a database connection timeout. Fixing one line would have resolved them all. **No before/after comparison.** After making changes, the agent can't verify whether a specific test actually improved. Did the fix work, or did a flaky test just happen to pass this time? Without comparing metrics across commits, the agent is guessing. This isn't theoretical. Google's 2025 DORA report found that [AI coding tools correlate with a 7.2% decrease in delivery stability](https://dora.dev/research/2025/ai-assistance/). The agents are fast at writing code, but they make poor decisions when they can't see test history. ## What Teams Try (and Why It Falls Short) **Piping CI logs into context.** Raw CI output is unstructured text designed for humans. An agent can't compute a flip rate from log lines. It can't cluster failures by error similarity. And it definitely can't compare results across commits when it's parsing different log formats from Playwright, Jest, and Pytest. **General-purpose memory tools.** Tools like mem0 or Letta give agents fuzzy vector recall — useful for remembering conversations, not for computing that `test_checkout_flow` has flipped 8 times in 20 runs. You can't calculate a flakiness score from semantic similarity search. **Checking CI directly.** GitHub Actions artifacts expire. Navigating workflow runs programmatically is brittle. And even if the agent finds the right artifact, it gets back HTML reports or raw XML — not structured data it can reason about. ## Structured Test Memory "Test memory" means giving agents deterministic, queryable access to test history — not fuzzy recall, but exact data: pass rates, flip counts, failure clusters, duration changes. Gaffer's [MCP server](https://www.npmjs.com/package/@gaffer-sh/mcp) exposes this as structured tool calls that any MCP-compatible agent (Claude, Cursor, Windsurf, Copilot) can use automatically. ### Identify Flaky Tests Before Wasting Time ```json // get_flaky_tests response { "flakyTests": [ { "name": "should complete checkout flow", "flipRate": 0.4, "flipCount": 8, "totalRuns": 20, "flakinessScore": 0.72 } ], "summary": { "totalFlaky": 3, "threshold": 0.1, "period": 30 } } ``` The agent sees a `flakinessScore` of 0.72 and a 40% flip rate. It skips the flaky test and focuses on real regressions. ### Group Failures by Root Cause ```json // get_failure_clusters response { "clusters": [ { "representativeError": "Connection refused: localhost:5432", "count": 11, "tests": [ { "name": "should create user", "fullName": "Auth > should create user" }, { "name": "should update profile", "fullName": "Profile > should update profile" } ] }, { "representativeError": "Expected 200, received 401", "count": 3, "tests": [ { "name": "should access dashboard", "fullName": "Dashboard > should access dashboard" } ] } ], "totalFailures": 14 } ``` 14 failures, 2 root causes. The agent fixes the database connection issue and the auth bug — not 14 individual tests. ### Track Pass/Fail Across Commits ```json // get_test_history response { "history": [ { "status": "failed", "commitSha": "a1b2c3", "durationMs": 4200 }, { "status": "passed", "commitSha": "d4e5f6", "durationMs": 3800 }, { "status": "passed", "commitSha": "g7h8i9", "durationMs": 3900 } ], "summary": { "totalRuns": 3, "passedRuns": 2, "failedRuns": 1, "passRate": 66.67 } } ``` Failed once, passed twice before — likely a regression in the latest commit, not a flaky test. ### Compare Before and After ```json // compare_test_metrics response { "before": { "status": "failed", "durationMs": 12400, "commit": "a1b2c3" }, "after": { "status": "passed", "durationMs": 3200, "commit": "d4e5f6" }, "change": { "statusChanged": true, "percentChange": -74.2 } } ``` The fix worked: status changed from failed to passed, duration dropped 74%. No guessing. ## Comparison | Capability | CI Logs | Vector Memory | Gaffer MCP | |------------|---------|---------------|------------| | Flip rate / flakiness score | No | No | Yes | | Failure clustering by root cause | No | No | Yes | | Cross-commit comparison | No | Approximate | Exact | | Structured, queryable data | No | Fuzzy | Yes | | Works across test frameworks | Varies | N/A | Yes | | Agent can use without parsing | No | Yes | Yes | ## Setup Three steps. No code changes to your test suite. ### 1. Upload test results from CI Add the [Gaffer uploader](https://gaffer.sh/guides/github-actions/) to your CI pipeline. Supports GitHub Actions, GitLab CI, CircleCI, and others. ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} ``` ### 2. Add the MCP server ```bash claude mcp add gaffer -e GAFFER_API_KEY=gaf_your_key -- npx -y @gaffer-sh/mcp@latest ``` Works with Claude Code, Cursor, Windsurf, and any MCP-compatible tool. ### 3. Agent queries automatically Once connected, the agent calls the right tool based on context. No configuration for when to use which tool — it sees a test failure and checks history, checks flakiness, clusters failures, and compares results across commits. ## Get Started Gaffer's free tier includes test history and flaky test detection. The MCP server is [open source](https://github.com/gaffer-sh/mcp).
--- ## Allure Report Alternative: Hosted Test Reports, No Setup Source: https://gaffer.sh/solutions/allure-alternative/ import { Image } from "astro:assets";

You picked Allure for its HTML reports. Then you tried to share those reports with your team, and hit the real problem: self-hosting a server, configuring a database, managing CI integration, handling user access, and keeping everything updated. For many teams, the infrastructure tax exceeds the reporting benefit.

## Why Teams Look for an Allure Report Alternative Allure Report generates beautiful HTML reports locally. Allure TestOps (the paid product) adds history and collaboration features. But both come with trade-offs: ### Self-Hosting Complexity Running Allure TestOps means: - Setting up and maintaining a server - Configuring PostgreSQL or another database - Managing backups and storage - Handling authentication and user management - Keeping everything updated and secure For small teams, this is a lot of overhead just to share test results. ### CI Pipeline Integration Allure requires adding the Allure commandline tool to your CI environment, generating reports, then uploading them somewhere your team can access. This works, but it's another moving part to maintain. ### Report Hosting Allure generates static HTML reports, but hosting them requires additional setup. Common approaches: - GitHub Pages (works but limited for private repos) - S3 + CloudFront (more infrastructure to manage) - Self-hosted web server (even more maintenance) ## Gaffer: Hosted Test Reporting Gaffer takes a different approach: hosted test reporting with zero infrastructure. ### No Self-Hosting Gaffer is a SaaS platform. No servers to manage, no databases to configure, no updates to apply. Sign up and start uploading test results. ### Simple CI Integration One step in your CI pipeline: ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results ``` Or with curl if you prefer: ```bash curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/report.html" ``` One YAML step is all it takes — no additional tools in CI, no database, no server to maintain. Teams migrating from Allure typically have their first report uploaded within ten minutes of creating a Gaffer project. ### Instant Team Sharing Every upload gets a shareable URL. Send it in Slack, paste it in a PR, or bookmark it. No downloads, no zip files, no "let me spin up the report server." ### Built-in Flaky Test Detection Gaffer automatically tracks test stability over time and identifies flaky tests by analyzing flip rates. No additional setup required: just upload your results. Full detail in [flaky test detection](/solutions/flaky-test-detection/). ## Feature Comparison | Feature | Allure Report | Allure TestOps | Gaffer | |---------|---------------|----------------|--------| | Test reports | Local HTML | Hosted | Hosted | | Historical trends | Manual setup | Yes | Yes | | Flaky test detection | No | Yes | Yes | | Team sharing | Manual hosting | Yes | Yes | | Self-hosting required | N/A | Yes | No | | Database required | No | Yes | No | | Pricing | Free | Per user | Flat-rate tiers | | Setup time | Minutes | Hours/Days | Minutes | ## Framework Support Both Allure and Gaffer support major test frameworks: | Framework | Allure | Gaffer | |-----------|--------|--------| | Playwright | Yes | Yes | | Jest | Yes | Yes | | Vitest | Via adapter | Yes | | pytest | Yes | Yes | | JUnit | Yes | Yes ([JUnit XML guide](/blog/junit-xml-format-guide/)) | | Cypress | Yes | Yes (via CTRF) | Gaffer also supports CTRF (Common Test Report Format), which provides adapters for 20+ test frameworks. ## When to Use Allure Allure might be a better fit if you: - Already have infrastructure and ops capacity to self-host - Need Allure's specific annotation features (@Step, @Attachment) - Have strict data residency requirements that prevent SaaS usage - Want to avoid any SaaS vendor dependency ## When to Use Gaffer Gaffer is probably a better fit if you: - Want test reporting without infrastructure overhead - Have a small team without dedicated DevOps - Need quick setup (minutes, not hours) - Want built-in Slack notifications and GitHub integration - Prefer flat-rate pricing with unlimited users over per-seat ## Migration from Allure Already using Allure? Gaffer works alongside your existing setup: 1. Keep generating Allure reports if your team is used to them 2. Add the Gaffer upload step to CI 3. Use CTRF or JUnit XML format for Gaffer analytics 4. Gradually shift to Gaffer for sharing and history No big-bang migration required. ## Other Comparisons Evaluating multiple tools? See how Gaffer compares to [ReportPortal](/solutions/reportportal-alternative/), [Currents.dev](/solutions/currents-dev-alternative/), and [TestDino](/solutions/testdino-vs-gaffer/). ## Try It Gaffer's free tier includes 500 MB storage and 7-day retention. Enough to evaluate whether it fits your workflow.
--- ## How to Analyze Test Failures Across CI Runs (Without Drowning in Logs) Source: https://gaffer.sh/solutions/analyze-test-failures/

A CI run goes red with 14 failed tests. You open the job log and start scrolling: stack traces, framework noise, retry output, the occasional unrelated warning. Twenty minutes later you've confirmed what you suspected on line one, that most of those failures share a single cause. Analyzing test failures shouldn't mean reading thousands of lines of console output to find the three that matter.

## What Analyzing Test Failures Actually Means for a CI/CD Team Analyzing test failures is the work of turning a list of red results into a short list of decisions: what to fix now, what to ignore, what to file. The raw CI log gives you the symptoms. The job is to find the cause, decide who owns it, and confirm the fix landed. ### Test failure vs. error: the distinction that changes your response A **test failure** is an assertion that didn't hold: the code ran, produced a result, and the result was wrong. `expected 200, received 500`. A **test error** is the test itself not completing: a thrown exception, a timeout, a missing fixture, a connection refused before any assertion ran. The distinction matters because the response differs. A failure usually points at application code or a stale expectation. An error often points at the environment, a dependency, or test setup. Treating every red result the same way is how teams waste time mocking around an assertion when the real problem is that a service never came up. ## Why Test Failures Are Hard to Triage at Scale One failed test is easy. A suite that produces dozens of failures across multiple CI runs per day is where triage breaks down, and where most teams fall into one of three traps. **The log-scanning trap.** You read the CI job output top to bottom. It works for a handful of failures and collapses past that. Logs are ordered by execution time, not by cause, so five failures from one dead database land scattered between unrelated passes and warnings. You reconstruct the pattern by hand, every single run. **The rerun-until-green trap.** Hit re-run, hope the failures were flaky, merge if they clear. Sometimes they were flaky. Sometimes you just rolled the dice on a real bug and got lucky. Either way you learned nothing about which tests are actually unreliable, so you do it again next week. **The spreadsheet trap.** Someone starts tracking failures manually: test name, date, "looking into it." It's stale within a week because nothing updates it automatically, and it answers none of the questions that matter, like whether this failure has happened before or how often. ## The Four Types of Test Failures and the Right Response to Each Every red result falls into one of four buckets, and each one calls for a different action. **Consistent failures** fail on every run with the same error. These are real regressions. The right response: fix the code or update the expectation, then confirm the test passes. **Flaky failures** pass and fail without code changes. The right response is not to re-run until green. It's to [identify the flaky test](/solutions/flaky-test-detection/) by its history and either stabilize it or quarantine it so it stops polluting triage. **New failures** are tests that started failing on a specific run. The right response: tie the failure to the change that introduced it and route it to whoever made that change. **Environment failures** are errors, not assertion failures: connection refused, missing variable, wrong runtime version. The right response is to fix the runner or the setup, not the test. These often cascade, which is exactly where clustering earns its keep. ## How Failure Clustering Changes Triage Most reporting tools show failures as a flat list ordered by test name or execution order. That ordering hides the thing you most need to see: which failures share a cause. [Failure clustering](/solutions/failure-clustering/) groups failures by their error signature instead of by test name. Gaffer extracts the error message from each failing test, strips the variable parts (timestamps, UUIDs, port numbers), and groups tests with matching signatures into a single cluster named after the shared error. Fourteen red tests become "Connection refused (9 tests)" and "expected 200, received 500 (5 tests)". You're now looking at two problems, not fourteen. The clustering runs on parsed error output, so it works the same whether the report came from Playwright, Jest, Vitest, pytest, or JUnit XML. No configuration, no per-framework rules. ## Step-by-Step: Triaging a Failure Cluster in Gaffer ### Step 1: Upload the CI report Add one step to your pipeline after the test run. GitHub Actions: ```yaml - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results/results.xml ``` The `if: always()` matters: without it the upload is skipped exactly when tests fail, which is when you need the report. ### Step 2: Read the clusters Open the run. Clusters appear below the failure list, sorted by how many tests each one affects. Start at the top, because the largest cluster is usually one fix. ### Step 3: Separate flaky from consistent For each cluster, check whether those tests have failed before without code changes. A cluster of known-flaky tests gets a different owner and a different priority than a cluster of consistent regressions. This is the step the rerun-until-green habit skips. ### Step 4: Assign or file an issue Consistent cluster: file one issue for the shared cause, not one per test. New failures: route to the author of the introducing change. Flaky cluster: send it to whoever owns test stability. ### Step 5: Confirm it clears After the fix merges, the next run either shows the cluster gone or it doesn't. No manual spreadsheet update, no guessing whether the re-run was luck. ## What Gaffer Shows That Raw CI Logs Don't A CI log is a snapshot of one run. It can't answer the questions that actually drive triage decisions, because those questions span runs. - **Cross-run persistence:** has this exact failure happened before, or is it new on this commit? A log can't tell you. Gaffer keeps the failure history, so you can tell a fresh regression from a failure that has come back before. - **Frequency:** is this test failing 2% of the time or 40%? That number decides whether you stabilize it or quarantine it, and it only exists if something is tracking runs over time. - **Co-occurrence:** which failures keep showing up together? Tests that fail as a group point at a shared dependency, and the cluster makes that obvious instead of leaving you to notice it across scrolling sessions. To get failures in front of the right person without anyone watching CI, connect [test failure notifications](/solutions/test-failure-notifications/): Slack or webhook alerts with the pass/fail summary and a direct link to the hosted failure detail, filterable by branch so feature branches don't spam the channel. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention, enough to wire up the upload step and watch a few real CI runs cluster their own failures.

Stop reconstructing failure patterns by hand

Gaffer clusters failures by root cause across runs and keeps the history a CI log throws away. Free tier, no credit card.

Start Free
--- ## CI Test Artifacts Keep Expiring? Here's the Fix Source: https://gaffer.sh/solutions/ci-test-artifacts-expiring/ import { Image } from "astro:assets";

You're debugging a production issue. You remember a test failed with the same error three months ago. You go to find that test report... and it's gone. The CI artifacts expired. Sound familiar?

## Why CI Providers Delete Your Test Artifacts Every major CI/CD platform automatically deletes artifacts after a retention period: | CI Provider | Default Retention | Max Retention | |-------------|-------------------|---------------| | GitHub Actions | 90 days | 400 days (paid) | | GitLab CI | 30 days | Configurable | | CircleCI | 30 days | 30 days | | Azure DevOps | 30 days | Configurable | | Bitbucket Pipelines | 14 days | 14 days | **Why do they do this?** Storage costs money. CI providers optimize for build speed and compute, not long-term artifact storage. Your test reports are considered disposable. ## The Real Cost of Expiring Artifacts Losing test artifacts isn't just annoying—it has real consequences: ### 1. Debugging Becomes Harder When a bug resurfaces, historical test data is invaluable. Which tests were failing before we "fixed" this? What did the error message say? Without the original artifacts, you're starting from scratch. ### 2. Flaky Test Analysis is Impossible To understand flaky tests, you need data across many runs. If artifacts expire after 30 days, you lose the historical context needed to identify patterns. ### 3. Compliance Requirements Regulated industries (finance, healthcare, automotive) often require test evidence retention for years. CI artifact expiration violates these requirements. ### 4. Onboarding Suffers New team members can't see how the test suite evolved. They can't reference past failures to understand why certain tests exist. ### 5. Post-Mortems Lack Evidence "What went wrong?" is hard to answer when the test reports from the incident are gone. ## Workarounds (and Why They Fall Short) ### Increase CI Retention Some CI providers let you extend retention. GitHub Actions goes up to 400 days with paid plans. But: - Still not permanent - Expensive at scale (GitHub charges for storage) - Doesn't solve the access problem (finding artifacts is still tedious) ### Upload to S3/GCS You can add a CI step to upload artifacts to your own cloud storage: ```yaml - name: Upload to S3 run: aws s3 cp ./test-results s3://my-bucket/reports/${{ github.sha }} --recursive ``` Problems: - Manual setup and maintenance - No built-in access control - No UI for browsing reports - Still need to manage storage costs and cleanup ### Save Locally Some teams download important artifacts before they expire. This doesn't scale and relies on someone remembering to do it. ## The Real Solution: Dedicated Test Report Hosting Instead of fighting CI artifact limits, host your test reports on a platform designed for it: 1. **Automatic upload** from CI pipeline 2. **Configurable retention** per project (up to 90 days on paid plans) 3. **Instant access** via shareable URLs 4. **Team-based permissions** tied to your organization 5. **Search and analytics** across all historical runs ## How Gaffer Solves CI Artifact Expiration Gaffer is purpose-built for test report hosting. Here's how it works: ### Step 1: Add One Line to CI **GitHub Actions:** ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results ``` **Any CI with curl:** ```bash curl -X POST https://api.gaffer.sh/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/report.html" \ -F "branch=$CI_BRANCH" \ -F "commit=$CI_COMMIT_SHA" ``` ### Step 2: Choose Your Retention Every upload gets a shareable URL. You control how long reports are kept: | Plan | Default Retention | Per-Project Settings | |------|-------------------|---------------------| | Free | 7 days | Custom retention periods | | Pro | 30 days | Custom retention periods | | Team | 90 days | Custom retention periods | **Longer retention than CI providers** - Keep test data for up to 90 days with Team plans, plenty of time for debugging and analysis. **Want to keep costs low?** Enable automatic cleanup and set a shorter retention period. Test reports from feature branches might only need 7 days, while production test data could be kept longer. ### Step 3: Access Anytime - Browse reports in the Gaffer dashboard - Search by branch, commit, date range - Share links with teammates - Get Slack notifications with direct links ## Per-Project Retention Control Different projects have different needs. Gaffer lets you configure retention per-project: - **Production tests**: Keep for 90 days (Team plan) - **Feature branch tests**: Auto-delete after 7 days - **Important projects**: Use longer retention periods All plans can set custom retention periods to match your needs. ## Beyond Storage: Analytics Over Time When you keep test data longer, you unlock insights that short CI retention makes difficult: - **Pass rate trends** - Is the suite getting more stable? - **Flaky test detection** - Which tests fail intermittently? - **Regression detection** - When did this test start failing? - **Duration trends** - Are tests getting slower? ## Migration Path Already have test reports scattered across CI providers? Gaffer works alongside your existing setup: 1. Add Gaffer upload to CI (takes 5 minutes) 2. New reports go to Gaffer automatically 3. Old reports in CI continue to expire (nothing changes) 4. Over time, Gaffer becomes your source of truth No migration, no data import, just better going forward. ## Get Started Stop losing test artifacts. Gaffer gives you control over retention with per-project settings and longer retention periods than most CI providers offer.
--- ## Track Code Coverage Trends Over Time Source: https://gaffer.sh/solutions/code-coverage/ import ScreenshotLink from "../components/ScreenshotLink.astro"; import coverageSummary from "../assets/screenshots/coverage-summary.png";

Your CI generates a coverage report every run. But by next month, that report is gone — CI artifacts expire, and with them, any record of what your coverage looked like. You can't track whether coverage is improving or regressing because there's no historical data.

## The Problem: Coverage Without History Most teams treat code coverage as a point-in-time check. CI runs, coverage is 74%, the build passes. But that number alone doesn't answer the questions that matter: - **Is coverage going up or down?** A single number doesn't tell you direction - **When did coverage drop?** Without history, you can't pinpoint the commit that removed tests - **Are new files getting tested?** You can't tell if recent code has coverage without comparing over time - **Is the team hitting coverage goals?** Progress requires a baseline and a trend line ### Why This Happens **CI artifacts expire.** GitHub Actions keeps artifacts 90 days max. Most teams set shorter retention. Once they're gone, the coverage data is gone. **No built-in trend tracking.** Coverage tools (Istanbul, coverage.py, gcov) generate reports for the current run. They don't store or compare across runs. **Context disappears.** A coverage report says `utils/parser.ts` has 45% coverage. It doesn't tell you it had 60% last week before someone deleted tests, or that it's been at 45% for six months while the file tripled in size. ## Better: Persistent Coverage Tracking The fix is to store coverage data from every run and visualize it over time. Every data point builds a picture of where your test suite is heading. ### What This Looks Like - **Trend charts** showing line, branch, and function coverage over days, weeks, or months - **Regression alerts** when coverage drops after a commit - **Per-file breakdown** showing which files have the lowest coverage - **Commit linking** connecting coverage changes to specific code changes ## Setting Up Coverage Tracking with Gaffer ### Step 1: Generate LCOV Output Most test frameworks can output LCOV format. For a full walkthrough of provider choice, threshold enforcement, and CI setup, see the [Vitest coverage reports guide](/blog/vitest-coverage-reports/). ```bash # JavaScript/TypeScript (Vitest) vitest run --coverage # JavaScript/TypeScript (Jest) jest --coverage --coverageReporters=lcov # Python coverage run -m pytest && coverage lcov # Go go test -coverprofile=coverage.out ./... ``` ### Step 2: Upload to Gaffer Add coverage upload to your CI pipeline alongside test results: ```yaml # GitHub Actions - name: Upload coverage to Gaffer if: always() run: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: ${{ secrets.GAFFER_TOKEN }}" \ -F "files=@coverage/lcov.info" \ -F 'tags={"commitSha":"${{ github.sha }}","branch":"${{ github.ref_name }}"}' ``` ### Step 3: View Trends The analytics dashboard shows coverage over time. Spot regressions immediately. Track progress toward goals. ## What You Get ### Trend Visualization See line, branch, and function coverage plotted over time. Zoom in on specific date ranges. Compare branches. ### Regression Detection and Coverage Gating When coverage drops after a commit, you see exactly when it happened and can correlate with the code change that caused it. You can also enforce coverage gates directly on pull requests — Gaffer posts a GitHub commit status that fails if coverage drops below a threshold or regresses by more than a configured delta compared to your default branch. No more relying on manual review to catch coverage regressions. ### Per-File Breakdown Drill down to see which files have the lowest coverage. Focus testing efforts where they'll have the most impact instead of chasing an aggregate number. ### Commit Correlation Coverage data is linked to specific commits and branches. See how each PR affects overall coverage before merging. Combined with commit status gating, you get automated enforcement — PRs that reduce coverage beyond your tolerance are flagged before review. ## Supported Formats Currently in beta with support for LCOV, Cobertura XML, JaCoCo XML, and Clover XML — covering JavaScript, TypeScript, Python, Go, Java, and more. Read the full documentation → ## Get Started Gaffer's free tier includes coverage tracking. Upload your LCOV data and start building a coverage history.
--- ## Currents.dev Alternative: Flat Pricing and Unlimited Users Source: https://gaffer.sh/solutions/currents-dev-alternative/

[Currents.dev](https://currents.dev/) is a mature Cypress-era test analytics product, now oriented around enterprise workflows (SSO/SCIM, Slack Connect, 1-year retention). Its Team plan is $49/month for 10 seats and 10,000 test results per month, with $5 per additional 1,000 results on top. Teams start shopping for a Currents.dev alternative when they cross 10 users, hit the test-result cap, or simply don't want a per-usage meter on their CI output. Gaffer prices on storage, not seats or test volume: $49/month flat, unlimited users, 50 GB, 90-day retention.

## Why Teams Look for Currents.dev Alternatives ### Seat Count Ceiling on the Team Plan Currents' Team plan includes 10 seats. Past that, the path is Enterprise with custom pricing. For a 15-person engineering team that wants everyone to see test results, the Team plan doesn't fit and the next step is a sales call. ### Test-Result Metering The 10,000 test results per month included on Team sounds generous until you do the arithmetic. A team running 300 CI runs per day at 50 tests per run produces roughly 450,000 test results per month. At $5 per additional 1,000 results, that's around $2,200/month in metered overage on top of the $49 base. Usage scales with your test suite size and CI frequency, both of which tend to grow. ### No Free Tier Currents does not publish a free tier. Evaluation means a trial window, not a persistent free workspace you can leave running on a side project. ## Gaffer: Same Outcomes, Different Economics ### Flat Pricing, Unlimited Users Gaffer has three tiers. Free is $0 with 500 MB and 7-day retention. Pro is $15/month with 10 GB and 30-day retention. Team is $49/month with 50 GB and 90-day retention. Every tier includes unlimited users. Overage on Pro and Team is $0.50/GB/month. There is no test-result cap; storage is the only billed resource. ### Same CI Upload Pattern One step in your CI pipeline: ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results ``` Or with curl: ```bash curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/report.xml" ``` JUnit XML and CTRF are both first-class. Playwright, Jest, Vitest, pytest, and Cypress all work. ### MCP Code-Mode for Agentic CI Both products expose an MCP server. The shapes differ. Currents' MCP provides roughly 25 CRUD tools for its Dashboard, GitHub PR, CLI, and Slack surfaces. Gaffer's MCP uses code-mode: three primitives (`search_tools`, `execute_code`, and a small set of named functions like `get_project_health`, `get_flaky_tests`, `get_slowest_tests`, `get_failure_clusters`, `get_test_history`, `get_coverage_summary`, `find_uncovered_failure_areas`, `compare_test_metrics`, `search_failures`, `list_test_runs`). The agent composes JavaScript that calls those functions, rather than issuing one MCP call per CRUD endpoint. A typical agent session is one `execute_code` call that does the work of 5 to 10 CRUD requests. See [GitHub Agentic Workflows with Gaffer MCP](/blog/github-agentic-workflows-test-analytics/) for the reasoning behind this shape. ### Built-in Analytics Without Extra Configuration Flaky detection, slowest-test surfacing, and failure clustering run on every upload. As a calibration point, Gaffer's own 30-day metrics (pulled via the MCP server) currently sit at a 95 health score, 99.96% pass rate over 200 runs, 4 flaky tests, and a p95 duration of 62 seconds on the slowest test. Those are the same numbers the dashboard and MCP tools would return for any project with comparable activity. ## Pricing Comparison | Tier | Currents.dev | Gaffer | |------|--------------|--------| | Free | Not offered | $0, 500 MB, 7-day retention, unlimited users | | Entry paid | Not offered | $15/month, 10 GB, 30-day retention, unlimited users | | Team | $49/month, 10 seats, 10K test results/month, +$5 per additional 1K results, up to 1-year retention | $49/month, unlimited users, 50 GB, 90-day retention | | Overage | $5 per 1,000 test results over cap | $0.50/GB/month (Pro and Team) | | Enterprise | Custom (SSO/SCIM, Slack Connect, data redaction) | Not offered | ## Feature Comparison | Feature | Currents.dev | Gaffer | |---------|--------------|--------| | Test report hosting | Yes | Yes | | Historical trends | Yes | Yes | | Flaky test detection | Yes | Yes | | Slowest-test and failure clustering | Yes | Yes | | MCP server | Yes (CRUD tools) | Yes (code-mode) | | GitHub commit status | Yes | Yes | | Slack notifications | Yes (incl. Slack Connect on Enterprise) | Yes | | Webhook notifications | Yes | Yes | | SSO / SCIM | Enterprise | Not available | | Jira / MS Teams / GitLab / BitBucket integrations | Yes | Limited | | Self-hosting | Enterprise | Not available | | Retention (paid) | Up to 1 year | Up to 90 days | | User pricing | Per seat (10 included on Team) | Unlimited users, all tiers | | Free tier | No | Yes | ## When to Use Currents.dev Currents is the better fit if you need: - SSO or SCIM today, not on a roadmap - Slack Connect, Jira, MS Teams, GitLab, or BitBucket as first-class integrations - A team larger than 10 users where per-seat pricing has already been negotiated - Retention longer than 90 days (up to 1 year on Team) - Self-hosting via an Enterprise contract ## When to Use Gaffer Gaffer is the better fit if: - Your team is approaching or past 10 users and per-seat pricing is the binding constraint - You want a free tier for evaluation or for side projects - Storage-based pricing matches your workload better than test-result metering - You want the MCP code-mode primitive for agentic CI workflows. See [Test Intelligence for AI Tools](/blog/test-intelligence-for-ai-tools/) - You prefer flat billing without a per-usage meter on CI output ## Migration from Currents.dev No big-bang cutover needed. 1. Keep uploading to Currents. Nothing breaks. 2. Add a Gaffer upload step to CI using JUnit XML or CTRF output. 3. Point the team at Gaffer for sharing and historical analytics. Retire Currents when you're ready. ## Other Comparisons Evaluating multiple tools? See how Gaffer compares to [Allure](/solutions/allure-alternative/), [ReportPortal](/solutions/reportportal-alternative/), and [TestDino](/solutions/testdino-vs-gaffer/). ## Try It Gaffer's free tier includes 500 MB storage and 7-day retention. Enough to run your CI against it for a week and decide.
--- ## How to Share Cypress Test Results with Your Team Source: https://gaffer.sh/solutions/cypress-test-reporting/

Cypress generates test results, but sharing them with your team means either paying for Cypress Cloud or pasting terminal output into Slack. If you want hosted results with shareable links, analytics, and notifications without Cypress Cloud pricing, there's a simpler path.

## The Cypress Reporting Problem Cypress runs tests and prints results to the console. The built-in dashboard experience requires Cypress Cloud (formerly Cypress Dashboard), which is a paid service for teams that need parallelization and analytics. ### What Teams Do Instead **1. Cypress Cloud** - Good feature set: parallelization, analytics, flaky test management - Pricing scales with recorded test runs — can get expensive for active teams - Tightly coupled to Cypress — doesn't help if you also run Jest, Playwright, or pytest **2. CI artifacts** - Cypress screenshots and videos upload as artifacts - Recipients need CI access to download zips - Artifacts expire (90 days max on GitHub Actions) - No analytics, no historical trends **3. Mochawesome reports** - Generates HTML reports locally, but hosting them is your problem - No built-in sharing, analytics, or notification support - Another reporter to configure and maintain **4. Terminal output in Slack** - Loses all context for screenshots and videos - Truncated for large suites - No way to filter or drill into failures ## Better: CTRF + Hosted Reports Cypress doesn't produce a report format that Gaffer parses natively (like Playwright HTML or JUnit XML). Instead, the integration uses [CTRF (Common Test Report Format)](https://ctrf.io/) — a standardized JSON format with reporters for 20+ test frameworks including Cypress. The setup: 1. Add the `cypress-ctrf-json-reporter` to your project 2. Cypress generates a CTRF JSON file after tests run 3. Upload the CTRF file to Gaffer in CI 4. Get a shareable URL, Slack notification, and analytics ### What This Looks Like After CI runs: - Your team gets a Slack notification with pass/fail summary - Anyone can click through to the full hosted report - Test analytics accumulate over time: pass rates, flaky tests, slow tests - No Cypress Cloud subscription required ## Setting Up Cypress Reporting with Gaffer ### Step 1: Install the CTRF Reporter ```bash npm install cypress-ctrf-json-reporter --save-dev ``` ### Step 2: Configure Cypress Add the reporter to your `cypress.config.ts` via the `setupNodeEvents` hook: ```typescript import { defineConfig } from 'cypress'; import { GenerateCtrfReport } from 'cypress-ctrf-json-reporter'; export default defineConfig({ e2e: { setupNodeEvents(on) { new GenerateCtrfReport({ on }); }, }, }); ``` This generates a `ctrf/ctrf-report.json` file after each test run. You can customize the output: ```typescript new GenerateCtrfReport({ on, outputFile: 'ctrf-report.json', outputDir: '.', }); ``` ### Step 3: Add the Upload Step to CI **GitHub Actions:** ```yaml - name: Run Cypress tests run: npx cypress run - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./ctrf/ctrf-report.json ``` **GitLab CI:** ```yaml test: script: - npx cypress run after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf/ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ### Step 4: Share the Link After upload, share the report URL in Slack, GitHub PRs, or Jira. Recipients see structured test results in their browser — no CI access needed. Need to share with someone outside your organization — a contractor, client, or partner team? Generate a [public share link](/solutions/shareable-test-report-links/) with an expiration you control (1 hour to 30 days, or never). Recipients view the results in their browser without needing a Gaffer account. ## Slack Notifications Once you're uploading results, connect Slack in project settings for automatic notifications: - Pass/fail summary with test counts - Direct link to the full report - Filter by branch — only notify on `main`, skip feature branches - Choose triggers: all runs, failures only, or consecutive failures ## Comparing Cypress Reporting Options | Feature | Cypress Cloud | Mochawesome | CI Artifacts | Gaffer (via CTRF) | |---------|--------------|-------------|--------------|-------------------| | Hosted reports | Yes | Self-host | Download zips | Yes | | Shareable links | Yes | Manual | With CI access | Yes | | Analytics/trends | Yes | No | No | Yes | | Flaky test detection | Yes | No | No | Yes | | Slack notifications | Yes | No | No | Yes | | Works with other frameworks | No | No | N/A | Yes | | Parallelization | Yes | No | No | No | | Video recording | Yes | No | Via artifacts | No | | Pricing | Paid (usage-based) | Free | Free | Free tier available | ### When Cypress Cloud Makes Sense Cypress Cloud is worth it if you need test parallelization (splitting tests across machines), video recording with cloud storage, or you're all-in on Cypress with no other frameworks. ### When Gaffer Makes Sense Gaffer is a better fit if you want a unified view across multiple frameworks (Cypress + Jest + Playwright), want test analytics without per-seat pricing, or need Slack/webhook notifications with more flexible triggers. ## Beyond Cypress: Multi-Framework Support Since Gaffer works with CTRF, you can upload results from any framework that has a CTRF reporter. One dashboard for: - **Cypress** (via `cypress-ctrf-json-reporter`) - **Playwright** (native HTML reports) - **Jest** (via `jest-ctrf-json-reporter`) - **Vitest** (native HTML reports) - **pytest** (via JUnit XML or CTRF) All results appear in the same project dashboard with unified analytics. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Enough to evaluate the CTRF workflow with your Cypress suite.
--- ## How to Get Test Metrics into Datadog Source: https://gaffer.sh/solutions/datadog-test-metrics/

Your services have dashboards, alerts, and SLOs in Datadog. Your test suite doesn't. Here's how to get test metrics into Datadog without writing custom instrumentation.

## The Problem You run hundreds of tests in CI every day, but the results live in CI logs. To answer "is our test suite healthy?" you check individual pipeline runs. There's no trend line, no alert when pass rates drop, and no way to correlate test health with deployment metrics. ### What Teams Usually Try **1. Custom metric scripts in CI** - Parse test output, POST to Datadog's API in an `after_script` block - Breaks when test runner output format changes - Every team reinvents this differently **2. Datadog CI Visibility** - Full-featured but requires Datadog's test runner instrumentation - Per-test pricing can add up at scale - Tightly coupled to Datadog — hard to switch later **3. Manual monitoring** - Someone checks CI periodically - Problems are discovered after they've compounded ## A Simpler Approach Gaffer already stores your test results and coverage reports. With OpenTelemetry export, it pushes metrics to Datadog automatically — pass rates, failure counts, test totals, and coverage percentages arrive as standard OTLP metrics. No test runner plugins. No CI script changes beyond what you already have for Gaffer uploads. ## Setup If you're already uploading test results to Gaffer, adding Datadog export takes a few steps: 1. Go to **Settings > OpenTelemetry** in the Gaffer dashboard 2. Click **"Add destination"** and select **Datadog** 3. Paste your Datadog API key and select your site region 4. Click **"Test"** to verify connectivity For full setup instructions including Grafana Cloud and generic OTLP endpoints, see the [OpenTelemetry integration guide](/docs/integrations/opentelemetry/). ## Dashboard Template We've published a Datadog dashboard template you can import directly. It includes: - **Overview gauges** — current pass rate, total tests, failed count, skipped count - **Pass rate over time** — line chart of `gaffer.tests.pass_rate` over 30 days, grouped by branch - **Test counts by result** — stacked bar chart of passed/failed/skipped per day - **Coverage trends** — line and branch coverage percentages over time ### Import the Dashboard 1. Download the [dashboard template](/templates/datadog-dashboard.json) 2. In Datadog, go to **Dashboards > New Dashboard** 3. Click the gear icon and select **Import Dashboard JSON** 4. Paste or upload the JSON file 5. Use the `$project` template variable to filter by Gaffer project ## Available Metrics | Metric | Type | Description | |--------|------|-------------| | `gaffer.tests.pass_rate` | Gauge (%) | Pass rate of the test run | | `gaffer.tests.total` | Gauge | Total test count | | `gaffer.tests.passed` | Gauge | Passed test count | | `gaffer.tests.failed` | Gauge | Failed test count | | `gaffer.tests.skipped` | Gauge | Skipped test count | | `gaffer.coverage.line_percent` | Gauge (%) | Line coverage percentage | | `gaffer.coverage.branch_percent` | Gauge (%) | Branch coverage percentage | All metrics include `gaffer.project.name`, `gaffer.branch`, and `gaffer.commit_sha` as attributes for filtering. ## What You Get - **Alerts** — set a Datadog monitor on `gaffer.tests.pass_rate` dropping below a threshold - **Correlation** — overlay test health with deploy events, error rates, and service metrics - **Trend visibility** — spot gradual test suite degradation before it becomes a problem - **Team dashboards** — share test health alongside service dashboards that your team already checks daily
--- ## How to Share .NET Test Results with Your Team Source: https://gaffer.sh/solutions/dotnet-test-reporting/

`dotnet test` produces results, but everyone who wants to see them has to go somewhere different: the Azure DevOps pipeline tab, a CI artifact zip, or the Visual Studio Test Explorer on whoever ran them locally. None of those work for a quick "did the suite pass?" link in a PR or Slack thread. There's a simpler path that reuses output your build already generates.

## The .NET Test Reporting Problem `dotnet test` runs MSTest, NUnit, and xUnit suites and prints results to the console. Turning that console output into something a teammate can open in a browser is left to you, and the common options each trap the results somewhere. ### What Teams Do Instead **1. Azure DevOps "Publish Test Results" task** - Solid in-pipeline view: pass/fail counts, history, failed-test drill-down - Locked to the Azure DevOps pipeline UI. There's no link you can hand to someone outside the org - Tied to the build it ran in. No unified view if some suites run elsewhere or in a different pipeline **2. Visual Studio Test Explorer** - Good for the developer at the keyboard - Local only. There's nothing to share. Results live and die in one IDE session **3. CI artifacts (.trx zips)** - The `.trx` file uploads as a build artifact - Recipients need CI access to download and a TRX viewer to read it - Artifacts expire (90 days max on GitHub Actions) and carry no historical trends **4. Pasting console output into Slack** - Loses the structure: no per-test status, no stack traces you can expand - Truncated for large suites - No way to filter failures or compare against the last run ## Better: TRX + Hosted Reports You don't need a new reporter. `dotnet test --logger:trx` writes a `.trx` file, and it does so whether the project uses MSTest, NUnit, or xUnit. The `dotnet test` CLI emits TRX for all three. Gaffer parses TRX natively, so the integration is: tell `dotnet test` to write TRX, then upload the file in CI. The setup: 1. Run `dotnet test --logger:trx` 2. Upload the `.trx` file to Gaffer in CI 3. Get a shareable URL, analytics, and flaky test detection If you prefer JUnit XML (for example to standardize across non-.NET suites), the `JUnitXml.TestLogger` NuGet package adds a `--logger:"junit"` option, and Gaffer parses JUnit XML natively too. ### What This Looks Like After CI runs: - Anyone with the link sees the full hosted report in their browser - Failures keep their structure: per-test status, duration, and stack traces - Analytics accumulate over time: pass rates, [flaky tests](/solutions/flaky-test-detection/), slow tests - No Azure DevOps access required to view results ## Setting Up .NET Reporting with Gaffer ### Step 1: Write a TRX File Add the TRX logger to your test run. This works for MSTest, NUnit, and xUnit projects without changing test code: ```bash dotnet test --logger:trx --results-directory ./TestResults ``` This writes a `.trx` file into `./TestResults`. The filename includes a timestamp, so reference the directory in the upload step rather than a fixed name. ### Step 2: Add the Upload Step to CI **GitHub Actions:** ```yaml - name: Run tests run: dotnet test --logger:trx --results-directory ./TestResults - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./TestResults/*.trx ``` **Azure Pipelines (curl):** ```yaml - script: dotnet test --logger:trx --results-directory $(Build.SourcesDirectory)/TestResults displayName: Run tests - script: | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $(GAFFER_PROJECT_TOKEN)" \ -F "files=@$(ls TestResults/*.trx | head -n1)" \ -F 'tags={"commitSha":"$(Build.SourceVersion)","branch":"$(Build.SourceBranchName)"}' displayName: Upload to Gaffer condition: always() ``` The `if: always()` and `condition: always()` guards matter: you want the report uploaded even when tests fail, since that's when the report is most useful. ### Step 3: Share the Link After upload, drop the report URL into a PR, Slack, or your Azure DevOps work item. Recipients see structured results in their browser with no CI access needed. Need to share with someone outside your organization, like a contractor or client? Generate a [public share link](/solutions/shareable-test-report-links/) with an expiration you control (1 hour to 30 days, or never). They view the results without a Gaffer account. ## Comparing .NET Reporting Options | Feature | Azure DevOps Test Results | CI Artifacts | Gaffer | |---------|---------------------------|--------------|--------| | Hosted reports | In pipeline UI | Download zips | Yes | | Shareable links outside the org | No | With CI access | Yes | | Analytics/trends | In pipeline | No | Yes | | Flaky test detection | Limited | No | Yes | | Works across frameworks | Per pipeline | N/A | Yes | | Pricing | Included with Azure DevOps | Free | Free tier available | ### When Azure DevOps Test Results Makes Sense If your whole team lives in Azure DevOps and every suite runs in the same pipeline, the built-in "Publish Test Results" task covers the in-pipeline case well and costs nothing extra. The friction shows up when you need to share a link outside the org or unify results from suites that run in different pipelines or providers. ### When Gaffer Makes Sense Gaffer fits when you want a link anyone can open, one dashboard across multiple frameworks and pipelines, or test analytics and flaky detection without per-seat pricing. ## Beyond .NET: Multi-Framework Support TRX and JUnit XML are not .NET-specific. The same upload step handles results from any framework Gaffer parses, so a project with a .NET backend, a JavaScript frontend, and a Python service can [report all three to one dashboard](/solutions/multiple-test-frameworks/): - **.NET** (TRX via `dotnet test --logger:trx`) - **JavaScript / TypeScript** (Jest, Vitest, Playwright) - **Python** (pytest via JUnit XML) All results land in the same project with [unified hosted reports](/solutions/host-test-reports/) and shared analytics. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Enough to wire `dotnet test --logger:trx` into CI and see your suite hosted with a shareable link.
--- ## Failure Clustering: Debug Root Causes, Not Individual Tests Source: https://gaffer.sh/solutions/failure-clustering/

Five tests fail in CI. You investigate the first one — "Connection refused." You investigate the second — same error. The third, fourth, fifth — all the same root cause. You just spent 30 minutes discovering what should have been obvious in 3 seconds: one service is down, and every test that depends on it failed.

## What Is Failure Clustering? Failure clustering groups test failures that share the same root cause into a single cluster. Instead of showing you 5 individual failures, it shows you 1 pattern affecting 5 tests. The idea is simple: failures don't happen in isolation. When a database connection drops, every test that queries the database fails. When an API endpoint changes, every test that calls that endpoint fails. Treating each failure independently wastes time. ## Why Individual Test Failures Mislead Most test reporting tools show a flat list of failures: | Test | Status | Error | |------|--------|-------| | `auth.test.ts > login` | FAIL | Connection refused | | `auth.test.ts > signup` | FAIL | Connection refused | | `api.test.ts > get users` | FAIL | Connection refused | | `api.test.ts > create user` | FAIL | ECONNREFUSED 127.0.0.1:5432 | | `db.test.ts > migrations` | FAIL | Connection refused | A developer sees 5 failures and might think 5 things broke. In reality, one thing broke — the database connection — and it cascaded to 5 tests. The fourth failure even has a slightly different error message, making it harder to spot the pattern manually. ### The cost of debugging individually - **Time:** 5-10 minutes investigating each failure before realizing they're the same - **False fixes:** Developers sometimes "fix" individual tests (adding retries, mocking) when the real issue is upstream - **Noise:** A 5-failure CI report feels worse than a 1-pattern report, even though the same amount of work is needed - **Context switching:** Jumping between 5 test files when the fix is in one shared dependency ## How Gaffer Clusters Failures Gaffer analyzes error messages across all failing tests in a run and groups them by shared patterns: ``` Clusters: 1 pattern (5 tests) "Connection refused" — 5 tests auth.test.ts > login auth.test.ts > signup api.test.ts > get users api.test.ts > create user db.test.ts > migrations ``` The algorithm: 1. **Extract error signatures** from each failing test — the error message, stripped of variable parts (timestamps, UUIDs, port numbers) 2. **Group by similarity** — tests with matching signatures form a cluster 3. **Name the cluster** using the most common error message in the group 4. **Sort by impact** — clusters with more affected tests appear first This works across frameworks. Whether your errors come from Playwright, Jest, pytest, or JUnit XML, the clustering applies to the parsed error output. ## When Clustering Matters Most ### Infrastructure failures Database down, Redis unavailable, network partition — these cascade across your entire test suite. Without clustering, a single infrastructure blip looks like 50 unrelated failures. ### API contract changes Someone changes an API response format. Every test that parses that response fails with a similar error. Clustering shows you the one change that needs reverting or adapting. ### Environment issues Missing environment variables, wrong Node version, incompatible dependency — these affect multiple tests in the same way. Clustering reveals the environment problem instead of burying it in individual test errors. ### Flaky infrastructure in CI CI runners sometimes have transient issues — DNS resolution failures, disk space, rate limits. Clustering helps you distinguish "the runner had a bad day" from "our code has bugs." ## Failure Clustering vs. Flaky Test Detection These are complementary features that answer different questions: | | Failure Clustering | Flaky Test Detection | |---|---|---| | **Question** | "Why did these tests fail together?" | "Does this test sometimes fail without code changes?" | | **Scope** | Within a single test run | Across multiple runs over time | | **Action** | Fix the shared root cause | Investigate test non-determinism | | **Example** | 5 tests fail with "Connection refused" | `login` test fails 40% of runs | Used together, you get a complete picture: clustering tells you what went wrong *right now*, and flaky detection tells you what's been unreliable *over time*. ## Getting Started Failure clustering works automatically with any test report uploaded to Gaffer. No configuration required. 1. **Upload a test report** — via [CLI](/docs/getting-started/), [API](/docs/guides/ci-setup/), or direct upload 2. **View clusters** — on the test run detail page, clusters appear below the failure list 3. **Query via CLI** — `gaffer query failures` shows clusters in your terminal 4. **Query via MCP** — AI agents access clusters through `get_test_run_details()`

Stop debugging the same root cause five times

Gaffer clusters failures automatically. Free tier, no credit card.

Start Free
--- ## Faster CI/CD Feedback Loop: Get Test Results in Slack Instantly Source: https://gaffer.sh/solutions/faster-ci-feedback-loop/ import { Image } from "astro:assets"; import slack from "../assets/screenshots/slack-alert.png";

A fast CI/CD feedback loop is essential for developer productivity. When tests fail, you need to know immediately - and you need to access the results without friction. Gaffer delivers test results directly to Slack, cutting your feedback loop from minutes to seconds.

## The CI/CD Feedback Loop Problem Every developer knows this frustrating workflow: 1. Push code and wait for CI to run 2. Notice the build failed (eventually) 3. Navigate to GitHub/GitLab to find the workflow 4. Scroll through logs trying to find the failure 5. Download artifact zip files 6. Extract and open locally to see the actual report This process takes 5-10 minutes *every time* a test fails. Multiply that by the number of failures per day, and you're losing hours to CI archaeology. The worst part? By the time you've found the failure, you've lost your mental context. You've context-switched to Slack, email, or another task. Getting back into flow takes even more time. ## How Gaffer Improves Your CI/CD Feedback Loop Gaffer shortens your feedback loop to a single Slack notification with a direct link to the full test report.
### Instant Slack Notifications When your CI pipeline finishes, Gaffer sends a notification to your configured Slack channel: - **Pass/fail summary** at a glance - **Direct link** to the full HTML report - **Branch and commit context** so you know exactly what triggered it - **Failure details** with test names and error snippets
### Smart Notification Filtering You won't get spammed. Gaffer lets you configure exactly when to notify: - **All runs** - Get notified on every test run - **Failures only** - Only hear about problems - **Consecutive failures** - Alert after multiple failures in a row, filtering out one-off noise - **Branch filtering** - Only notify on `main`, `release/*`, or specific branches ### One-Click Access to Full Reports Click the link in Slack and you're looking at the full test report - no downloads, no zip files. The report is hosted and shareable with your entire team. ## Real Impact on Developer Velocity | Before Gaffer | After Gaffer | |---------------|--------------| | 5-10 min to find failure details | 10 seconds from notification to report | | Context switch to CI provider UI | Stay in Slack, click one link | | Scattered across CI runs | Centralized, searchable history | | Manual log parsing | Structured HTML reports with filtering | ## Setting Up Your CI/CD Feedback Loop Getting started takes less than 5 minutes: ### 1. Add the Gaffer Upload Step ```yaml - name: Upload to Gaffer uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results ``` ### 2. Connect Slack In your Gaffer project settings, click "Connect to Slack" and authorize the integration. ### 3. Configure Notifications Choose which channel to notify, which branches to monitor, and when to send alerts. That's it. Your next CI run will notify your team automatically. ## Beyond Notifications: Full Test Analytics A faster feedback loop is just the start. Gaffer also provides: - **Pass rate trends** - Track test health over time - **[Flaky test detection](/solutions/flaky-test-detection/)** - Identify tests that fail intermittently - **Duration tracking** - See which tests are getting slower - **Historical comparison** - Compare test runs across commits and branches Want more control over when and how you get notified? See [Test Failure Notifications](/solutions/test-failure-notifications/) for details on triggers, branch filtering, and webhook integrations. ## Get Started Gaffer's free tier includes Slack integration with 500 MB of storage and 7-day retention. Connect your first project in under 5 minutes.
--- ## Flaky Test Detection: Find Which Tests Are Unreliable Source: https://gaffer.sh/solutions/flaky-test-detection/ import flakyTestsTable from "../assets/screenshots/flaky-tests-table.png"; import PullCta from "../components/PullCta.astro"; import ScreenshotLink from "../components/ScreenshotLink.astro";

A test fails on `main`, you re-run the pipeline, and it passes. You know flaky tests are eating your team's time, but you can't name the specific tests doing the damage. CI artifacts expire before patterns emerge, retries hide which tests are actually unstable, and nobody has time to keep a spreadsheet. The missing piece is persistent pass/fail history per test.

### What is a flaky test? A **flaky test** is an automated test that produces inconsistent results without any code change: it passes on one run and fails on the next under the same conditions. Flaky tests are distinct from legitimate test failures. A real failure means the code is broken. A flaky test means the test itself is unreliable, typically due to timing dependencies, shared state, or external service calls. A suite with a high proportion of flaky tests trains engineers to ignore red builds, which is how real regressions slip through. ## The Flaky Test Detection Problem Most teams experience flakiness without being able to fix it because the data needed to identify the specific bad tests is gone by the time anyone goes looking. - **Re-runs mask the signal.** A test that passes on attempt 2 looks identical to a test that passed on attempt 1. - **CI artifacts expire.** GitHub Actions defaults to 90 days, GitLab to 30. By the time a pattern is obvious, the underlying runs are gone. - **No history per test.** Build pages show "this run." Nobody shows "this test, last 50 runs." - **Retry configs lie.** `retries: 2` keeps the build green and tells you nothing about which tests needed the retry. - **Quarantine has no exit criteria.** Tests get `.skip`'d, then forgotten. Coverage rots. ### Common (Bad) Solutions **1. Manual re-run tracking** - Slack channel for "this failed but passed on rerun" - Falls apart at >2 engineers; nobody catches the trends **2. Retry everything** - `retries: 3` in CI keeps builds green - Hides root cause, doubles or triples build time, masks regressions **3. Spreadsheets** - Someone updates a sheet with flaky tests after each red build - Dies in week three when that engineer goes on vacation **4. Reading CI artifacts manually** - `actions/upload-artifact` keeps reports for 90 days - Finding the right artifact across hundreds of runs is its own job ## Better Solution: Flip-Rate Analysis Across Runs The metric that actually identifies flaky tests is **flip rate**: how often a test transitions between pass and fail across consecutive runs. A test that stays passing is stable. A test that flips constantly is the one wasting your time. ``` Flip Rate = (number of pass/fail transitions) / (total runs - 1) ``` A test with results `[pass, fail, pass, fail, pass]` has 4 flips across 5 runs, or 80%. | Flip Rate | Interpretation | |-----------|----------------| | 0–10% | Stable test | | 10–30% | Moderately flaky, worth investigating | | 30–50% | Severely flaky, fix immediately or quarantine | | 50%+ | Random; remove from the build until rewritten | Flip rate only works with persistent history. A single run cannot tell you anything. A handful of runs cannot either: Gaffer requires at least 5 runs before evaluating a test, to keep a single bad run from poisoning the score. ## Detecting Flaky Tests with Gaffer Gaffer stores every test result and computes flip rate per test, per branch, automatically. - **Flip rate**: transitions per run, last 30 days - **Flip count**: total pass/fail transitions observed - **Total runs**: sample size, so you know whether to trust the score - **Last seen flaky**: when the most recent flip happened - **Branch filter**: separate `main` flakiness from feature-branch noise No new test framework to adopt, no annotation to add. If your CI already produces JUnit XML, CTRF, Playwright HTML, Jest JSON, or pytest output, Gaffer ingests it and starts the count. Threshold defaults to 10% flip rate before a test is flagged. Tune it higher if your suite is noisy and you want to focus on the worst offenders first. ## Setting Up Flaky Detection in CI One step in your CI pipeline. Test results upload, Gaffer parses them, flaky detection happens server-side. ```yaml # .github/workflows/test.yml - name: Run tests run: npx playwright test --reporter=junit - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results/junit.xml ``` Gaffer supports [multiple test frameworks](/solutions/multiple-test-frameworks/) (Jest, Vitest, pytest, Mocha, and more) with the same setup: emit a JUnit XML or CTRF JSON, point the uploader at it, done. Full setup per CI provider lives in the [CI guides](/docs/guides/). ## Fixing Flaky Tests Once You've Found Them Detection is the hard part. Once you have a list of specific tests with high flip rates, the fixes are mostly mechanical. For deeper background, see [how to manage flaky E2E tests](/blog/how-to-manage-flaky-e2e-tests/) and [how much flaky tests are actually costing your team](/blog/how-much-are-flaky-tests-costing-you/). ### Quarantine first, fix second Don't let a flaky test block the build while you investigate. Move it to a separate suite that runs but doesn't gate merges. Set a calendar reminder; quarantine without an exit date is just deletion. ```javascript // Jest example: skip with a tracking issue test.skip('checkout flow: flaky, see #4127', () => { // ... }); ``` ### Replace arbitrary waits with explicit conditions ```typescript // Bad: fixed timeout, hopes the page is ready await page.waitForTimeout(2000); // Good: wait for the specific thing you're testing await page.waitForSelector('[data-testid="loaded"]'); ``` ### Mock external dependencies Network calls are the single largest source of flakiness in E2E suites. Mock the API at the test boundary; let an integration test exercise the real call. ```typescript await page.route('**/api/data', route => { route.fulfill({ json: mockData }); }); ``` ### Reset shared state per test ```javascript beforeEach(() => { jest.clearAllMocks(); db.reset(); }); ``` ### Run in random order to surface order dependence ```bash jest --randomize ``` ## Comparing Flaky Test Detection Approaches
MethodHistory per testIdentifies which testsBranch-awareSetup
Re-run, hope, repeatNoneNone
Manual spreadsheetManual~ (whoever's tracking)ManualHours/week
CI artifact archaeology30–90 days~ (if you click through)None
Retry configsNoneOne line
GafferUp to 90 days✓ flip rate per testOne CI step
## What Comes With Detection Once test history is hosted, the analytics that depend on it come along: pass-rate trends, duration regressions per test, [failure clustering](/solutions/failure-clustering/) to group related failures automatically, and a [health score](/docs/analytics/) that combines them. When you need to dig into why specific tests are failing, [failure analysis](/solutions/analyze-test-failures/) gives you the full breakdown. Gaffer can also [post a Slack message](/solutions/test-failure-notifications/) the moment a new flaky test crosses your threshold. ## Who Uses Flaky Test Detection Small teams and OSS projects who can't justify dedicated test-infrastructure engineers but still want to know which 10 tests in a 2,000-test suite are responsible for half the red builds. Free tier covers 500 MB storage with 7-day retention. Paid plans extend to 90 days, which is usually long enough for the worst flakiness patterns to surface.
--- ## GitHub PR Test Status: See Test Results Without Leaving GitHub Source: https://gaffer.sh/solutions/github-pr-test-status/ import { Image } from "astro:assets"; import githubPrCommitStatus from "../assets/screenshots/github-commit-statuses.png";

Your PR has 3 green checkmarks and 1 red X. You click the red X, land on a workflow run page, expand the test job, scroll through 200 lines of output, and finally find the failing test. Why is seeing test results in GitHub so hard?

## The GitHub Actions Test Results Problem Every developer knows this workflow: 1. Push code to PR 2. Wait for CI to finish 3. See a red X on the PR 4. Click to see details 5. Land on the workflow run page 6. Click into the job 7. Expand the test step 8. Scroll through logs to find failures **Total time to see what failed: 2-5 minutes** And if you want the full test report? Download artifacts, extract the zip, open the HTML file. Good luck doing that on your phone when you're away from your desk. ## What You Actually Want
- See pass/fail on the PR (GitHub does this) - See *which tests* failed without digging through logs - Click directly to a readable report - Share the report with QA who doesn't have GitHub access
GitHub pull request showing Gaffer commit status with test results and Details link
GitHub's built-in features get you partway there, but not all the way: | Feature | What It Does | Limitation | |---------|--------------|------------| | Status checks | Shows pass/fail | No details about which tests | | Annotations | Shows failures inline | Limited to ~50, no full context | | Job summaries | Markdown in Actions tab | Manual setup, buried in workflow | | Artifacts | Stores reports | Requires download, expires | ## The Solution: Commit Statuses with Report Links The fix is simple: post a commit status that links directly to a hosted test report. ### How It Works ``` Tests run in GitHub Actions ↓ Results uploaded to Gaffer ↓ Gaffer posts commit status to GitHub ↓ PR shows: ✓ Gaffer / Vitest — 47 passed [Details →] ``` ### What Developers See On your pull request, under the checks section: ``` ✓ Gaffer / Playwright — 23 passed Details ✗ Gaffer / Vitest — 45 passed, 2 failed Details ``` Click "Details" and you're taken directly to the full test report in Gaffer. No logs, no artifacts, no extraction. ### What QA and PMs See A simple link they can open in any browser: ``` https://app.gaffer.sh/r/abc123 ``` No GitHub access required. No downloading. Click and view. ## Setting Up the Gaffer GitHub App ### Step 1: Install the GitHub App 1. In Gaffer, go to **Settings → GitHub** 2. Click **Install GitHub App** 3. Choose your GitHub organization or account 4. Select which repositories Gaffer can access ### Step 2: Link a Repository to Your Project 1. Go to your project in Gaffer 2. Navigate to **Settings → GitHub** 3. Select your repository from the dropdown 4. Enable **Commit statuses** ### Step 3: Upload Results with Commit SHA If you're already uploading to Gaffer, just make sure you're passing the commit SHA: ```yaml - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` For pull requests, use the PR head SHA: ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results commit_sha: ${{ github.event.pull_request.head.sha || github.sha }} branch: ${{ github.head_ref || github.ref_name }} ``` That's it. The GitHub App handles posting the status automatically. ## What the Status Looks Like Gaffer posts commit statuses in this format: ``` Gaffer / {framework} ``` For example: - `Gaffer / Playwright` - Your Playwright E2E tests - `Gaffer / Vitest` - Your Vitest unit tests - `Gaffer / Jest` - Your Jest tests If you have multiple test suites, each gets its own status. At a glance, you can see which suite failed. ## Comparison: Finding Test Results | Approach | Time to Results | Click to Report | Works for QA | |----------|-----------------|-----------------|--------------| | Check workflow logs | 2-5 minutes | No | No | | GitHub Annotations | 30 seconds | No | No | | Download artifacts | 1-2 minutes | After extraction | No | | Gaffer commit status | 5 seconds | Yes, one click | Yes | ## Beyond Status Checks Once you have test results flowing through Gaffer, you also get: ### Historical Context See if this test has failed before. Was it flaky last week? The history is right there. ### Flaky Test Detection Tests that fail intermittently are flagged automatically. Know whether a failure is a real bug or just flakiness before you start debugging. ### Slack Notifications Get notified in Slack when tests fail, with a direct link to the report. No need to watch GitHub. ### Cross-Repo Dashboard If you have multiple repositories, see all your projects' test health in one place. ## Getting Started Gaffer's free tier includes 500 MB storage with 7-day retention - enough to try the GitHub integration on a real project. Commit statuses are included on all plans.
--- ## How to Host Test Reports Beyond CI Artifact Expiration Source: https://gaffer.sh/solutions/host-test-reports/

Every CI provider treats test reports as disposable. If you need to host test reports beyond your CI's retention window — to debug a regression, answer a compliance question, or figure out when a test started flaking — the data is gone. GitHub Actions deletes artifacts after 90 days. GitLab defaults to 30. Bitbucket gives you 14.

## CI Artifact Retention Limits Default retention limits across major CI platforms: | CI Provider | Default Retention | Max Retention | Access Method | |-------------|-------------------|---------------|---------------| | GitHub Actions | 90 days | 400 days (paid) | Download zip, extract locally | | GitLab CI | 30 days | Configurable | Download zip or browse in UI | | CircleCI | 30 days | 30 days (hard limit) | Download from artifacts tab | | Bitbucket Pipelines | 14 days | 14 days (hard limit) | Download from pipeline page | | Azure DevOps | 30 days | Configurable | Download from build summary | | Jenkins | Until disk fills up | Manual cleanup | Browse build artifacts (if server is up) | Even when artifacts exist, accessing them is friction-heavy. Most CI providers require downloading a zip file, extracting it locally, and opening it in a browser. There's no shareable URL. There's no way for someone without CI access — a QA lead, a product manager, a contractor — to view the results directly. The underlying issue is straightforward: CI providers optimize for compute, not for report hosting. Test artifacts are a side effect of the build, not a first-class feature. ## Why Teams Need Persistent Test Report Hosting Hosting test reports isn't about hoarding data. It solves specific problems that come up repeatedly in engineering teams: **Debugging regressions.** A bug resurfaces that was supposedly fixed two months ago. The original test failure had a stack trace and screenshot that would tell you exactly what happened. With CI artifacts expired, you're starting from scratch. **Flaky test analysis.** Identifying [flaky tests](/solutions/flaky-test-detection/) requires data across dozens or hundreds of runs. If your CI only retains 30 days of artifacts, you're working with a narrow window — sometimes too narrow to see the pattern. **Cross-team visibility.** QA, product, and management often need to see test results. Giving everyone CI access just to view reports is either impractical (permissions) or impossible (external stakeholders). **Audit trails.** Regulated industries need evidence that tests passed before deployment. "The CI artifact expired" doesn't satisfy an auditor. ## DIY Approaches and Their Drawbacks Most teams eventually try to solve this themselves. Each approach works up to a point, then falls apart. ### S3 or GCS Bucket The most common approach: add a CI step to upload reports to cloud storage. ```yaml - name: Upload test reports to S3 run: | aws s3 cp ./test-results s3://team-test-reports/${{ github.sha }}/ \ --recursive ``` This gives you retention control, but creates new problems: - **No browsable UI.** You need to know the exact commit SHA to find a report. There's no way to browse by branch, date, or test status. - **Access control is manual.** S3 bucket policies are notoriously difficult to get right. Too permissive and it's a security risk. Too restrictive and people can't access what they need. - **No cleanup automation.** Lifecycle rules work for simple cases, but break down when you need different retention per project or per branch. - **HTML reports don't just work.** Playwright reports reference relative assets (CSS, JS, screenshots). Serving them from S3 requires CloudFront or static hosting configuration to get Content-Type headers right. - **[Storage costs compound](/solutions/test-report-storage-costs/).** Without active management, costs grow linearly every month. A team generating 50 GB/month of test artifacts is paying real money within a quarter. ### GitHub Pages Some teams push reports to a dedicated GitHub Pages repo. ```yaml - name: Deploy report to GitHub Pages run: | git clone https://token@github.com/org/test-reports.git cp -r ./playwright-report test-reports/$(date +%Y-%m-%d)-${{ github.sha }} cd test-reports && git add . && git commit -m "Add report" && git push ``` This creates browsable HTML, but: - The repo grows unboundedly — Git wasn't designed for this - No authentication (GitHub Pages is public by default for free plans) - No search, no filtering, no analytics - Cleanup requires rewriting Git history ### Custom Server A few teams build a lightweight Express or Flask app to receive and serve reports. This gives you full control, but now you're maintaining infrastructure for a problem that isn't your core product. You need to handle uploads, storage, authentication, cleanup, monitoring, and uptime — for a tool that your team uses but nobody's job is to maintain. ### The Common Thread Every DIY approach solves the storage problem while creating operational problems. You trade "artifacts expire" for "who maintains this," "how do I find the report from Tuesday," and "why is the S3 bill so high." ## What a Purpose-Built Solution Looks Like A proper test report hosting platform handles the full lifecycle: upload, store, browse, share, analyze, and clean up. It should be framework-agnostic (not just Playwright or just JUnit), CI-agnostic (not just GitHub Actions), and require minimal setup. Specifically, it should: 1. Accept any report format — HTML reports, JUnit XML, [CTRF JSON](/docs/guides/ctrf/), raw files 2. Provide shareable URLs for every test run 3. Parse structured data (JUnit XML, CTRF) for analytics — pass rates, flaky tests, duration trends 4. Handle retention automatically with per-project controls 5. Work with any CI provider through a standard upload mechanism ## How Gaffer Handles Test Report Hosting Gaffer is built specifically for this. Upload test reports from any CI provider, any framework, and access them through shareable URLs with configurable retention. ### Upload Any Report Format Gaffer accepts HTML reports (Playwright, pytest-html, Mochawesome), structured data (JUnit XML, CTRF JSON), and raw files (screenshots, logs, traces). Upload them together or separately. **[HTML reports](/solutions/html-test-reports/)** are served directly in the browser — your team sees the full interactive report without downloading anything. For everything Playwright's HTML report contains and how to get the most out of it, the [Playwright reports guide](/blog/playwright-reports-guide/) is a useful reference. **Structured data** (JUnit XML, CTRF JSON) is parsed for analytics: pass/fail counts, test durations, failure messages. This feeds into trend tracking, flaky test detection, and pass rate history. ### Framework-Agnostic Setup Gaffer works with whatever your tests already produce. Here are two common patterns: **JUnit XML (works with nearly every test framework):** ```yaml # GitHub Actions - name: Run tests run: npx vitest --reporter=junit --outputFile=results.xml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./results.xml ``` **CTRF JSON (standardized format across frameworks):** ```yaml - name: Run tests run: npx playwright test --reporter=ctrf-json-reporter - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./ctrf/ctrf-report.json ``` **HTML reports with structured data (both human-readable and machine-parseable):** ```yaml - name: Run Playwright tests run: npx playwright test - name: Upload HTML report to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./playwright-report - name: Upload CTRF results to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./ctrf/ctrf-report.json ``` ### Non-GitHub CI Providers For [GitLab CI](/docs/guides/gitlab-ci/), [CircleCI](/docs/guides/circleci/), [Jenkins](/docs/guides/jenkins/), [Bitbucket Pipelines](/docs/guides/bitbucket-pipelines/), or [Azure DevOps](/docs/guides/azure-devops/), use the CLI upload: ```bash curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@./results.xml" \ -F 'tags={"branch":"'"$CI_BRANCH"'","commitSha":"'"$CI_COMMIT_SHA"'"}' ``` Three lines. Works anywhere you can run `curl`. ### Share with Anyone Every upload gets a URL in the Gaffer dashboard. Team members with access can browse reports by project, branch, or date. For people outside your organization — contractors, stakeholders, clients — generate a [share link](/solutions/shareable-test-report-links/) with a configurable expiration. Recipients view the report directly in their browser without needing a Gaffer account. ### Retention You Control | Plan | Retention | Storage | |------|-----------|---------| | Free | 7 days | 500 MB | | Pro ($15/mo) | 30 days | 10 GB | | Team ($49/mo) | 90 days | 50 GB | All plans include unlimited users — no per-seat pricing. Configure retention per project: keep production test reports for 90 days while feature branch reports auto-delete after 7. ### Analytics from Structured Data When you upload JUnit XML or CTRF JSON alongside your HTML reports, Gaffer parses the structured data and gives you: - **Pass rate trends** — track stability across builds - **[Flaky test detection](/solutions/flaky-test-detection/)** — identify tests that flip between pass and fail - **Duration tracking** — catch tests that are getting slower - **Failure patterns** — see which tests fail most frequently These work across [multiple test frameworks](/solutions/multiple-test-frameworks/). A project with Playwright E2E tests, Cypress integration tests, and Vitest unit tests shows unified analytics in one dashboard. For teams running Cypress, the [Cypress reports guide](/blog/cypress-reports-guide/) covers how to get structured output from Cypress into Gaffer. ## Comparing Your Options | | CI Artifacts | S3 + CloudFront | GitHub Pages | Gaffer | |--|--------------|-----------------|--------------|--------| | Retention | 14-90 days | Unlimited (manual cleanup) | Unlimited (repo grows) | 7-90 days (auto cleanup) | | Shareable URLs | No (download zip) | Yes (with setup) | Yes (public only) | Yes | | HTML report viewing | Download required | Yes (with config) | Yes | Yes | | Structured data parsing | No | No | No | Yes | | Analytics | No | No | No | Yes | | Access control | CI permissions only | S3 policies | Public or private repo | Team-based + share links | | Setup time | Built-in | Hours | Hours | Minutes | | Maintenance | None | Ongoing | Ongoing | None | ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention — enough to evaluate the workflow.
--- ## How to Host HTML Test Reports for Your Team Source: https://gaffer.sh/solutions/html-test-reports/ import ScreenshotLink from "../components/ScreenshotLink.astro"; import htmlReportExample from "../assets/screenshots/test-run-detail.png";

HTML test reports exist because developers want more than console output. Playwright gives you screenshots, traces, and video. pytest-html gives you an interactive summary. But these reports are local files — and there's no built-in way to host them where your team can access them.

## The Hosting Problem HTML reports are designed for browsers, but they're generated on CI runners or local machines. Your team can't view them without extra steps: ### Common (Bad) Solutions **Email the zip file** Zip up the report folder, email it, hope the recipient extracts it and opens `index.html`. Works, but feels like 2005. **Upload to S3 manually** Set up a bucket, configure permissions, upload after each run, manage cleanup. That's infrastructure work just to share a test report. **Rely on CI artifacts** GitHub Actions and GitLab CI store artifacts, but: - Viewing requires downloading and extracting a zip - Finding the right artifact in dozens of workflow runs is tedious - QA might not have CI access - [Artifacts expire](/solutions/ci-test-artifacts-expiring/) (30-90 days depending on the CI system) **Share your screen** Works for live debugging. Can't reference it later. Not async-friendly. ## Better: Hosted HTML Reports with Shareable Links Upload your HTML reports after every CI run. Every report gets a permanent URL that opens directly in the browser — no downloads, no extraction, no CI access required. ### What This Looks Like 1. CI runs your tests and generates the HTML report (unchanged) 2. One upload step sends the report to Gaffer 3. You get a URL like `https://app.gaffer.sh/reports/abc123` 4. Share it in Slack, GitHub PRs, Jira — anywhere 5. Anyone with access sees the full interactive report in their browser Need to share with someone outside your organization? Generate a [public share link](/solutions/shareable-test-report-links/) with an expiration you control — recipients view the HTML report directly in their browser without needing an account. ## Setting Up HTML Report Hosting with Gaffer ### Step 1: Generate Your Report (Unchanged) Use whatever reporter your framework provides: ```bash # Playwright npx playwright test # pytest pytest --html=report.html --self-contained-html # Cypress (with mochawesome) npx cypress run --reporter mochawesome ``` ### Step 2: Upload in CI **GitHub Actions:** ```yaml - name: Run tests run: npx playwright test - name: Upload report to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./playwright-report ``` ### Step 3: Share the Link Share the URL in Slack, add it to a GitHub PR comment, attach it to a Jira ticket. Recipients see the full interactive report in their browser. ## HTML Reports + Structured Data HTML reports are great for humans, but you also want structured data for analytics — pass/fail trends, duration tracking, flaky test detection. Most frameworks can output both: ```typescript // playwright.config.ts export default defineConfig({ reporter: [ ['html', { open: 'never' }], // For humans ['json', { outputFile: 'results.json' }] // For analytics ], }); ``` ```bash # pytest pytest --html=report.html --junitxml=results.xml ``` Upload both formats. Gaffer serves the HTML for viewing and parses the structured data for trend analytics. ## Slack and Webhook Notifications Get notified when tests finish instead of checking CI manually: - Pass/fail summary posted to your Slack channel - Direct link to the hosted HTML report - Filter by branch — only notify on `main`, skip feature branches - Webhooks for custom integrations ## Comparing Your Options | Solution | HTML Viewing | Shareable Link | Automated | Analytics | |----------|--------------|----------------|-----------|-----------| | Local `open report.html` | Native | No | No | No | | Email zip file | After extraction | Awkward | Manual | No | | CI artifacts | Download required | With CI access | Built-in | No | | S3 + CloudFront | Native | Yes | DIY | No | | Gaffer | Native | Yes | Yes | Yes | ## Get Started This page covers HTML output specifically. The same workflow [hosts test reports](/solutions/host-test-reports/) in any format Gaffer parses. Gaffer's free tier includes 500 MB of storage with 7-day retention. Paid plans offer up to 90-day retention.
--- ## The True Cost of Ignoring Test Failures Source: https://gaffer.sh/solutions/ignoring-test-failures/

It starts small. A test fails, but it's probably flaky. Someone re-runs the pipeline. It passes. Everyone moves on. Over weeks and months, this pattern repeats until the team has unconsciously learned to ignore test failures. The test suite becomes decoration - green checkmarks that nobody trusts.

## How Teams Learn to Ignore Failures Test failure fatigue doesn't happen overnight. It's a gradual erosion: ### Stage 1: Friction Creates Delay Test results are buried in CI logs. Accessing them requires: - Navigating to the CI provider - Finding the right workflow run - Downloading artifact zip files - Extracting and opening locally When checking a failure takes 5 minutes, developers wait. "I'll look at it after this meeting." The failure sits. ### Stage 2: Flaky Tests Breed Skepticism Some tests fail randomly. Developers learn which ones. When they see a failure, the first thought is "is this the flaky one?" not "did I break something?" Re-running the pipeline becomes the default response. ### Stage 3: Normalization The team develops shared knowledge: "Oh, that test always fails on Mondays" or "just re-run it, it'll pass." Failures become background noise. New team members pick up these habits. The institutional knowledge is "tests fail sometimes, don't worry about it." ### Stage 4: Real Bugs Slip Through A genuine failure appears. It looks like all the other noise. Someone re-runs the pipeline. It fails again, but they're in a hurry. "Probably infrastructure, I'll check later." The bug ships to production. ## The Compounding Cost Ignoring test failures creates a vicious cycle: ### Trust Erosion When tests cry wolf too often, the team stops listening. The suite's primary value - confidence that the code works - disappears. You're paying the cost of running tests without getting the benefit. ### Technical Debt Accumulation Tests that "always fail" get skipped or deleted rather than fixed. Test coverage shrinks. The gaps compound as new code builds on untested foundations. ### Slower Debugging Without reliable test history, debugging production issues becomes harder: - "Did we have a test for this?" - "When did this behavior change?" - "Was this ever working?" These questions have no answers when test data is ignored or expired. ### Release Anxiety Teams add manual verification steps because they don't trust automated tests. QA cycles extend. Releases slow down. The promise of continuous delivery breaks. ## Breaking the Cycle The solution isn't more discipline or stricter processes. It's reducing friction and surfacing signal. ### 1. Make Results Instantly Accessible If checking a failure takes 5 minutes, it won't happen consistently. If it takes 10 seconds, it becomes habit. **What helps:** - Shareable links directly to the failure (no downloads) - Notifications in Slack where the team already works - Mobile-friendly reports for quick checks **What doesn't help:** - Telling developers to "check CI more often" - Adding Slack bots that just say "build failed" without details - Documentation on "how to download artifacts" ### 2. Separate Signal from Noise Flaky tests poison the well. Quarantine them ruthlessly: - Track flip rates to identify flaky tests automatically - Move flaky tests to a separate suite that doesn't block merges - Fix or delete - don't let them linger When failures are rare and meaningful, developers pay attention. ### 3. Make Trends Visible A single failure is easy to dismiss. A trend is harder to ignore. - Pass rate dropping from 95% to 80% over two weeks - Three new flaky tests introduced this sprint - Test duration increasing 20% month over month Dashboards and reports make patterns visible to the whole team, not just whoever happened to see the last failure. ### 4. Keep Historical Data "When did this start failing?" is only answerable with history. CI artifacts expire. If your test data disappears after 30-90 days, you lose the ability to: - Correlate failures with code changes - Track improvement over time - Debug recurring issues Persistent storage turns test runs into organizational knowledge. ### 5. Involve the Whole Team Test health isn't just a developer concern. When PMs, QA, and stakeholders can access results: - Failures get visibility beyond the person who triggered them - "Test health" becomes a team metric, not a CI detail - Accountability distributes naturally If only developers can access test results, only developers will care about them. ## How Gaffer Helps Gaffer is built around reducing friction and surfacing signal: **Instant Access** - Shareable URLs for every test run - No downloads needed - Share links in Slack, Jira, PRs **Slack Notifications** - Test results delivered where your team works - Direct links to full reports - Filter by branch to reduce noise **Flaky Test Detection** - Automatic flip rate tracking - Dashboard showing most problematic tests - Catch new flakiness before it spreads **Analytics and Trends** - Pass rate over time - Health scores per project - Historical data with configurable retention **Team Access** - Organization-based permissions - No CI login required - Works for PMs, QA, anyone who needs visibility ## The Alternative: A Healthy Test Culture Teams that maintain trust in their test suites share common traits: - Failures are rare enough to be meaningful - When failures happen, investigation is immediate - Flaky tests are quarantined quickly - Test health metrics are visible and discussed - Everyone - not just developers - can access results This doesn't require heroic effort. It requires removing friction and making signal visible. ## Get Started Stop ignoring test failures. Gaffer makes results accessible, surfaces flaky tests, and tracks trends so your team can trust the suite again.
--- ## How to Share Jest Test Results with Your Team Source: https://gaffer.sh/solutions/jest-test-reporting/ import { Image } from "astro:assets";

Jest runs your tests and prints results to the console. CI captures the output, but sharing it with your team means "go check the workflow run" or screenshotting terminal output into Slack. There's no built-in way to share Jest results as a link.

## The Sharing Problem Jest outputs to stdout by default. You can add reporters for JSON or HTML, but those files are stuck in CI artifacts: ### Common (Bad) Solutions **1. "Check the CI logs"** - Requires repo access and navigating through workflow runs - QA or PMs might not have CI access - Logs disappear when retention expires **2. CI artifacts** - Download a zip, extract, find the right file - GitHub Actions keeps artifacts 90 days max - Finding the right artifact in dozens of workflow runs is tedious **3. Screenshot the terminal** - Loses all structure and detail - Can't reference later - Looks terrible in Slack **4. Paste JSON output in a thread** - Nobody reads raw JSON - Truncated for large test suites - No filtering or navigation ## Better: Hosted Jest Reports with Shareable Links Upload Jest results after every CI run. Every test run gets a permanent URL that anyone on your team can open. ### What This Looks Like 1. CI runs your Jest tests 2. Results upload automatically 3. You get a URL like `https://app.gaffer.sh/reports/abc123` 4. Share in Slack, GitHub PRs, Jira — anywhere 5. Anyone with access sees the full report No downloads. No CI access required. No expiring artifacts. ## Setting Up Jest Report Sharing with Gaffer ### Step 1: Configure Jest Output CTRF gives the richest analytics data: ```bash npm install jest-ctrf-json-reporter --save-dev ``` ```javascript // jest.config.js module.exports = { reporters: [ 'default', ['jest-ctrf-json-reporter', { outputFile: 'ctrf-report.json' }] ] }; ``` Gaffer also accepts Jest's built-in JSON output and jest-html-reporter if you prefer those. ### Step 2: Add the Upload Step to CI **GitHub Actions:** ```yaml - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./ctrf-report.json commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` **GitLab CI:** ```yaml test: script: - npm ci - npm test after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ### Step 3: Share the Link After upload, share the URL in Slack, GitHub PR comments, or Jira tickets. Recipients see the full report in their browser. For people outside your organization, you can generate a [public share link](/solutions/shareable-test-report-links/) with configurable expiration. They view the results without needing a Gaffer account. ## Slack and Webhook Notifications Stop checking CI manually. Get notified when Jest tests finish: **Slack integration:** ``` [FAILED] my-project - main 3 tests failed, 142 passed View report: https://app.gaffer.sh/reports/abc123 ``` - Direct link to the full report - Filter by branch — only notify on `main`, skip feature branches **Webhooks:** - Send results to any endpoint - Integrate with your existing alerting or workflow tools - Trigger custom actions on failure No more "did the tests pass?" messages in Slack. See [Test Failure Notifications](/solutions/test-failure-notifications/) for the full setup guide. ## GitHub Integration Commit statuses show test results directly on PRs. No clicking through to CI logs to find out if tests passed. ## Monorepo Support Running Jest across multiple packages? Upload results from each: ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: | ./packages/api/ctrf-report.json ./packages/web/ctrf-report.json ./packages/shared/ctrf-report.json ``` All results appear in the same project dashboard. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Paid plans offer extended retention up to 90 days.
--- ## Test Report Sprawl: Managing Results Across Multiple Frameworks Source: https://gaffer.sh/solutions/multiple-test-frameworks/ import ScreenshotLink from "../components/ScreenshotLink.astro"; import activityFeed from "../assets/screenshots/org-activity-feed.png";

Modern applications don't use a single test framework. You're running Playwright for E2E, Jest for unit tests, maybe Vitest for the newer modules. Each framework generates reports in different formats, stored in different places. Finding "what failed" becomes an archaeology expedition.

## The Multi-Framework Reality A typical project might have: | Layer | Framework | Report Format | |-------|-----------|---------------| | E2E | Playwright | HTML + trace files | | Integration | Cypress | JSON + screenshots | | Unit (React) | Jest | JSON or HTML | | Unit (Vite) | Vitest | JSON or HTML | | API | pytest | JUnit XML or HTML | Each framework has its own: - Report format and structure - CI artifact location - Viewing tool or command - Retention period ## The Problems This Creates ### 1. Context Switching To understand "did tests pass?" you need to: 1. Open GitHub Actions 2. Find the workflow run 3. Download Playwright artifacts 4. Extract and open locally 5. Go back to GitHub 6. Download Jest artifacts 7. Extract and open those too 8. Repeat for any other frameworks This process takes 5-10 minutes and breaks your flow every time. ### 2. No Unified View Each framework's report only knows about its own tests. There's no single place to see: - Overall pass rate across all test types - Which layer is most problematic - Trends over time for the full suite ### 3. Inconsistent Retention CI artifacts expire at different rates. Your Playwright traces from last month are gone, but Jest JSON files from the same run might still exist. Historical analysis becomes impossible. ### 4. Team Communication Overhead When someone asks "what's the test status?", you can't point them to one place. You end up copying and pasting results from multiple sources, or scheduling a screen share to walk through different reports. ## The Solution: Unified Test Report Hosting Instead of fighting multiple formats, upload everything to one platform that normalizes the data: 1. **Single upload step** - All reports go to the same place 2. **Automatic format detection** - The platform parses each format correctly 3. **Unified dashboard** - See all results in one view 4. **Consistent retention** - Same history for all frameworks 5. **One link to share** - Point teammates to a single URL ## How Gaffer Handles Multiple Frameworks Gaffer automatically detects and parses reports from all major test frameworks. ### Supported Formats | Framework | Format | What's Extracted | |-----------|--------|------------------| | Playwright | HTML | Full report with traces, screenshots, videos | | Jest | JSON | Test cases, durations, failure messages | | Jest | HTML (jest-html-reporter) | Visual report with embedded results | | Vitest | JSON | Test cases, durations, failure messages | | Vitest | HTML | Full report with test details | | pytest | HTML (pytest-html) | Test cases, logs, captured output | | Any | JUnit XML | Universal format from most frameworks | | Any | CTRF JSON | Common Test Report Format (15+ frameworks) | ### Automatic Detection You don't need to specify the format. Upload your reports and Gaffer figures out what they are. For teams tracking [Vitest coverage reports](/blog/vitest-coverage-reports/) alongside test results, Gaffer keeps each package's coverage history and the combined picture in the same dashboard. For teams running Cypress alongside Playwright or Jest, see the [Cypress reporters setup guide](/blog/cypress-reports-guide/) for how to configure Mochawesome and JUnit export before uploading. ```yaml # GitHub Actions example - upload everything - name: Upload all test reports uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: | ./playwright-report ./coverage/jest-report.json ./test-results/vitest.json ``` ### Normalized Analytics Regardless of source format, Gaffer extracts: - Total tests, passed, failed, skipped - Individual test names and durations - Failure messages and stack traces - Pass rate trends over time This lets you compare apples to apples across frameworks. ### Per-Framework Filtering The dashboard shows all results together, but you can filter by framework to drill into specific layers: - "Show me only E2E failures" - "What's the trend for unit tests?" - "Which framework has the most flaky tests?" ## Example: Multi-Framework CI Setup Here's a complete GitHub Actions workflow uploading results from multiple frameworks: ```yaml name: Tests on: [push, pull_request] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - name: Run Jest tests run: npm run test:unit -- --json --outputFile=jest-results.json - name: Run Vitest tests run: npm run test:vitest -- --reporter=json --outputFile=vitest-results.json - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: | ./jest-results.json ./vitest-results.json e2e-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./playwright-report ``` Both jobs upload to the same Gaffer project, giving you a unified view of all test results for each commit. ## Using CTRF for Universal Coverage If your framework isn't directly supported, [CTRF (Common Test Report Format)](https://ctrf.io) provides a universal JSON standard with reporters for 15+ frameworks: - Mocha, Jasmine, Cucumber - Go test, PHPUnit, RSpec - .NET, Java (JUnit, TestNG) - And more Install a CTRF reporter, generate the JSON, upload to Gaffer. Done. ```bash # Example: Mocha with CTRF npm install mocha-ctrf-json-reporter mocha --reporter mocha-ctrf-json-reporter ``` ## Benefits of Consolidation Once all your reports are in one place: | Before | After | |--------|-------| | 5+ minutes to check all results | 10 seconds, one dashboard | | Different retention per framework | Consistent history | | Can't compare across frameworks | Unified analytics | | Multiple links to share | One URL for everything | | Framework-specific tooling | One platform to learn | ## Get Started Stop juggling multiple report formats. Gaffer normalizes everything into one dashboard with unified analytics across all your test frameworks.
--- ## How to Share pytest Results with Your Team Source: https://gaffer.sh/solutions/pytest-test-reporting/ import htmlReportExample from "../assets/screenshots/html-report-example.png"; import { Image } from "astro:assets";

pytest runs your tests. pytest-html gives you a nice report. Then someone asks "what failed in CI?" and you're walking them through downloading artifacts from GitHub Actions. There's no built-in way to share pytest results with your team.

## The Sharing Problem pytest outputs results to the terminal or generates files (HTML, JUnit XML). Either way, those results are stuck wherever the tests ran — your machine or a CI runner. Getting them to your team means: ### Common (Bad) Solutions **1. CI artifacts** - GitHub Actions keeps artifacts 90 days max, GitLab even shorter by default - "Go to the workflow run, click artifacts, download, unzip, open in browser" - QA might not have CI access at all **2. Paste terminal output in Slack** - Loses formatting, hard to parse - Can't reference later - Doesn't scale past a handful of tests **3. Email the HTML report** - Recipient downloads, opens locally - Version confusion across multiple runs - Feels like 2005 **4. Upload to S3 manually** - Works, but it's a manual step that gets skipped - Someone has to manage the bucket and permissions ## Better: Hosted pytest Reports with Shareable Links Upload pytest results after every CI run. Every test run gets a permanent URL your team can open directly.
Hosted pytest test report in the Gaffer dashboard
### What This Looks Like 1. CI runs pytest 2. Results upload automatically 3. You get a URL like `https://app.gaffer.sh/reports/abc123` 4. Share in Slack, GitHub PRs, Jira — anywhere 5. Anyone with access sees the full report No downloads. No manual steps. No expiring artifacts. ## Setting Up pytest Report Sharing with Gaffer ### Step 1: Generate Reports Pick your output format — or use multiple: ```bash # HTML for humans pip install pytest-html pytest --html=report.html --self-contained-html # CTRF for richest analytics pip install pytest-ctrf pytest --ctrf=ctrf-report.json # Or both pytest --html=report.html --self-contained-html --ctrf=ctrf-report.json ``` ### Step 2: Add the Upload Step to CI **GitHub Actions:** ```yaml - name: Run tests run: pytest --html=report.html --self-contained-html --ctrf=ctrf-report.json - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v1 with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: | ./report.html ./ctrf-report.json commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` **GitLab CI:** ```yaml test: image: python:3.11 script: - pip install -r requirements.txt - pip install pytest pytest-html pytest-ctrf - pytest --html=report.html --self-contained-html --ctrf=ctrf-report.json after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@report.html" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ### Step 3: Share the Link After upload, share the URL in Slack, add it to a GitHub PR comment, or attach it to a Jira ticket. Recipients see the full report in their browser — no downloads. For people outside your organization, you can generate a [public share link](/solutions/shareable-test-report-links/) with configurable expiration. They view the results without needing a Gaffer account. ## Slack and Webhook Notifications Stop checking CI manually. Get notified when pytest finishes: **Slack integration:** ``` [FAILED] api-tests - main 2 tests failed, 89 passed View: https://app.gaffer.sh/reports/xyz789 ``` - Direct link to the full report - Filter by branch — only notify on `main`, skip feature branches **Webhooks:** - Send results to any endpoint - Integrate with your existing alerting tools - Trigger custom actions on failure No more "did the tests pass?" back-and-forth. ## GitHub Integration Commit statuses show test results directly on PRs. Your team sees pass/fail without clicking through to CI logs. ## One Dashboard for Every Suite Running JavaScript or Go suites alongside Python? Gaffer combines [multiple test frameworks](/solutions/multiple-test-frameworks/) in one dashboard, so every suite reports to the same place regardless of language. ## What About Flaky Tests? Python tests can be flaky for many reasons — database state, network calls, timing issues, fixture leaks. Gaffer tracks test results across runs and identifies tests that flip between pass and fail over time. You get a list of unreliable tests to fix or quarantine, instead of investigating the same "random failure" repeatedly. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Paid plans offer extended retention up to 90 days.
--- ## ReportPortal Alternative: Test Analytics Without Enterprise Complexity Source: https://gaffer.sh/solutions/reportportal-alternative/ import { Image } from "astro:assets";

You need test analytics, so you evaluate ReportPortal. Then you see the requirements: Elasticsearch, PostgreSQL, RabbitMQ, MinIO — five services just to view test results. For most teams, the infrastructure cost of running ReportPortal dwarfs the value of the analytics it provides.

## The ReportPortal Trade-offs ReportPortal offers a lot of features: real-time reporting, ML-based failure analysis, custom dashboards, and extensive integrations. But those features come with costs: ### Infrastructure Requirements A typical ReportPortal deployment needs: - PostgreSQL database - Elasticsearch (or OpenSearch) - RabbitMQ message queue - MinIO or S3 for binary storage - The ReportPortal services themselves That's 5+ services to deploy, monitor, and maintain. The official docs recommend 16GB+ RAM for a production setup. ### Operational Overhead Running ReportPortal means: - Managing Elasticsearch indices and storage - Monitoring RabbitMQ queues - Database backups and maintenance - Keeping all services updated - Handling scaling as test volume grows This requires dedicated DevOps attention, which smaller teams often can't spare. ### Learning Curve ReportPortal's feature set is extensive. Dashboards, widgets, filters, defect types, analysis rules - there's a lot to learn. Teams often end up using 20% of the features after spending significant time on setup. ## Gaffer: Test Reporting Without the Overhead Gaffer focuses on the core problem: getting test results out of CI and into a place where your team can actually use them. ### Zero Infrastructure Gaffer is a hosted service. No databases, no message queues, no Elasticsearch. Sign up, get an API key, upload results. ### Simple Setup Add one step to CI: ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v1 with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./playwright-report ``` That's it. Reports are now hosted and shareable. ### Focus on What Matters Gaffer provides: - **Test report hosting** - Shareable URLs for every test run - **Flaky test detection** - Automatic identification of unreliable tests - **Pass rate trends** - See test health over time - **Slow test tracking** - Find tests dragging down your CI - **Slack notifications** - Get alerts when tests fail - **GitHub integration** - Commit statuses and PR comments No ML-powered failure classification, no complex dashboard builders. Just the essentials, done well. ## Feature Comparison | Feature | ReportPortal | Gaffer | |---------|--------------|--------| | Test report hosting | Yes | Yes | | Historical trends | Yes | Yes | | Flaky test detection | Yes | Yes | | ML failure analysis | Yes | No | | Custom dashboards | Yes | No | | Self-hosting required | Yes | No | | Infrastructure needs | High | None | | Setup time | Hours/Days | Minutes | | Pricing | Self-host or SaaS | SaaS | ## Framework Support Both platforms support major frameworks, though integration methods differ: | Framework | ReportPortal | Gaffer | |-----------|--------------|--------| | Playwright | Agent | Native HTML + CTRF | | Jest | Agent | JSON + CTRF | | Vitest | Agent | HTML + CTRF | | pytest | Agent | pytest-html + CTRF | | JUnit | Agent | JUnit XML | ReportPortal uses "agents" that report results in real-time. Gaffer parses standard report formats after test runs complete - no custom agents needed. ## When to Use ReportPortal ReportPortal might be a better fit if you: - Have dedicated DevOps capacity for infrastructure - Need ML-powered failure analysis - Want highly customizable dashboards - Run thousands of tests and need real-time reporting - Have enterprise compliance requirements for self-hosting ## When to Use Gaffer Gaffer is probably a better fit if you: - Want test reporting without infrastructure overhead - Have a small-to-medium team - Need quick setup that just works - Value simplicity over feature count - Want to avoid the ops burden of running 5+ services ## Migration Path If you're currently using ReportPortal and finding the maintenance burden too high: 1. Add Gaffer upload to your CI pipeline (alongside ReportPortal initially) 2. Use your existing report formats - no agent changes needed 3. Evaluate for a few weeks 4. Remove ReportPortal agents if Gaffer meets your needs 5. Decommission ReportPortal infrastructure The upload-based approach means you're not locked in - you can switch back or run both if needed. ## Other Comparisons Evaluating multiple tools? See how Gaffer compares to [Allure](/solutions/allure-alternative/) and [TestDino](/solutions/testdino-vs-gaffer/). ## Try It Gaffer's free tier gives you 500 MB storage with 7-day retention. Enough to see if the simpler approach works for your team.
--- ## How to Share Playwright Test Reports with Your Team Source: https://gaffer.sh/solutions/share-playwright-test-reports/ import playwrightReportExample from "../assets/screenshots/playwright-report-example.png"; import BrowserChrome from "../components/BrowserChrome.astro"; import PullCta from "../components/PullCta.astro";

Playwright generates HTML test reports, but sharing them with your team is surprisingly difficult. `npx playwright show-report` works on your laptop; sending the results to QA, your PM, or a client on a Slack thread is a different problem. Here's how to share Playwright test reports properly.

## The Playwright Report Sharing Problem The local server from `npx playwright show-report` is only accessible on your machine. How do you share the report when: - **QA needs to see what failed** before approving a release - **A PM wants to check test coverage** for a feature - **You're debugging with a colleague** and need to show them the exact failure - **You need to reference an old test run** from weeks ago ### Common (Bad) Solutions **1. Email the zip file** - Clunky, fills up inboxes, version confusion - Recipient has to download, extract, open in browser **2. Upload to S3/GCS manually** - Works, but requires manual steps every time - Who manages the bucket? What about access control? **3. Rely on CI artifacts** - [GitHub Actions artifacts expire](/solutions/ci-test-artifacts-expiring/) after 90 days (configurable) - GitLab artifacts expire too - Finding the right artifact in a sea of workflow runs is tedious **4. Share your screen** - Not async-friendly - Can't reference later ## Better Solution: Hosted Playwright Reports The real solution is to host your Playwright reports on a dedicated platform. Every test run gets a unique URL that your team can access through the dashboard. ### What Good Looks Like 1. CI runs your Playwright tests 2. Reports automatically upload to a hosting service 3. You get a shareable URL like `https://app.gaffer.sh/reports/abc123` 4. Share the link in Slack, Jira, GitHub PR, wherever 5. Anyone with access can view the full interactive report No downloads. No manual steps. Configurable retention periods. ### Sharing Beyond Your Team Need to share a report with someone who doesn't have a Gaffer account — a contractor, a stakeholder, or another team? [Share links](/solutions/shareable-test-report-links/) let you generate a public URL for any test run. Set an expiration (1 hour to 30 days, or never), and revoke the link when you're done. Recipients see the full interactive Playwright report in their browser without needing to log in. ## Setting Up Playwright Report Sharing with Gaffer Gaffer hosts your Playwright reports with a single step in your CI pipeline: run your tests, then upload `./playwright-report` with the Gaffer uploader action (GitHub Actions) or a `curl` POST (GitLab, CircleCI, anywhere else). Full setup for each CI provider is in the [CI guides](/docs/guides/). A minimal GitHub Actions step: ```yaml - name: Upload report to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./playwright-report ``` After upload, Gaffer returns a URL to the hosted report. Paste it in Slack, PR comments, or Jira tickets, wherever your team already talks about releases. ## Bonus: Slack Notifications Tired of checking CI manually? Gaffer can [send test results directly to Slack](/solutions/test-failure-notifications/): - Pass/fail summary at a glance - Direct link to the full report - Filter by branch (only notify on `main`, not every feature branch) No more "did the tests pass?" messages in Slack. ## Comparing Playwright Report Sharing Options
MethodRetentionShareable linkAutomatedTeam access
Local show-reportNone
Email zip filesManual
CI artifacts30–90 days✓ (if CI access)
S3 manual uploadConfigurableDepends
GafferUp to 90 days✓ (public share links)
## Beyond Sharing: Playwright Analytics Once you're hosting reports, you unlock analytics: - **Pass rate trends** - Is your test suite getting more stable or less? - **Flaky test detection** - Which tests fail intermittently? - **Duration tracking** - Are tests getting slower? - **Failure patterns** - See which tests fail most often For a deeper look at Playwright's report formats, trace files, and what each artifact tells you, see the [Playwright reports guide](/blog/playwright-reports-guide/). ## Who Uses Hosted Playwright Reports QA teams, consultancies, and product teams use Gaffer to send hosted Playwright reports to stakeholders who don't have CI access. Free tier includes 500 MB of storage with 7-day retention; paid plans extend retention up to 90 days. Running Cypress alongside Playwright? The same setup works for Cypress. See [Cypress test reporting](/solutions/cypress-test-reporting/) for the configuration, or the [Cypress reports guide](/blog/cypress-reports-guide/) for a walkthrough of Cypress's report formats.
--- ## Share test reports with anyone. No login required. Source: https://gaffer.sh/solutions/shareable-test-report-links/ import { Image } from "astro:assets";

Your test results live behind a login. That works for your team, but not for the contractor debugging a regression, the PM checking release status, or the partner team blaming your API. Share links give you a read-only public URL for any test run. Recipients open the full interactive HTML report in their browser without a Gaffer account.

## The Problem: Test Results Are Locked Behind Logins Test reports sit in your CI dashboard or your Gaffer organization. When someone outside your team needs to see them, you're stuck with bad options: - **Screenshots in Slack** — Loses detail, can't drill into failures, no interactivity - **Screen sharing** — Requires both people online at the same time - **Exporting to PDF** — Static, loses the interactive HTML report - **Adding them to your org** — Overkill for a contractor who needs one report You shouldn't need to add someone to your organization just to show them a test result. ## Share Links: Public URLs with Controls From any test run in Gaffer, open the share modal and get a public URL. The link gives read-only access to the test run summary and the HTML report — no login required. ### How It Works 1. Open a test run and click **Share** 2. Gaffer generates a unique URL for that test run 3. Send the link to anyone — they see the results in their browser 4. Set an expiration (1 hour, 24 hours, 7 days, 30 days, or never) 5. Revoke the link at any time Each test run gets one share link. Creating a link is idempotent — opening the share modal again returns the same URL. ### What Recipients See The share page shows the same data your team sees on the test run detail page: - **Test counts** — Passed, failed, and skipped - **Branch and commit info** - **Interactive HTML report** — The full Playwright, Jest, or other framework report, viewable in the browser Recipients who don't have a Gaffer account see a sign-up prompt. Members of your organization see a link back to the dashboard. Authenticated users from other organizations see an option to request access. ## Use Cases ### Sharing with Contractors A freelance QA engineer is testing your app. They don't need full dashboard access — they need to see what failed in the latest run. Send them a share link with a 7-day expiration. ### Stakeholder Updates Your PM asks "did the regression suite pass before release?" Instead of a screenshot or a verbal "yes," send them the link. They see the actual results. ### Cross-Team Debugging Another team's API change broke your integration tests. Share the link in their Slack channel. They can see exactly which tests failed and what the errors were. ### Client Reporting If you're an agency running tests for a client, share links let you send test results without provisioning accounts. Set the link to expire after 30 days so it doesn't linger. ## Expiration and Revocation Share links aren't permanent by default — you control how long they're active: | Setting | Duration | |---------|----------| | 1 hour | Quick review, then auto-expires | | 24 hours | End-of-day sharing | | 7 days | Sprint-length access | | 30 days | Monthly reporting | | Never | Link stays active until manually revoked | You can update the expiration after creating the link, or revoke it entirely. Revoked links return a "not found" page. ## How It Fits with the Rest of Gaffer Share links complement the existing workflow: - **CI uploads test results** to Gaffer automatically - **Your team** views results in the dashboard and gets [Slack notifications](/solutions/test-failure-notifications/) - **External collaborators** get share links when they need access to specific runs - **[HTML reports](/solutions/html-test-reports/)** are viewable through share links — no downloads required Share links are read-only. Recipients can view results but can't modify anything in your organization. ## Get Started Share links are available on all Gaffer plans. Open any test run, click Share, and send the link.
--- ## Centralize Test Artifacts: Reports, Logs, Screenshots Source: https://gaffer.sh/solutions/test-artifact-management/ import htmlReportExample from "../assets/screenshots/html-report-example.png"; import { Image } from "astro:assets";

Test artifacts — reports, screenshots, logs, coverage data — are the outputs generated every time your tests run. They're essential for debugging failures and tracking test health over time. For a deep dive into each type, see [What Are Test Artifacts in Software Testing?](/blog/what-are-test-artifacts/). But once you have more than a handful of test runs, managing these artifacts becomes its own problem.

Your Playwright report is in GitHub Actions. The screenshot is in a Slack thread from last Tuesday. The coverage data is in an S3 bucket that two people have access to. When a test fails, the first ten minutes aren't spent debugging — they're spent finding the evidence. Test artifacts are scattered across every tool your team uses, and nobody has the full picture.

Playwright HTML Test Report Example
## What Are Software Testing Artifacts? Test artifacts in software testing include any file or data produced during test execution: - **Test Reports** - HTML, JSON, or XML summaries of test results (Playwright, Jest, Vitest, pytest, JUnit) - **Screenshots & Videos** - Visual evidence of test failures, especially for E2E tests - **Log Files** - Console output, stack traces, and debugging information - **Coverage Reports** - Code coverage data showing which lines were tested - **Performance Metrics** - Timing data, memory usage, and resource consumption These artifacts are crucial for debugging, but they're often difficult to access, share, and retain. For a deeper look at each type, see [What Are Test Artifacts in Software Testing?](/blog/what-are-test-artifacts/). ## The CI Artifact Expiration Problem Most CI/CD platforms (GitHub Actions, GitLab CI, CircleCI) automatically delete test artifacts after a retention period - typically 30-90 days. This creates real problems: - **Debugging old failures** - When a bug resurfaces, the original test artifacts are gone - **Compliance audits** - Regulated industries need to retain test evidence for years, not months - **Onboarding engineers** - New team members can't see historical context for test behavior - **[Flaky test analysis](/solutions/flaky-test-detection/)** - Understanding intermittent failures requires data across many runs Even worse, accessing artifacts *while they exist* is painful. You have to navigate to the CI provider, find the right workflow run, download a zip file, and extract it locally. ## How Gaffer Solves Test Artifact Management Gaffer provides searchable storage for your test artifacts with configurable retention and instant team access: ### Automatic Upload from CI Add one step to your CI pipeline and Gaffer captures your test artifacts automatically: ```yaml - name: Upload to Gaffer uses: gaffer-sh/gaffer-uploader@v2 with: gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results ``` ### Instant Team Sharing Every test run gets a URL in the Gaffer dashboard. Share it in Slack, add it to a bug ticket, or bookmark it for later. No downloads, no zip files. For external collaborators, you can generate [public share links](/solutions/shareable-test-report-links/) with configurable expiration — they view the results in their browser without needing a Gaffer account. ### Slack Notifications Get notified in Slack when tests fail, with a direct link to the full report. Filter notifications by branch so you only hear about failures that matter. ### Historical Analytics Track test health over time with pass rate trends, [flaky test detection](/solutions/flaky-test-detection/), and [historical analytics](/solutions/test-results-dashboard/). ## Supported Test Frameworks Gaffer works with the test artifacts you already generate: | Framework | Report Types | |-----------|--------------| | [Playwright](/solutions/share-playwright-test-reports/) | HTML reports, traces, screenshots | | Jest | JSON output, jest-html-reporter | | Vitest | HTML reports, JSON output | | pytest | pytest-html, JUnit XML | | Any framework | JUnit XML, [CTRF JSON](/docs/guides/ctrf/) | ## Test Artifacts Best Practices 1. **Generate structured reports** - HTML or JSON reports are more useful than plain text logs 2. **Include screenshots on failure** - Visual evidence makes debugging faster 3. **Store artifacts with longer retention** - Don't rely on short CI retention policies 4. **Make artifacts accessible** - If your team can't find artifacts easily, they won't use them 5. **Track trends over time** - Individual test runs matter less than patterns across runs ## Get Started Gaffer's free tier includes 500 MB of artifact storage with 7-day retention. Paid plans offer extended retention (up to 90 days) and up to 50 GB of storage.
--- ## Test Failure Notifications: Get Alerted in Slack, Not CI Logs Source: https://gaffer.sh/solutions/test-failure-notifications/

Your CI pipeline runs tests on every push. When tests fail, how does your team find out? If the answer is "someone checks the GitHub Actions tab," you have a notification problem. Failures sit unnoticed for minutes or hours while developers context-switch to other work.

## The Problem: Nobody Checks CI Proactively Here's what typically happens when tests fail in CI: 1. Developer pushes code and moves on to the next task 2. CI runs tests in the background 3. Tests fail — but nobody's watching 4. Eventually someone notices the red X on the PR (or doesn't) 5. By the time they investigate, they've lost the mental context The feedback loop is broken. CI runs tests, but the results sit in a dashboard nobody monitors in real-time. ### What Teams Do Instead **1. Check CI manually** - Requires remembering to check after every push - Developers context-switch away while waiting for CI - Easy to miss failures on shared branches like `main` **2. GitHub email notifications** - Buried in inbox noise alongside PR reviews, mentions, and issue updates - No summary of *which* tests failed or why - No link to the full report **3. Custom scripts with `curl` and Slack webhooks** - Brittle, hard to maintain, lives in CI config - Usually just sends "build failed" with no detail - No filtering by branch or failure severity - Every team reinvents this from scratch **4. CI provider's built-in notifications** - GitHub Actions has no native Slack integration for test results - GitLab notifications are noisy and lack test-level detail - No structured test result data — just "job passed/failed" ## Better: Structured Test Notifications What you actually want is a notification that tells you: - **Which tests failed** — not just "the build broke" - **A link to the full report** — one click to see errors, traces, screenshots - **Branch and commit context** — what triggered the failure - **Filtering** — only notify on branches you care about ## How Gaffer Handles Test Notifications Gaffer sends test results to Slack, webhooks, and GitHub commit status after every upload. No custom scripts, no maintenance. ### Slack Integration Connect your Slack workspace in project settings. After each test run upload, Gaffer posts a summary: - Pass/fail counts and overall status - Direct link to the hosted test report - Branch, commit SHA, and trigger context - Failed test names for quick triage ### Notification Triggers Choose when to get notified: - **All runs** — every test upload sends a notification - **Failures only** — only hear about problems - **Consecutive failures** — alert when a test fails multiple runs in a row, filtering out one-off noise ### Branch Filtering Don't get spammed by feature branch noise. Configure notifications per branch pattern: - Only notify on `main` and `release/*` - Skip `dependabot/*` and draft PRs - Different channels for different branches ### Webhooks For custom integrations, Gaffer sends HMAC-SHA256 signed HTTP POST payloads to any endpoint. Payloads include test run results, project context, and report links. See the [webhook docs](/docs/integrations/webhooks/) for the full payload schema. Use webhooks to: - Post to Microsoft Teams, Discord, or any chat tool - Trigger PagerDuty alerts for critical test suites - Update a status page or internal dashboard - Feed into your existing alerting pipeline ### GitHub Commit Status Gaffer sets commit status checks on your PRs automatically. Developers see test results without leaving GitHub — green check or red X directly on the commit. ## Setting Up Test Failure Notifications ### Step 1: Upload Test Results from CI ```yaml # GitHub Actions example - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results ``` ### Step 2: Connect Slack In your Gaffer project settings, click "Connect to Slack" and authorize the integration. Select a channel and choose your notification trigger. ### Step 3: Configure Triggers Pick the trigger that matches your workflow: | Trigger | Best For | |---------|----------| | All runs | Small teams that want full visibility | | Failures only | Teams that only want to hear about problems | | Consecutive failures | Filtering out flaky one-off failures | That's it. Your next CI run will notify your team automatically. ## Comparing Test Notification Approaches | Approach | Setup | Maintenance | Test Detail | Filtering | |----------|-------|-------------|-------------|-----------| | Check CI manually | None | None | Full (if you find it) | None | | GitHub email notifications | Built-in | None | Minimal | Limited | | Custom Slack webhook script | High | High | Whatever you build | Whatever you build | | CI provider notifications | Built-in | Low | Job-level only | Limited | | Gaffer | Low | None | Full report link + summary | Branch, trigger type | ## Beyond Notifications Notifications are the entry point. Once you're uploading test results, you also get: - **[Hosted test reports](/solutions/html-test-reports/)** — shareable URLs for every run - **[Flaky test detection](/solutions/flaky-test-detection/)** — automatic identification of unreliable tests - **[Test analytics dashboard](/solutions/test-results-dashboard/)** — pass rate trends, duration tracking, failure patterns ## Get Started Gaffer's free tier includes Slack integration, webhook support, and GitHub commit status with 500 MB of storage and 7-day retention.
--- ## Test Report Storage Costs Getting Out of Control? Here's the Fix Source: https://gaffer.sh/solutions/test-report-storage-costs/ import { Image } from "astro:assets";

You set up an S3 bucket to store test reports. Smart move—now you have historical data. Six months later, the storage bill is 10x what you expected. Sound familiar?

## The Hidden Cost of "Just Store It" When teams outgrow CI artifact storage, the natural solution is cloud storage: ```yaml - name: Upload to S3 run: aws s3 cp ./test-results s3://test-reports/${{ github.sha }} --recursive ``` Simple. Effective. And a ticking time bomb for your cloud bill. ### Why Storage Costs Compound Unlike compute (which stops billing when idle), storage bills accumulate: | Month | New Reports | Total Stored | Monthly Cost* | |-------|-------------|--------------|---------------| | 1 | 50 GB | 50 GB | $1.15 | | 3 | 50 GB | 150 GB | $3.45 | | 6 | 50 GB | 300 GB | $6.90 | | 12 | 50 GB | 600 GB | $13.80 | | 24 | 50 GB | 1.2 TB | $27.60 | *S3 Standard pricing at $0.023/GB That's assuming steady growth. Teams that scale up CI runs or add more test suites see exponential increases. ### The Playwright Problem E2E testing frameworks like Playwright make storage costs explode. A typical Playwright setup generates: - **Screenshots on failure**: 200KB–2MB each - **Video recordings**: 5–50MB per test - **Trace files**: 10–100MB per test run A team running 100 E2E tests across 3 browsers, 4 times per day: | Artifact Type | Size per Run | Daily | Monthly | |---------------|--------------|-------|---------| | HTML Report | 5 MB | 20 MB | 600 MB | | Screenshots | 50 MB | 200 MB | 6 GB | | Videos | 500 MB | 2 GB | 60 GB | | Traces | 200 MB | 800 MB | 24 GB | | **Total** | **755 MB** | **~3 GB** | **~90 GB** | That's 90 GB per month from *one* project. Most teams have multiple. And nobody wants to disable recordings—they're essential for debugging flaky tests. ## DIY Cleanup: Harder Than It Looks "We'll just write a cleanup script." Famous last words. ### Lifecycle Rules Look Simple ```xml 90 test-reports/ ``` ### But Reality Is Messy **Problem 1: One-size-fits-all doesn't work** Your main branch test reports are critical for debugging production issues. Feature branch reports are disposable after merge. S3 lifecycle rules can't distinguish between them without complex prefix schemes. **Problem 2: Accidental deletion** A misconfigured rule deleted 6 months of production test history. Now you're explaining to leadership why you can't investigate that customer-reported bug from Q2. **Problem 3: Cost visibility** Which project is eating all the storage? S3 doesn't tell you without additional tooling (CloudWatch, Cost Explorer tags, third-party analytics). **Problem 4: Access control** Who can view reports? S3 bucket policies are notoriously tricky. Teams end up either too permissive (security risk) or too restrictive (people can't access what they need). ## What Teams Actually Need After talking to dozens of engineering teams, the requirements are clear: 1. **Automatic cleanup** - Old reports deleted without manual intervention 2. **Per-project control** - Different retention for different projects 3. **Predictable costs** - Know what you'll pay before the bill arrives 4. **Easy access** - Browse and share reports without S3 permissions 5. **No maintenance** - No scripts to write, debug, or update ## How Gaffer Solves Storage Costs Gaffer is purpose-built for test report hosting. Storage management is built in, not bolted on. ### Automatic Cleanup by Default Every plan includes automatic cleanup based on your retention period: | Plan | Default Retention | Storage Included | |------|-------------------|------------------| | Free | 7 days | 500 MB | | Pro | 30 days | 10 GB | | Team | 90 days | 50 GB | Old reports are automatically deleted. No lifecycle rules to configure. No scripts to maintain. ### Per-Project Retention Not all projects are equal. Configure retention per-project: - **Feature branch tests**: 7 days (auto-cleanup after merge) - **Main branch tests**: 90 days (debug production issues) - **Compliance projects**: Unlimited (paid plans can disable cleanup) ```yaml # Your CI stays simple - name: Upload to Gaffer uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results ``` Retention is configured in the Gaffer dashboard, not your CI pipeline. ### Predictable Pricing Go over your storage limit? Simple overage billing at $0.50/GB. No surprise bills, no complex pricing tiers. **Example scenario:** - Team plan: 50 GB included - Actual usage: 65 GB - Overage: 15 GB × $0.50 = $7.50 extra Compare that to debugging S3 cost spikes across multiple buckets and projects. ### Cost Visibility Built In See storage usage per project in your dashboard: - Which projects use the most storage? - How is usage trending over time? - Where should you enable more aggressive cleanup? No CloudWatch dashboards or Cost Explorer tags required. ## Migration Is Painless Already have test reports in S3? You don't need to migrate anything: 1. Add Gaffer upload to CI (5 minutes) 2. New reports go to Gaffer automatically 3. Old S3 reports stay where they are (or delete them to save costs) 4. Over time, Gaffer becomes your source of truth Your S3 bucket can run down naturally while Gaffer handles new reports. ## When to Keep S3 Gaffer isn't the right fit for everyone: - **Raw build artifacts** (binaries, packages) → Keep in S3/artifact storage - **Compliance archives** requiring specific certifications → Use compliant storage - **Existing tooling** that depends on S3 paths → Evaluate migration cost For test reports specifically—HTML reports, JUnit XML, JSON results—Gaffer is purpose-built and cost-optimized. ## The Bottom Line S3 is great for general-purpose storage. It's not optimized for test reports. | Concern | S3 DIY | Gaffer | |---------|--------|--------| | Automatic cleanup | Manual lifecycle rules | Built-in | | Per-project retention | Complex prefix schemes | Dashboard toggle | | Cost predictability | Varies with usage | Fixed + simple overage | | Access control | Bucket policies | Team-based permissions | | Setup time | Hours to days | 5 minutes | | Maintenance | Ongoing | Zero | ## Get Started Stop watching storage costs climb. Gaffer's automatic cleanup keeps your test report storage predictable, with per-project controls when you need them.
--- ## Test Reporting for QA Teams: Visibility Without Developer Tooling Source: https://gaffer.sh/solutions/test-reporting-qa-teams/ import { Image } from "astro:assets";

QA needs to see test results. But test results live in CI — behind GitHub logins, buried in workflow runs, locked in expiring artifacts. QA shouldn't need to learn developer tooling just to find out what failed.

## The QA Access Problem Automated test results typically live in CI: - **GitHub Actions** - Requires repo access, navigation through workflow runs - **Jenkins** - Another dashboard to learn, often requires VPN - **GitLab CI** - Mixed with pipelines, deployments, and other dev stuff QA engineers end up asking developers "can you send me the test results?" or learning CI tools they shouldn't need to know. And when CI artifacts expire (30-90 days), historical data disappears. Good luck comparing this week's test health to last month. ## What QA Teams Need ### Direct Report Access A URL that shows test results. No CI login, no navigating pipelines, no downloading zip files. Share it in Slack: ``` Here's the regression suite from this morning: https://app.gaffer.sh/reports/abc123 ``` Anyone with the link sees the results immediately. ### Historical View QA needs to answer questions like: - "Is this test suite getting more stable?" - "When did these tests start failing?" - "How often does this test actually pass?" This requires data across time, not just the latest run. ### Flaky Test Tracking Flaky tests are a QA nightmare. "It passed locally" vs "It failed in CI" wastes hours of back-and-forth. Knowing which tests are legitimately unreliable vs which failures are real bugs is essential. ### Non-Technical Interface QA shouldn't need to learn git, CI pipelines, or command-line tools to check test results. A web interface that shows pass/fail counts, trends, and failures is enough. ## How Gaffer Helps QA Teams ### Shareable Report URLs Every test run gets a URL in the Gaffer dashboard. QA accesses results directly — no CI login required. Share links in: - Slack channels - Bug tickets - Test management tools - Emails to stakeholders For people outside your organization — external QA consultants, clients, or partner teams — you can generate [public share links](/solutions/shareable-test-report-links/) with configurable expiration. Recipients view the full report in their browser without needing a Gaffer account. ### Dashboard View See all projects and recent test runs in one place: - Pass/fail counts - Trend direction (improving or degrading) - Recent failures No navigating through CI menus. Just a list of projects and their test health. ### Historical Trends Track test suite health over time: - **Pass rate trends** - Is quality improving? - **Failure patterns** - Which tests fail most often? - **Duration trends** - Is the suite getting slower? Gaffer retains data for up to 90 days (depending on plan), longer than most CI artifact retention. ### Flaky Test Reports Automatically identify tests with inconsistent results: - **Flip rate** - How often does this test change between pass and fail? - **Last seen** - When did the flaky behavior last occur? - **Run count** - How many executions are in the analysis? QA can take this list to developers: "These 5 tests are flaky. Can we fix or quarantine them?" ### Slack Notifications Get notified when tests fail: ``` [FAILED] regression-suite - main 3 tests failed, 142 passed View report: https://app.gaffer.sh/reports/xyz789 ``` QA sees failures as they happen, with direct links to investigate. ## QA + Dev Collaboration Test results become a shared artifact: 1. **CI runs tests** - Results upload to Gaffer automatically 2. **QA monitors** - Sees failures in Slack or dashboard 3. **QA investigates** - Opens report, reviews failures 4. **QA files bugs** - Includes report link for context 5. **Dev fixes** - Uses the same report link for debugging 6. **QA verifies** - Checks next test run No "can you send me the output?" conversations. Everyone looks at the same report. ## For QA Leads and Managers ### Test Health Reporting Need to report on test suite health to stakeholders? Gaffer provides: - Overall pass rates - Trend direction - Flaky test counts - Test count over time Export data or share dashboard views in status meetings. ### Coverage Across Projects If your team tests multiple projects, see them all in one dashboard. Compare health across projects, identify which need attention. ## Getting Started QA doesn't need to set up Gaffer - developers add one CI step and results start flowing. But QA should be involved in: 1. **Getting access** - Request a Gaffer account from whoever set it up 2. **Connecting Slack** - Ensure notifications go to the right channels 3. **Learning the dashboard** - It's simple, but a quick walkthrough helps Once set up, QA has direct access to test results without touching CI tools.
--- ## Test Reporting for Small Teams: Simple Setup, No DevOps Required Source: https://gaffer.sh/solutions/test-reporting-small-teams/ import { Image } from "astro:assets";

Small teams ship fast. You don't have dedicated DevOps. You don't have time to set up and maintain test infrastructure. You just need to see what failed in CI without digging through logs.

## The Small Team Testing Reality You're probably dealing with some combination of: - **Limited CI artifacts** - GitHub Actions gives you 90 days, then reports disappear - **No central dashboard** - Test results scattered across branches and workflow runs - **Slack is your command center** - That's where work happens - **Nobody has time for infrastructure** - You're writing features, not managing servers Enterprise test reporting tools want you to set up databases, deploy services, and manage infrastructure. That's not realistic when you have 3-10 engineers and a hundred things to ship. ## What Small Teams Actually Need ### Shareable Test Results When a test fails, you need to share it with the person who can fix it: - Not "go to GitHub, find the workflow, click through three menus, download artifacts" - Just a URL that shows the failure Need to share results with someone outside your org — a contractor or a stakeholder? Generate a [public share link](/solutions/shareable-test-report-links/) with an expiration you control. They see the results in their browser, no account needed. ### Slack Integration You're already in Slack. Test failures should show up there: ``` [FAILED] api-tests - main 2 tests failed, 47 passed View report: https://app.gaffer.sh/reports/abc123 ``` Click the link, see the failure, fix it. No context switching to CI dashboards. ### Flaky Test Detection Small teams can't afford to waste time re-running "that flaky test." You need to know which tests are unreliable so you can fix or quarantine them. ### Zero Infrastructure If it requires running a server, maintaining a database, or configuring Kubernetes, it's not for small teams. You need something that works with a single CI step. ## How Gaffer Works for Small Teams ### 5-Minute Setup 1. Sign up (free tier available) 2. Get an API key 3. Add one step to CI **GitHub Actions:** ```yaml - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v1 with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results ``` That's it. Test reports are now hosted and shareable. ### Connect Slack Enable Slack notifications and choose which branches trigger alerts. Most teams only want notifications for `main` - you don't need noise from every feature branch. ### Connect GitHub Enable commit statuses to see test results directly on PRs. No more "CI passed" that actually means "tests passed" - you see the test count right on the commit. ### Track Flaky Tests Gaffer automatically identifies tests that flip between pass and fail. The analytics page shows: - Which tests are most unreliable - How often they flip - Whether they're getting better or worse ## Pricing That Makes Sense Small teams don't have enterprise budgets. Gaffer pricing is flat-rate, not per seat: - **Free** - 500 MB storage, 7-day retention - **Pro** - $15/mo, 10 GB storage, 30-day retention - **Team** - $49/mo, 50 GB storage, 90-day retention All plans include unlimited users and unlimited projects. One price whether you have 3 engineers or 15. ## Real Small Team Workflow Here's how it typically works: 1. **Developer pushes code** - Tests run in CI 2. **Tests fail** - Slack gets a notification with a report link 3. **Developer clicks link** - Sees exactly what failed, with screenshots and stack traces 4. **Developer fixes it** - Or asks for help by sharing the same link 5. **Tests pass** - Commit status turns green No digging through CI logs. No downloading artifacts. No "can you check the test output?" conversations. ## What You Don't Need Gaffer skips features that small teams don't use: - No complex dashboard builder - No ML-powered failure analysis - No role-based access control tiers - No enterprise SSO requirements Just test reports, sharing, and analytics. The basics, done well. ## Try It Start with the free tier. One project, 7-day retention. Enough to see if it fits your workflow before paying anything.
--- ## Test Results Dashboard: One Place for Every CI Run Source: https://gaffer.sh/solutions/test-results-dashboard/ import ScreenshotLink from "../components/ScreenshotLink.astro"; import dashboard from "../assets/screenshots/project-overview.png"; import durationTrends from "../assets/screenshots/duration-trends-chart.png";

Your tests finished 10 minutes ago. You click into GitHub Actions, find the workflow run, expand the test step, scroll through 500 lines of output looking for the failure. By the time you find it, you've forgotten why you were looking. Sound familiar?

## Where CI Test Results Hide Every CI platform buries test results differently, but they all make you work for it: - **GitHub Actions** - Workflow run → Job → Step → Expand logs → Scroll - **GitLab CI** - Pipeline → Job → Scroll through output - **Jenkins** - Build → Console Output → Search for "FAILED" - **CircleCI** - Workflow → Job → Steps → Find the test step Even when you find the results, you're looking at raw terminal output. Pass counts mixed with stack traces mixed with timing info. Good luck sharing that with your PM. ## The Real Pain Points ### No Single View Test results are scattered across CI runs, branches, and PRs. Want to know the overall health of your test suite? You'd need to manually check multiple workflow runs and piece it together. ### Context Switching Every failed test means: leave your IDE → open CI → find the right run → expand the right step → scroll to the failure. Multiply by 5 failures and 10 minutes are gone. ### No Historical Context Was this test flaky last week? Has the pass rate been declining? CI logs don't tell you. Each run exists in isolation with no connection to the past. ### Team Visibility When QA asks "did the tests pass?" you have to go check. When the PM wants to see test coverage for a feature, you export a CSV. When a colleague debugs the same flaky test you fixed last month, there's no shared knowledge. ## What Teams Actually Need A test results dashboard should answer these questions instantly: | Question | Dashboard Answer | |----------|------------------| | Did the tests pass? | Green checkmark or red X, visible in 2 seconds | | Which tests failed? | Click to see the specific failures | | Is this test flaky? | Historical pass rate shows the pattern | | Is the suite healthy? | Trend line shows improvement or decline | | Can I share this with QA? | Send a link, no CI access required | ## CI Logs vs. Test Dashboard | Aspect | CI Logs | Test Dashboard | |--------|---------|----------------| | Time to find failure | 2-5 minutes | 10 seconds | | Historical data | None | Full run history | | Shareable | Requires CI access | Simple link | | Cross-framework view | Separate per tool | Unified | | Flaky test detection | Manual observation | Automatic | | Team access | Developer-only | Anyone with link | ## How Gaffer Works as Your Test Dashboard Gaffer collects test results from your CI pipeline and presents them in a clean, searchable dashboard. ### Automatic Upload from CI Add one step to your workflow. Results appear in the dashboard immediately. **GitHub Actions:** ```yaml - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v2 with: api-key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report-path: ./test-results ``` **GitLab CI:** ```yaml test: script: - npm test after_script: - curl -X POST https://api.gaffer.sh/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@test-results/results.json" ``` ### Dashboard Features **Project Overview** - Pass rate at a glance - Recent test runs with status - Trend over time **Test Run Details** - Every run listed with status, duration, commit SHA - Filter by branch, date range, pass/fail - Click into any run to see individual test results **Failure Investigation** - See exactly which tests failed - Error messages and stack traces - Link directly to the failing test ### Works With Any Framework Gaffer accepts results from: - **JavaScript/TypeScript**: Jest, Vitest, Playwright, Cypress - **Python**: pytest (with JUnit XML output) - **Java**: JUnit, TestNG - **Any framework** that outputs JUnit XML or JSON ## Beyond Visibility: Analytics Once you have historical test data, you unlock insights that CI logs can't provide: ### Flaky Test Detection Gaffer tracks pass/fail rates across runs. Tests that fail intermittently (10-90% failure rate) are flagged as flaky. No more "I think that test is flaky" - you have data. ### Duration Trends Are your tests getting slower? The analytics dashboard shows duration trends so you can catch performance regressions early. ### Filter and Search Quickly find what you need: - Filter by branch, commit, or date range - See which tests fail most frequently - Track test duration trends over time ## Getting Started Gaffer's free tier includes 500 MB storage with 7-day retention - enough to try it on a real project. Paid plans offer up to 90-day retention and advanced analytics.
--- ## Gaffer vs TestDino: Two Bets on Test Reporting Source: https://gaffer.sh/solutions/testdino-vs-gaffer/ import JsonLd from '../components/JsonLd.astro';

Gaffer and TestDino both host and analyze CI test reports, and they bet on different things. [TestDino](https://testdino.com/) goes deep on Playwright: inline trace, DOM, and network debugging, a built-in test-case management layer, and an agent-writable MCP server. Gaffer stays framework-agnostic and prices on storage instead of executions or seats. The clearest split is the bill. TestDino's tiers gate on a monthly execution quota and a hard user cap (1 user on Free, 3 on Pro, 20 on Team), while Gaffer bills on stored bytes with unlimited users on every tier. A 25-person team running roughly 100,000 Playwright results a month clears TestDino's Team tier on two axes at once, the 75,000-result quota and the 20-seat cap, while the same workload sits inside Gaffer's $49/month Team plan, which never caps users.

## What Gaffer and TestDino Actually Are TestDino is a Playwright-native test reporting and analytics platform. It ingests Playwright output and adds inline debugging (trace, DOM snapshots, network logs), root-cause failure categorization, test-case management, and an MCP server that coding agents can both read from and write to. The full feature reference is in [TestDino's docs](https://docs.testdino.com/). Gaffer hosts and analyzes test reports from many frameworks. It ingests JUnit XML, CTRF, Jest and Vitest JSON, native Playwright JSON, and TRX/.NET, plus four coverage formats, then runs [flaky detection](/solutions/flaky-test-detection/), pass-rate trends, and [failure clustering](/solutions/failure-clustering/) on every upload. It is a reporting and analytics surface, not a test-management tool. ## Where TestDino Is Stronger If your stack is Playwright and you want depth over breadth, TestDino has real advantages Gaffer does not match: - **Playwright-native inline debugging:** trace, DOM snapshots, and network logs attached to each failure, viewable without leaving the report. - **Write-capable MCP:** its public MCP server (`testdino-hq/testdino-mcp`) exposes 33 tools, including mutations like `create_manual_test_case` and `create_release`, so a coding agent can manage test artifacts directly. - **Built-in test-case management:** suites, cycles, and imports from TestRail or CSV. - **Root-cause failure grouping:** failures are grouped by error message and a root-cause category (timing, environment, network, assertion), on top of message matching. - **Failed-only reruns and flakiness merge-gates:** rerun only the failures, and block merges on a flaky signal. - **Published compliance posture:** TestDino publicly claims SOC 2 Type II and ISO 27001 certification and GDPR compliance on its security page. Gaffer claims no certifications, which is a real gap for teams with a security-review checklist. ## Where Gaffer Is Stronger Gaffer's advantages are pricing shape and framework breadth, plus a smaller surface to adopt. - **Flat, storage-based pricing with unlimited users:** no per-tier execution quota and no seat cap. Everyone on the team can have an account on any tier, including Free. - **Multi-framework reporting:** one dashboard for JUnit XML, CTRF, Jest, Vitest, native Playwright JSON, and TRX/.NET, which transitively covers pytest, Go, Cypress, and anything that emits JUnit XML or CTRF. See [multiple test frameworks](/solutions/multiple-test-frameworks/). - **[Coverage tracking](/solutions/code-coverage/) alongside test results:** four coverage formats parsed and trended in the same view as pass rate. - **No test-management layer to adopt:** Gaffer hosts reports and runs analytics. There is nothing to configure beyond a CI upload step. ## Pricing: Execution Quotas vs Flat Storage The two products meter different things. TestDino charges a flat monthly subscription but gates each tier on a monthly execution quota and a hard user cap. Gaffer charges a flat monthly subscription that meters only storage, and never caps users. TestDino counts each test-case run as one result: a single test run across three browsers counts as three results, and each retry counts again. Cross-browser matrices and retries consume the quota quickly. Gaffer counts nothing per-execution. It bills on stored bytes, with $0.50/GB/month overage on Pro and Team, and puts no cap on users on any tier including Free. Which is cheaper depends on the shape of your workload. A small Playwright-only team may fit comfortably inside TestDino's Free or Pro tier. A CI-heavy team, or one past 20 people, usually pays less on Gaffer, because neither execution volume nor headcount moves the bill. The above-quota overage price on TestDino is not published, so a team near its quota should confirm it before scaling up. ## Pricing Comparison | Tier | TestDino | Gaffer | |------|----------|--------| | Free | $0, 5,000 results/mo, 1 user, 1 project | $0, 500 MB, 7-day retention, unlimited users | | Entry paid | $39/mo billed annually ($49/mo monthly), 25,000 results/mo, 3 users, 3 projects | $15/month, 10 GB, 30-day retention, unlimited users | | Team | $79/mo (annual-effective), 75,000 results/mo, 20 users, 5 projects | $49/month, 50 GB, 90-day retention, unlimited users | | Overage | Above-quota price not published | $0.50/GB/month (Pro and Team) | | Enterprise | Custom | Not offered | | Billing basis | Execution quota + user cap per tier | Storage, unlimited users | A "result" on TestDino is one test-case run, so cross-browser runs and retries multiply against the quota. Gaffer's bill moves only with stored bytes. ## Feature Comparison | Feature | TestDino | Gaffer | |---------|----------|--------| | Test report hosting | Yes | Yes | | Framework scope | Playwright-native | Multi-framework (JUnit XML, CTRF, Jest, Vitest, Playwright JSON, TRX/.NET; transitively pytest, Go, Cypress) | | Inline trace / DOM / network debugging | Yes (Playwright-native) | No | | Historical trends | Yes | Yes | | Flaky test detection | Yes | Yes | | Failure grouping | Error message + root-cause category | Error-message similarity + file-path fallback | | Test-case management (suites, cycles, imports) | Yes | No (by design) | | Failed-only reruns / flaky merge-gates | Yes | No | | MCP server | Yes, write-capable (33 tools) | Yes, read-only (16 GET functions) | | Coverage tracking | Not advertised | Yes (4 formats) | | Published compliance posture | SOC 2 Type II and ISO 27001 claimed; GDPR compliance stated | None | | User pricing | Capped per tier (1 / 3 / 20) | Unlimited users, all tiers | Both MCP servers are real. TestDino's is write-capable so an agent can create test cases and releases. Gaffer's is read-only code-mode analytics (16 GET functions, zero mutations), a deliberate choice that matches Gaffer not being a test-management tool: query your results, do not manage test cases from the editor. ## When to Use TestDino TestDino is the better fit if: - Your suite is Playwright and you want inline trace, DOM, and network debugging on every failure. - You want a coding agent that can write test artifacts (manual test cases, releases) through MCP, beyond reading metrics. - You need built-in test-case management: suites, cycles, and imports from TestRail or CSV. - Failed-only reruns and flakiness merge-gates are part of your CI workflow. - Your security review needs a vendor that publishes SOC 2 Type II and ISO 27001 claims. - Your team is small enough to sit inside the 1 / 3 / 20 user caps and the per-tier result quota. ## When to Use Gaffer Teams shopping for a TestDino alternative usually hit one of two walls first: the seat cap or the execution quota. Gaffer is the better fit if: - You run more than one test framework and want them in a single dashboard. - Your team is at or past 20 people, or growing, and a seat cap is the binding constraint. - Your CI produces high result volume (cross-browser matrices, frequent retries) and you would rather not track it against a quota. - You want coverage tracking trended next to pass rate for the same upload. - You want a plain hosted-report and analytics surface with no test-management layer to adopt. - You want read-only MCP analytics for agentic CI without giving an agent write access to your test data. ## Using Both You can run both without a migration. Standard report formats are portable, so a common split is to upload Playwright output to TestDino for deep, inline debugging and send JUnit XML or CTRF to Gaffer for multi-framework hosting and analytics. Neither side requires a big-bang cutover, and JUnit XML and CTRF are first-class inputs on Gaffer, so adding a second upload step in CI is the whole cost. ## FAQ ### Is Gaffer or TestDino cheaper? It depends on the shape of your workload. TestDino gates each tier on a monthly execution quota and a hard user cap (1 user on Free, 3 on Pro, 20 on Team), while Gaffer bills on storage with unlimited users. A CI-heavy team, or one larger than 20 people, usually pays less on Gaffer, because neither execution volume nor headcount changes the bill. A small Playwright-only team may fit inside TestDino's free or Pro tier. ### Does Gaffer support Playwright? Yes. Gaffer ingests native Playwright JSON, and also Jest, Vitest, pytest, Go, Cypress, and .NET through JUnit XML, CTRF, and native JSON formats, plus coverage formats. TestDino is Playwright-native and does not claim other frameworks. ### Does Gaffer have test-case management? No, by design. Gaffer hosts and analyzes test results; it is not a test-management tool and has no suites, cycles, or manual test plans. TestDino includes test-case management with suites, cycles, and imports from TestRail or CSV. ### Is TestDino only for Playwright? TestDino is Playwright-native and does not claim support for other frameworks. Its inline trace, DOM, and network debugging are built on Playwright's own artifacts. If you run multiple frameworks and want them in one dashboard, Gaffer's multi-framework ingestion is the better fit. ### Can I use both Gaffer and TestDino? Yes. Standard report formats are portable, so a team can upload Playwright output to TestDino for deep debugging and send JUnit XML or CTRF to Gaffer for multi-framework hosting and analytics. There is no big-bang migration required in either direction. ## Other Comparisons Evaluating more tools? See how Gaffer compares to [Currents.dev](/solutions/currents-dev-alternative/), [Allure](/solutions/allure-alternative/), and [ReportPortal](/solutions/reportportal-alternative/). ## Try It Gaffer's free tier includes 500 MB storage, 7-day retention, and unlimited users. Enough to point your CI at it for a week and decide.
--- ## How to Share Vitest Test Results with Your Team Source: https://gaffer.sh/solutions/vitest-test-reporting/ import { Image } from "astro:assets";

Vitest's HTML reporter is excellent — interactive, fast, and genuinely useful for debugging. But it generates local files. When your teammate asks "what failed?", you can't just send them a link. You end up screenshotting terminal output or walking them through downloading CI artifacts.

## The Sharing Problem After `vitest run`, your HTML report lands in a local directory. To share it with your team, you need to get that file to them somehow. Here's what most teams do: ### Common (Bad) Solutions **1. CI artifacts** - GitHub Actions keeps artifacts for 90 days max, other CI systems are shorter - Finding the right artifact means clicking through workflow runs - Recipient downloads a zip, extracts it, opens `index.html` locally - QA might not even have CI access **2. Email or Slack the file** - "Here's the test report" with a zip attachment - Recipient has to download, extract, open in browser - Version confusion when multiple runs are in flight **3. Upload to S3 manually** - Works, but someone has to set up the bucket, permissions, and cleanup - Manual step every time — gets skipped when things are busy **4. Paste terminal output** - Loses all the interactive HTML goodness - Hard to parse in a Slack thread - Can't reference it later ## Better: Hosted Vitest Reports with Shareable Links The fix is straightforward: upload your Vitest reports to a hosting service after every CI run. Every test run gets a permanent URL that anyone on your team can open. ### What This Looks Like 1. CI runs your Vitest tests 2. Reports upload automatically 3. You get a URL like `https://app.gaffer.sh/reports/abc123` 4. Share it in Slack, GitHub PRs, Jira — anywhere 5. Anyone with access sees the full interactive report No downloads. No manual steps. No expiring artifacts. ## Setting Up Vitest Report Sharing with Gaffer ### Step 1: Configure Vitest Reporters Make sure you're generating output Gaffer can parse. CTRF gives the richest analytics: ```bash npm install vitest-ctrf-json-reporter --save-dev ``` ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { reporters: ['default', 'vitest-ctrf-json-reporter'], } }) ``` Gaffer also accepts Vitest's built-in HTML and JSON reporters if you prefer those. If you also want to [set up a Vitest coverage report and track it across runs](/blog/vitest-coverage-reports/), that's a separate upload step covered in the coverage guide. ### Step 2: Add the Upload Step to CI **GitHub Actions:** ```yaml - name: Run tests run: npm test - name: Upload to Gaffer if: always() uses: gaffer-sh/gaffer-uploader@v1 with: gaffer_api_key: ${{ secrets.GAFFER_PROJECT_TOKEN }} report_path: ./test-results commit_sha: ${{ github.sha }} branch: ${{ github.ref_name }} ``` **GitLab CI:** ```yaml test: script: - npm ci - npm test after_script: - | curl -X POST https://app.gaffer.sh/api/upload \ -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \ -F "files=@ctrf-report.json" \ -F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}' ``` ### Step 3: Share the Link After upload, Gaffer returns a URL. Share it anywhere — Slack, GitHub PR comments, Jira tickets. Recipients see the full report in their browser. For people outside your organization, you can generate a [public share link](/solutions/shareable-test-report-links/) with configurable expiration. They view the full interactive report without needing a Gaffer account. ## Slack and Webhook Notifications Instead of checking CI manually, get notified when tests finish: **Slack integration:** - Pass/fail summary posted to your channel - Direct link to the full report - Filter by branch — only notify on `main`, skip feature branches **Webhooks:** - Send test results to any endpoint - Integrate with your existing alerting or workflow tools - Trigger custom actions on failure No more "did the tests pass?" messages. Your team sees failures as they happen, with a link to investigate. ## GitHub Integration Commit statuses show test results directly on PRs. No clicking through to CI logs — you see pass/fail right on the commit. ## One Dashboard for Every Suite Most JavaScript teams run Vitest for unit tests and Playwright or Cypress for end-to-end. Gaffer aggregates [multiple test frameworks](/solutions/multiple-test-frameworks/) into one dashboard, so unit and e2e results land in the same place instead of separate report folders. ## What About Flaky Tests? Vitest's `--retry` flag handles flaky tests within a single run. Gaffer tracks flakiness *across* runs — identifying tests that sometimes pass and sometimes fail over time. Different problem, complementary solution. ## Get Started Gaffer's free tier includes 500 MB of storage with 7-day retention. Paid plans offer extended retention up to 90 days.
# Blog - [Your AI Agent's Missing Layer: Test Intelligence](https://gaffer.sh/blog/agentic-ci-test-intelligence/) - [Best Test Automation Reporting Tools in 2026 (Compared)](https://gaffer.sh/blog/best-test-automation-reporting-tools/) - [Cypress Reports: Setup Guide for Mochawesome, JUnit, and Allure](https://gaffer.sh/blog/cypress-reports-guide/) - [Why `gaffer affected-tests` Returned Empty for Every E2E-Touching Edit I Made](https://gaffer.sh/blog/dogfooding-affected-tests-e2e-gap/) - [We Built Coverage Analytics. Then We Used Them On Ourselves.](https://gaffer.sh/blog/dogfooding-coverage-mcp-tools/) - [Dogfooding Gaffer's MCP Server to Fix Slow Playwright Tests](https://gaffer.sh/blog/dogfooding-mcp-playwright-optimization/) - [GitHub Agentic Workflows: Automated Test Reviews with MCP](https://gaffer.sh/blog/github-agentic-workflows-test-analytics/) - [Give Your AI Coding Tools Access to Your Test Results](https://gaffer.sh/blog/give-ai-tools-test-results/) - [Health Score Alerts: Know Before Your Test Suite Degrades](https://gaffer.sh/blog/health-score-alerts/) - [How Much Are Flaky Tests Costing You?](https://gaffer.sh/blog/how-much-are-flaky-tests-costing-you/) - [How to Manage Flaky E2E Tests at Scale](https://gaffer.sh/blog/how-to-manage-flaky-e2e-tests/) - [JUnit XML Format Explained: Schema, Examples, CI Integration](https://gaffer.sh/blog/junit-xml-format-guide/) - [OpenTelemetry for Test Metrics: Export to Any Stack](https://gaffer.sh/blog/opentelemetry-test-metrics/) - [Playwright MCP with Claude Code: Setup & CI Guide](https://gaffer.sh/blog/playwright-mcp-claude-code-setup/) - [Playwright MCP + Claude Code: A Complete Test Loop](https://gaffer.sh/blog/playwright-mcp-claude-code-test-loop/) - [Playwright MCP + Cursor: Complete Setup and Testing Workflow](https://gaffer.sh/blog/playwright-mcp-cursor-setup/) - [Playwright Pricing in 2026: Free vs Paid Compared](https://gaffer.sh/blog/playwright-pricing/) - [Playwright Reports: HTML, JSON, JUnit & Sharing in CI](https://gaffer.sh/blog/playwright-reports-guide/) - [Playwright Test Agents: Planner, Generator & Healer](https://gaffer.sh/blog/playwright-test-agents-guide/) - [Test Failures: Types, Root Causes, and How to Fix Them](https://gaffer.sh/blog/test-failures-types-causes-fixes/) - [Test Intelligence: The Missing Context for AI Coding Tools](https://gaffer.sh/blog/test-intelligence-for-ai-tools/) - [Test Reporting: What to Include and How to Automate It](https://gaffer.sh/blog/test-reporting-guide/) - [Vitest Coverage Reports: CI Setup and Team Visibility](https://gaffer.sh/blog/vitest-coverage-reports/) - [What Are Test Artifacts in Software Testing?](https://gaffer.sh/blog/what-are-test-artifacts/)