Thank you for your interest in contributing to Mallard Metrics. This document covers everything you need to get started.
- Development Setup
- Project Structure
- Development Workflow
- Code Standards
- Testing
- Benchmark Protocol
- Security Guidelines
- Pull Request Checklist
- Getting Help
- Rust 1.94.0+ -- The
rust-toolchain.tomlfile will install the correct version automatically viarustup - Git
- Disk space -- DuckDB's bundled compilation produces large build artifacts (~27GB in debug mode). Run
cargo cleanbetween major rebuilds if space is constrained.
git clone https://github.com/tomtom215/mallardmetrics.git
cd mallardmetrics
# Verify your setup compiles and all tests pass
cargo test --all-targets
# Verify zero lint warnings and formatting violations
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt -- --checkIf all three commands succeed, your development environment is ready.
mallardmetrics/
├── src/
│ ├── main.rs -- Entry point, background tasks, signal handling
│ ├── lib.rs -- Module declarations
│ ├── config.rs -- TOML + env var configuration, validation, advisories
│ ├── server.rs -- Axum router, middleware, routes, /metrics, health
│ ├── test_support.rs -- Shared AppState builder for tests
│ ├── api/
│ │ ├── auth.rs -- Authentication, sessions, API keys
│ │ ├── errors.rs -- Error types and HTTP responses
│ │ └── stats.rs -- All analytics endpoint handlers
│ ├── ingest/
│ │ ├── handler.rs -- POST /api/event and the GET pixel
│ │ ├── buffer.rs -- Bounded in-memory event buffer with flush
│ │ ├── visitor_id.rs -- HMAC-SHA256 visitor ID and salt rotation
│ │ ├── useragent.rs -- User-Agent and Client Hints parsing
│ │ ├── geoip.rs -- MaxMind GeoIP reader
│ │ └── ratelimit.rs -- Per-site and per-IP token-bucket limiters
│ ├── query/
│ │ ├── mod.rs -- QueryScope: site, range and session window
│ │ ├── metrics.rs -- Core metrics, including session metrics
│ │ ├── breakdowns.rs -- Dimension breakdown queries
│ │ ├── timeseries.rs -- Gap-filled time-bucketed aggregations
│ │ ├── realtime.rs -- Last-N-minutes activity snapshot
│ │ ├── funnel.rs -- Cumulative funnels (behavioral ext)
│ │ ├── retention.rs -- Per-visitor retention cohorts (behavioral ext)
│ │ ├── sequences.rs -- Sequence matching (behavioral ext)
│ │ ├── flow.rs -- Flow analysis (behavioral ext)
│ │ ├── events.rs -- Goals and custom property keys/values
│ │ ├── revenue.rs -- Per-currency revenue totals
│ │ ├── export.rs -- Daily and raw export as CSV or JSON
│ │ ├── cache.rs -- TTL + LRU query cache
│ │ └── test_support.rs -- TestDb for query tests
│ ├── storage/
│ │ ├── mod.rs -- ReaderPool: round-robin read connections
│ │ ├── schema.rs -- DuckDB schema, events_all view, behavioral ext
│ │ ├── parquet.rs -- Atomic writes, compaction, retention, erasure
│ │ └── migrations.rs -- Schema versioning
│ └── dashboard/
│ └── mod.rs -- Embedded Preact+HTM SPA
├── tests/
│ └── ingest_test.rs -- Integration tests (66 tests)
├── benches/
│ └── ingest_bench.rs -- Criterion.rs benchmarks
├── src/dashboard/assets/ -- Frontend SPA files (embedded via rust-embed)
├── tracking/script.js -- Tracking script (~3.8 KB gzipped)
├── mallard-metrics.toml.example -- Configuration template
├── Dockerfile -- Alpine musl builder, FROM scratch runtime
├── docker-compose.yml -- Production-ready compose file
├── scripts/
│ ├── smoke-test.sh -- End-to-end checks against the real binary
│ ├── check-dashboard-methods.mjs -- Static check for dead method references
│ ├── check-dashboard-browser.mjs -- Drives the dashboard in a real browser
│ └── seed-demo-data.py -- 30 days of synthetic events, for demos
└── .github/workflows/ci.yml -- CI pipeline (8 jobs; the first is a 4-way matrix)
- Read
DEVELOPMENT.mdfor project context and module map - Read
LESSONS.mdfor pitfalls and proven patterns - Establish a baseline by running the full validation suite:
cargo test --all-targets \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& cargo fmt -- --check \
&& RUSTDOCFLAGS="-D warnings" cargo doc --no-deps- Create a feature branch from
main - Make your changes
- Write or update tests for every change
- Run the full validation suite:
cargo test --all-targets \
&& cargo clippy --all-targets --all-features -- -D warnings \
&& cargo fmt -- --check \
&& RUSTDOCFLAGS="-D warnings" cargo doc --no-deps- Commit with a clear, descriptive message
- Open a pull request
All four commands must pass before submitting a PR:
| Command | Requirement |
|---|---|
cargo test --all-targets |
All 602 tests pass (535 unit + 67 integration) |
cargo clippy --all-targets --all-features -- -D warnings |
Zero warnings (pedantic + nursery + cargo lints) |
cargo fmt -- --check |
Zero formatting violations |
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps |
Documentation builds without errors |
Tests that need the behavioral extension skip when it cannot be downloaded, so
a green run on an air-gapped machine does not prove they ran. CI sets
MALLARD_REQUIRE_BEHAVIORAL=1, which turns a skip into a failure; run it the
same way before touching any behavioral query:
MALLARD_REQUIRE_BEHAVIORAL=1 cargo test --all-targetsThe test suite drives the router in-process, which cannot catch a route that only fails once the binary is assembled and listening. Run the end-to-end checks too before changing a handler, a route, or the auth middleware:
cargo build && scripts/smoke-test.shNothing in the Rust suite executes the dashboard's JavaScript. node --check
sees only syntax, so scripts/check-dashboard-methods.mjs (which CI runs)
catches a handler wired to a method that no longer exists. For anything more —
a render crash, a fetch to a route that moved — drive it in a browser:
node scripts/check-dashboard-browser.mjs http://127.0.0.1:8000That one needs Playwright, which is why it is a local script rather than a CI job. Run it before shipping a dashboard change.
An empty dashboard exercises very little, so seed it first —
scripts/seed-demo-data.py writes 30 days of synthetic events with recurring
visitors, multi-step journeys, revenue and custom events, which is enough for
every panel to render something.
Pedantic, nursery, and cargo lint groups are all enabled. Zero warnings are tolerated. If clippy flags something that seems wrong, investigate before suppressing -- it is almost always correct.
Run cargo fmt before every commit. The CI pipeline will reject improperly formatted code.
- Always use parameterized queries (
$1,?) for user-provided values - Never interpolate user input into SQL strings via
format!()or string concatenation - The only exceptions are:
- Column names from fixed enums (not user input)
- Internal values from previous query results
- See
LESSONS.mdfor background on SQL safety
- Return meaningful error types (see
api/errors.rs) - Query functions that depend on the
behavioralextension must degrade gracefully when it is unavailable -- return defaults or empty results, not errors - Log errors with structured fields using
tracing
- Never store IP addresses in DuckDB or Parquet files
- IP addresses may only be used for HMAC hashing (visitor ID) and GeoIP lookup, then must be discarded
- See
SECURITY.mdfor the full privacy model
# All tests
cargo test
# Unit tests only
cargo test --lib
# Integration tests only
cargo test --test ingest_test
# A specific test
cargo test test_name
# With output
cargo test -- --nocapture
# Benchmarks (compilation check only)
cargo bench --no-run- Every public function needs tests covering:
- Happy path (normal usage)
- Edge cases (empty input, boundary values)
- Error cases (invalid input, missing dependencies)
- Integration tests go in
tests/ingest_test.rsand test the full HTTP path: JSON request -> handler -> buffer -> DuckDB -> HTTP response - Use
tower::ServiceExt::oneshot()for testing Axum routers without starting a real server - Test against real DuckDB output, not hand-written expectations (see
LESSONS.md) - Use
tempfile::TempDirfor test isolation -- never write to shared directories
When adding tests, update the test count in DEVELOPMENT.md by running:
# Count unit tests
cargo test --lib 2>&1 | grep "test result"
# Count integration tests
cargo test --test ingest_test 2>&1 | grep "test result"Benchmarks use Criterion.rs 0.8 and live in benches/ingest_bench.rs.
- 100 samples per benchmark minimum
- Run 3+ times before comparing results
- Report mean with 95% confidence intervals -- never a single number
- Improvements accepted only when confidence intervals do not overlap
- Document negative results with the same rigor as positive results
- One optimization per commit -- never batch multiple changes into one measurement
- No performance claims without measurement -- "not yet measured" is the default
# Full benchmark suite
cargo bench
# Compilation check only (faster, for CI)
cargo bench --no-runSee PERF.md for the full benchmark framework and current baselines.
Security-sensitive changes require extra care:
- Authentication (
api/auth.rs) -- Timing-safe comparisons, secure cookie attributes (HttpOnly, Secure, SameSite), session expiration - Password hashing -- Argon2id only, never store plaintext
- API keys -- SHA-256 hashed at rest,
mm_prefix for identification - Input validation -- Validate at the boundary (handler level) before data reaches the buffer or database
- SQL injection -- See SQL Safety above
- Dependencies -- All third-party GitHub Actions must be pinned to commit SHAs
If you discover a security vulnerability, see SECURITY.md for responsible disclosure.
Before submitting your PR, verify every item:
-
cargo test --all-targets-- all tests pass -
MALLARD_REQUIRE_BEHAVIORAL=1 cargo test --all-targets-- behavioral tests really ran -
scripts/smoke-test.sh-- the real binary still serves every route -
node scripts/check-dashboard-browser.mjs-- if the dashboard changed -
cargo clippy --all-targets --all-features -- -D warnings-- zero warnings -
cargo fmt -- --check-- zero formatting violations -
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps-- documentation builds without errors - New or changed functionality has corresponding tests
- No SQL injection vectors introduced (parameterized queries used)
- No PII stored (IP addresses only for hashing/GeoIP, then discarded)
- CHANGELOG.md updated with your changes
- Documentation updated if applicable (README.md, DEVELOPMENT.md)
- Open an issue on the GitHub repository for bugs or feature requests
- Read
LESSONS.mdfor common pitfalls and their solutions - Read
PERF.mdfor benchmark methodology - Read
ROADMAP.mdfor planned features and current status