This feature is currently in beta. Make sure you have set up the Tusk Drift CLI and SDK before enabling coverage.
Enable Coverage
Addcoverage.enabled: true to your .tusk/config.yaml:
--validate-suite-if-default-branch in CI, which the Drift GitHub Action sets by default). No changes to your CI pipeline needed. PR branches run tests normally without coverage overhead.
Filter files
Exclude files from coverage reports using glob patterns:include to restrict coverage to your service’s code:
** for recursive directory matching:
| Pattern | Matches |
|---|---|
**/migrations/** | Any file in any migrations/ directory |
backend/src/** | All files under backend/src/ |
**/*.test.ts | Any .test.ts file |
backend/src/db/migrations/** | A specific subdirectory |
Paths are relative to the git root (e.g.,
backend/src/db/migrations/...). Use **/migrations/** rather than migrations/** to match correctly.Test locally
Use--show-coverage to see coverage output during local development:
Reading the Output
After tests complete with--show-coverage, you’ll see:
--print), coverage appears in the service logs panel (aggregate) and each test’s log panel (per-test detail).
CI/CD Integration
Automatic (via config)
Withcoverage.enabled: true, coverage data is automatically collected during validation runs. No CI changes needed:
Exporting to Codecov
Add--coverage-output to export LCOV for third-party tools:
What’s included in coverage output:
- In-suite tests: Always included, even if they fail. A failing test still exercises code paths.
- Draft tests: Excluded from the file. Draft coverage data is uploaded to Tusk Cloud for promotion decisions.
- After promotion: The Tusk Cloud dashboard will include newly promoted tests. The LCOV catches up on the next validation run.
Language Notes
- Node.js
- Python
- Uses V8’s built-in precise coverage, no external dependencies needed
- TypeScript source maps handled automatically when compiling (
sourceMap: truein tsconfig.json required fortsc,swc,esbuild) - Node.js
--experimental-strip-typesworks out of the box (no source maps needed — V8 runs.tsdirectly) - Tested with: CJS (
require), ESM (import),tsc,ts-node,ts-node-dev,swc,esbuild(compile mode),--experimental-strip-types - If using
tsc, run a clean build to avoid stale artifacts:rm -rf dist && tsc - Near-zero performance overhead
- Single-process mode required — Node.js
clustermodule and PM2 cluster mode are not supported during coverage runs - Bundlers (webpack, esbuild bundle mode, Rollup, Vite) should work if source maps are produced, but are not yet fully tested. Contact us if you use a bundler and want to confirm compatibility.
Docker Compose
If your service runs in Docker Compose, add coverage env vars to yourdocker-compose.tusk-override.yml:
- Node.js
- Python
NODE_V8_COVERAGE must be a fixed container path (not ${NODE_V8_COVERAGE:-}) because the CLI creates a host temp directory that doesn’t exist inside the container.strip_path_prefix to convert container paths to repo-relative:
/app/src/views.py). Set the value to whatever path your project root is mounted at in the container.
Export Formats
LCOV (default)
JSON
Troubleshooting
Coverage shows 0% / no files
Coverage shows 0% / no files
- Check that
TUSK_COVERAGEreaches the service. For Docker, ensure it’s indocker-compose.tusk-override.yml. - For Node.js Docker, ensure
NODE_V8_COVERAGEis set to a writable container path (e.g.,/tmp/tusk-v8-coverage). - For Python, ensure
coverageis installed (pip install coverage). - Run with
--show-coverage --printlocally to verify coverage works before CI.
Coverage is higher than expected from a single test
Coverage is higher than expected from a single test
The aggregate includes startup coverage (module loading, decorator execution, DI registration) which runs before any test request. This matches how standard coverage tools like Istanbul and NYC work. The per-test breakdown shows only lines exercised by each individual test request, excluding startup.
Many files show 100% coverage
Many files show 100% coverage
Type definitions, barrel exports (
index.ts), and config files are fully executed during module loading since their code is entirely top-level.Docker paths are container-absolute
Docker paths are container-absolute
Add
strip_path_prefix to your coverage config. Set it to the container mount point from your docker-compose.yaml volumes (e.g., - .:/app then use strip_path_prefix: "/app").TypeScript coverage shows .js paths instead of .ts
TypeScript coverage shows .js paths instead of .ts
This applies when compiling TypeScript to JavaScript (via
tsc, swc, or esbuild). Ensure sourceMap: true is set in your tsconfig.json and source map files (.js.map) are present alongside compiled output. If using tsc, run a clean build: rm -rf dist && tsc. If using --experimental-strip-types, this doesn’t apply — paths are already .ts.How is the coverage denominator calculated?
How is the coverage denominator calculated?
Before any tests run, the CLI takes a “baseline” snapshot that captures every line the runtime considers executable. For Node.js, this is every line V8 compiled (functions, statements, branches). For Python, this is every line coverage.py’s compiler identifies as executable. Comments, blank lines, and structural syntax (closing braces, etc.) are not counted. The denominator starts with files loaded at startup, and expands to include any additional files loaded during test execution (e.g., lazily-imported modules).
Does --concurrency work with coverage?
Does --concurrency work with coverage?
No. Coverage forces concurrency to 1, overriding any
--concurrency flag or test_execution.concurrency config. Per-test coverage relies on counter resets between tests. Running tests concurrently would mix coverage data between tests, making per-test attribution impossible.Understanding coverage numbers
Understanding coverage numbers
Drift coverage measures which lines of your service code are exercised by API trace tests. A few things to keep in mind when interpreting the numbers:Tusk selects traces that are representative of how your API is actually used, rather than optimizing for maximum code coverage. This means coverage maps directly to the code paths your users depend on — which is often more useful than a high percentage from synthetic tests.Coverage tracks code exercised through API requests. Background jobs, cron tasks, CLI commands, and other non-API code paths won’t appear in the results. This is by design — Drift tests verify API behavior.Branch coverage depends on input variety. A handler with an
if/elif chain will only show coverage for the branch arms that the recorded traffic actually triggered. As more diverse traffic is recorded and added to the test suite, branch coverage naturally increases.Coverage numbers will grow over time as more traces are added to the suite and as traffic covers more code paths.