DevMetrics is a self-hosted developer productivity dashboard that tracks local Git activity in real time. It scans your Git repositories, aggregates commit history and diff statistics, and surfaces craftsmanship metrics, focus burst analysis, and code quality signals through a live web dashboard with automatic updates via SignalR.
- Craftsmanship comparison β label repositories (e.g. "Mine", "Friend") and compare groups side-by-side across all quality metrics
- Burst analysis β detects deep-work focus sessions from commit timing patterns
- Code quality snapshots β fix-commit ratio, message depth, test coverage ratio, and top churn files per repository
- Language distribution β file-level language breakdown across your tracked repositories
- Inline label editing β assign owner labels directly from the repository table without leaving the dashboard
- Clean-room migration β database schema now includes all columns in the initial migration; no incremental migration debt
The dashboard gives you a live view of commit activity across all tracked repositories β total commits, lines added and deleted, active days, current streak, and peak day β all updated in real time via SignalR whenever a scan completes.
- Scans local Git repositories on a configurable cron schedule (default: hourly)
- Tracks commits per day, lines added/deleted, and files changed
- Stores history in an embedded SQLite database via EF Core
- Real-time dashboard updates pushed via WebSocket (SignalR)
- Weekly email productivity reports via MailKit/SMTP
- Watches repository
.gitdirectories for immediate activity detection - REST API with Swagger UI for programmatic access
- Health-check endpoints (
/health,/health/live,/health/ready)
DevMetrics treats your Git history as a structured event stream and derives four distinct signal families from it. Each signal is designed to be objective that is computed purely from repository data with no manual input and is updated automatically on every scan cycle.
Every scan computes a RepoQualitySnapshot for each tracked repository. The snapshot contains:
Fix-commit ratio measures what fraction of commits begin with a conventional fix prefix (fix:, bugfix:, hotfix:, patch:, revert:). A codebase where more than 30β40% of commits are fixes is telling you something: features are shipping before they are stable, or the feedback loop from testing to code is too slow. A low fix ratio is not inherently good either: it may mean bugs are being shipped without labels but tracking it over time reveals trend direction.
Average message length uses commit message length as a proxy for intentionality. Short messages (fix, wip, asdf) indicate commits made in a hurry or without a clear mental model of the change. Longer messages that describe why a change was made, not just what, are a signature of developers who think in units of reasoning rather than units of keystrokes. DevMetrics uses 40 characters as a rough floor for "deliberate" messages.
Test file ratio computes the proportion of source-code files (excluding config, markup, and data formats) that match common test naming conventions (.test., .spec., _test., tests.). This is a structural signal, not a coverage signal β it measures whether testing is a first-class practice in the codebase rather than an afterthought.
Average commit size is the mean lines-changed (added + deleted) per commit. Large commits are harder to review, harder to bisect, and harder to revert cleanly. Consistently small commits indicate that work is being decomposed thoughtfully before it is written, not after.
DevMetrics analyzes commit timestamps to detect focus bursts: sequences of commits from the same author where the gap between any two consecutive commits does not exceed two hours. A burst of two or more commits within that window is recorded as a single session.
From this the system derives:
- Total bursts β how many focused sessions occurred across the repository's history
- Average burst size β commits per session; larger bursts indicate more sustained output per sitting
- Average burst duration β how long sessions last in minutes; longer durations suggest deeper immersion
- Total commits in bursts β the fraction of all commits that occurred inside a burst rather than in isolation
This matters because isolated commits i.e, one commit, then silence, then another hours later and burst commits look identical in a daily totals chart but represent fundamentally different ways of working. Burst patterns correlate with uninterrupted focus time, which research consistently links to higher-quality output on complex problems.
To enable comparison, assign a free-text label to each repository directly from the Tracked Repositories table on the dashboard. Click the dash (β) in the Label column, type a name β anything like Mine, Friend, Work, or a team member's name and press Enter. Each repository can have one label. Repositories without a label are excluded from the comparison view.
Once at least two different labels exist, the Craftsmanship Comparison section at the bottom of the dashboard populates automatically. DevMetrics groups repositories by label and computes commit-weighted averages of all quality metrics per group β a repository with 500 commits contributes proportionally more than one with 20, so the comparison reflects actual output rather than repository count. A WIN badge highlights the stronger group per metric (lower fix ratio wins, higher message depth and test signal win, burst duration higher is better, commit size is shown for context with no directional preference).
DevMetrics is itself a telemetry pipeline. The architecture maps directly onto patterns used in industrial control systems and data engineering platforms, just applied to developer behavior instead of physical processes.
Observation layer. LibGit2Sharp reads the Git object store β a compact, append-only event log and extracts structured events: commits with timestamps, authors, diff sizes, and messages. This is equivalent to reading from a sensor or an event bus. The data is already structured; DevMetrics just knows how to query it.
Processing layer. The scan handler aggregates raw commit events into daily summaries, quality snapshots, and burst sessions. This is a batch ETL step: transform raw events into pre-aggregated facts that are cheap to query at dashboard time. The same pattern appears in any analytical pipeline i.e, raw events are expensive to query repeatedly, so you materialize the aggregates that are critical.
Feedback layer. The SignalR hub pushes updated metrics to connected dashboards in real time after each scan. The weekly email report delivers a digest on a schedule. These are the output channels of the system β the equivalent of an alert, a report, or a control signal in a closed-loop architecture.
Closed-loop relevance. A closed-loop system observes a process, computes a signal, and feeds that signal back to influence the process. DevMetrics closes the loop on developer workflow: you write code (process) β DevMetrics measures craftsmanship signals (observe) β the dashboard and weekly report surface patterns (feedback) β you adjust how you work (influence). The fix-commit ratio going up over two weeks is a signal that something upstream changed β a new dependency, a rushed sprint, a gap in testing. Without measurement, that pattern is invisible until it becomes a problem.
Extensibility. Because the core data model is a structured SQLite database with a REST API on top, DevMetrics can serve as a data source for external analytics tools. The /api/dashboard/summary endpoint returns time-series commit data that can be ingested directly into Grafana, Power BI, or any analytics platform that consumes JSON. The health endpoints are compatible with Prometheus scraping and Uptime Kuma monitoring out of the box.
| Dependency | Version | Notes |
|---|---|---|
| .NET SDK | 8.0+ | Required for dotnet run and dotnet build |
| Docker Desktop | Any | Required for the Docker quick start |
| Git | Any | Repos must be valid Git repositories |
Make sure Docker Desktop is installed and running, then substitute your repository path:
Windows:
docker run -p 5000:80 -v "C:\Users\YourName\Projects\your-repo:/repos/my-repo" imann122/devmetricsMac / Linux:
docker run -p 5000:80 -v /home/username/projects/your-repo:/repos/my-repo imann122/devmetricsThen open http://localhost:5000, click Add Repository, enter /repos/my-repo, and click Scan Now.
The image is published on Docker Hub at imann122/devmetrics.
git clone https://github.com/imann128/DevMetrics.git
cd DevMetricsTo run from source, add your repository path to the volumes section of docker-compose.yml:
volumes:
- devmetrics-data:/app/Data
- devmetrics-logs:/app/Logs
- C:\Users\YourName\Projects\your-repo:/repos/your-repo:ro
# Add one line per repository, for example:
# - C:\Users\Hp\Documents\ogdc-project:/repos/ogdc-project:roThe path before the : is the folder on your machine. The path after is where it appears inside the container. This is what you enter in the Add Repository form. The :ro flag makes it read-only so DevMetrics cannot modify your source code.
Then start the container:
docker compose up -d --buildOr run locally without Docker:
# Apply migrations
dotnet ef database update \
--project DevMetrics.Infrastructure \
--startup-project DevMetrics.Api
# Start the API
dotnet run --project DevMetrics.Api
# Open the dashboard
# http://localhost:5000
# Swagger UI: http://localhost:5000/swaggerAll settings live in appsettings.json.
Via the web dashboard (easiest): open http://localhost:5000, type the path in the Add Repository card, and click Track Repository.
Important β Docker uses Linux paths. The path you enter must be the container-side path from your volume mount, not the Windows path on your machine. If your
docker-compose.ymlhas:- C:\Users\Hp\Documents\ogdc-project:/repos/ogdc-project:rothen enter
/repos/ogdc-projectin the Add Repository form β that is the path DevMetrics sees inside the container.
Via the REST API:
curl -X POST http://localhost:5000/api/repositories \
-H "Content-Type: application/json" \
-d '{"path": "/repos/ogdc-project"}'After adding a repository the dashboard will be empty until the next hourly cron tick. To populate it immediately, trigger a manual scan:
From the dashboard: click the Scan Now button.
From Swagger UI: open http://localhost:5000/swagger, find POST /api/scan/trigger, and click Execute.
From the command line:
curl -X POST http://localhost:5000/api/scan/triggerThe scan runs asynchronously. The dashboard updates automatically via SignalR when it completes β no page refresh needed. For large repositories with deep history, expect it to take up to a minute or two.
"CronExpressions": {
"HourlyScan": "0 * * * *",
"WeeklyReport": "0 9 * * 1"
}POSIX cron format: minute hour day-of-month month day-of-week.
| Expression | Meaning |
|---|---|
0 * * * * |
Every hour at minute 0 |
*/15 * * * * |
Every 15 minutes |
0 9 * * 1 |
Monday at 09:00 UTC |
0 8 * * 1-5 |
Weekdays at 08:00 UTC |
DevMetrics sends a weekly productivity summary on a cron schedule (default: Monday 09:00 UTC). To enable it you need a Gmail App Password β a one-time setup that takes about two minutes.
Step 1 β Enable 2-Step Verification on your Google account Go to myaccount.google.com β Security β 2-Step Verification and turn it on if it isn't already. App Passwords are not available without it.
Step 2 β Create an App Password for DevMetrics
Go to myaccount.google.com/apppasswords. Under App name type DevMetrics and click Create. Google will show you a 16-character password β copy it, you won't see it again.
Step 3 β Configure DevMetrics
Add the following to your docker-compose.yml under environment, or set them as environment variables:
Email__Enabled=true
Email__Host=smtp.gmail.com
Email__Port=587
Email__Username=you@gmail.com
Email__Password=your-16-char-app-password
Email__FromAddress=you@gmail.com
Email__FromName=DevMetrics
Email__Recipients__0=you@gmail.com
Email__DashboardBaseUrl=http://localhost:5000Or in appsettings.json (do not commit credentials β use environment variables in production):
"Email": {
"Enabled": true,
"Host": "smtp.gmail.com",
"Port": 587,
"Username": "you@gmail.com",
"Password": "your-16-char-app-password",
"FromAddress": "you@gmail.com",
"FromName": "DevMetrics",
"UseSsl": false,
"Recipients": ["you@gmail.com"],
"DashboardBaseUrl": "http://localhost:5000"
}Use Port: 587 with UseSsl: false β Gmail uses STARTTLS on port 587, not SSL on 465.
Every scan appends a new timestamped quality snapshot rather than overwriting the previous one, giving you a full time-series record of how your metrics evolve. To query it, first get your repository's ID from http://localhost:5000/api/repositories β look for the id field in the JSON response, which is a UUID like 550e8400-e29b-41d4-a716-446655440000. Then open:
http://localhost:5000/api/quality/{repoId}/history
You'll get an array of data points in chronological order, one per scan cycle, each with computedAtUtc, fixCommitRatio, avgMessageLength, testFileRatio, avgCommitSize, and totalCommitsAnalyzed. After running several scans over days or weeks you can plot these to see whether your craftsmanship signals are improving or degrading over time.
DevMetrics exposes a standard Prometheus metrics endpoint at:
http://localhost:5000/metrics
Open it in a browser and you'll see plain-text gauge readings like:
devmetrics_active_repositories 2
devmetrics_fix_commit_ratio{repository="ogdc-project"} 0.0
devmetrics_test_file_ratio{repository="ogdc-project"} 0.0
devmetrics_avg_commit_size_lines{repository="ogdc-project"} 1914
devmetrics_focus_bursts_total{repository="ogdc-project"} 1
These are refreshed every 5 minutes. Any Prometheus-compatible monitoring stack β Grafana, Datadog, Uptime Kuma β can scrape this endpoint directly without any additional configuration.
DevMetrics can automatically send an alert email when a repository's quality metrics breach configurable thresholds after a scan β this is the actuator that closes the feedback loop. To enable it, set the following in docker-compose.yml under environment:
QualityThresholds__Enabled=true
QualityThresholds__MaxFixRatio=0.40
QualityThresholds__MinAvgMessageLength=20
QualityThresholds__MinTestRatio=0.05
QualityThresholds__MaxAvgCommitSize=500Email must also be configured and enabled (see the Email reports section). When a threshold is breached, a consolidated alert email is sent listing every violation β for example "ogdc-project: fix-commit ratio 45% exceeds threshold 40%". Adjust the threshold values to match your team's standards.
All /api/* routes are protected by API key authentication via the X-Api-Key request header. Authentication is opt-in β if no key is configured the middleware is disabled and all requests pass through, preserving zero-config local development.
Generating a key
An API key is a secret string you generate yourself. Use PowerShell to generate a cryptographically random one:
$bytes = New-Object Byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
[System.Convert]::ToBase64String($bytes)Or on Mac/Linux:
openssl rand -base64 32Copy the output β that is your API key. Never commit it to Git.
Enabling it
Set the key in docker-compose.yml:
ApiKey__Key: "your-secret-key-here"Or in appsettings.json (use environment variables in production β do not commit a real key):
"ApiKey": {
"Key": "your-secret-key-here"
}Once enabled, every API call must include the header:
curl http://localhost:5000/api/repositories -H "X-Api-Key: your-secret-key-here"PowerShell:
Invoke-WebRequest http://localhost:5000/api/repositories -Headers @{"X-Api-Key" = "your-secret-key-here"}Swagger UI (easiest): Swagger UI is only available when the container is running in Development or Staging mode. By default docker-compose.yml sets ASPNETCORE_ENVIRONMENT: Production, which disables Swagger intentionally to avoid exposing API documentation in production. To enable it for local testing, change the environment to Development in docker-compose.yml:
ASPNETCORE_ENVIRONMENT: DevelopmentThen restart the container with docker compose restart and open http://localhost:5000/swagger. Once there, click the Authorize button at the top right, enter your API key, and every request made from Swagger will include the X-Api-Key header automatically. Remember to switch back to Production before pushing to Docker Hub.
Requests missing the header or supplying the wrong key receive 401 Unauthorized with a standard ProblemDetails body.
Design decisions:
- The dashboard UI, health endpoints, Prometheus metrics, Swagger UI, and SignalR hub are intentionally excluded from authentication. Requiring an API key to view the dashboard would break the core use case of the tool β authentication belongs on the programmatic API surface, not the human-facing UI.
- Authentication is disabled by default when no key is configured. The middleware exits immediately without checking the header, so the container works out of the box with zero config. You opt into security when you need it β the right default for a self-hosted tool.
- Rejections return a
ProblemDetailsbody, consistent withErrorHandlingMiddleware. Every error response in the API has the same shape, including auth failures.
POST /api/scan/trigger is rate-limited to 1 request per 30 seconds per IP address using ASP.NET Core's built-in fixed-window rate limiter. Exceeding the limit returns 429 Too Many Requests immediately β the request is not queued. All other endpoints are unaffected.
This protects the most expensive operation in the system β a full repository scan that walks Git history, recomputes quality snapshots, and writes to the database β from being called in rapid succession.
In the default Docker setup, /metrics and /health are accessible on localhost:5000. For any deployment exposed beyond localhost, restrict these paths at the reverse proxy layer:
location /metrics { allow 127.0.0.1; deny all; }
location /health { allow 127.0.0.1; deny all; }This keeps telemetry and health data internal to the host while the dashboard remains publicly accessible.
Swagger UI is available at http://localhost:5000/swagger in Development mode.
| Endpoint | Method | Description |
|---|---|---|
/api/repositories |
GET | List all tracked repositories |
/api/repositories |
POST | Add a repository by path |
/api/repositories/{id} |
DELETE | Remove a repository and all its history |
/api/repositories/{id}/label |
PATCH | Set or clear the owner label for a repository |
/api/dashboard/summary |
GET | Aggregated daily stats (default: 14 days) |
/api/dashboard/health |
GET | DB connectivity + last scan timestamp |
/api/scan/trigger |
POST | Manually trigger an immediate scan (async 202) |
/api/scan/status/{operationId} |
GET | Poll the status of a triggered scan |
/api/quality/{repoId} |
GET | Quality snapshot for a single repository |
/api/burst/{repoId} |
GET | Burst analysis snapshot for a single repository |
/api/comparison |
GET | Commit-weighted metrics grouped by owner label |
/health |
GET | Combined health (database + git + background) |
/health/live |
GET | Liveness probe (is the process alive?) |
/health/ready |
GET | Readiness probe (can it serve traffic?) |
SignalR hub: ws://localhost:5000/dashboardHub
| Event | Payload | When |
|---|---|---|
ScanCompleted |
ScanResultDto |
After every scheduled scan |
DashboardUpdated |
DashboardDataDto |
After a manually triggered scan |
RepositoryActivityDetected |
{ repositoryPath, repositoryName } |
On .git directory change |
Bash / Linux / macOS:
# Run all tests
dotnet test
# Unit tests only (fast, no DB)
dotnet test --filter "Category!=Integration"
# Integration tests only
dotnet test --filter "FullyQualifiedName~Integration"
# With coverage report
dotnet test --collect:"XPlat Code Coverage" --settings DevMetrics.Tests/DevMetrics.runsettings --results-directory ./TestResults
# Generate HTML coverage report (requires reportgenerator)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"./TestResults/**/coverage.cobertura.xml" -targetdir:"./CoverageReport" -reporttypes:HtmlDevMetrics has 45 automated tests split across two categories. Unit tests cover the Application layer β the command and query handlers that contain the core business logic. Each handler is tested in isolation using mocked dependencies (Moq), so the tests run fast and don't touch the database or file system. The unit tests verify things like: duplicate repository detection, the commit ingestion pipeline, message backfill logic, and quality metric computation on quiet repositories with no recent commits. Integration tests cover the API layer using ASP.NET Core's WebApplicationFactory, which spins up the full application in memory against a real SQLite database. These tests make actual HTTP calls to the controllers and verify end-to-end behaviour β adding a repository, querying the dashboard, SignalR hub connectivity. The integration test database is reset between tests using Respawn, which deletes all rows without dropping the schema, keeping test isolation fast and reliable.
PowerShell (Windows):
# Run all tests
dotnet test
# Unit tests only (fast, no DB)
dotnet test --filter "Category!=Integration"
# Integration tests only
dotnet test --filter "FullyQualifiedName~Integration"
# With coverage report
dotnet test --collect:"XPlat Code Coverage" --settings DevMetrics.Tests/DevMetrics.runsettings --results-directory ./TestResults
# Install reportgenerator (first time only)
dotnet tool install -g dotnet-reportgenerator-globaltool
# Generate HTML coverage report β then open ./CoverageReport/index.html
reportgenerator -reports:"./TestResults/**/coverage.cobertura.xml" -targetdir:"./CoverageReport" -reporttypes:HtmlThe coverage report is generated by Coverlet, which instruments the compiled assemblies during the test run and produces a Cobertura XML file. ReportGenerator then converts that XML into a browsable HTML report showing line-by-line coverage per class. The report excludes EF Core migrations, auto-generated code, and the test project itself β only the four production projects (Core, Application, Infrastructure, Api) are measured. This gives an honest picture of how much of the actual business logic is exercised by the test suite.
# Create a new migration after changing entities
dotnet ef migrations add YourMigrationName \
--project DevMetrics.Infrastructure \
--startup-project DevMetrics.Api
# Apply pending migrations
dotnet ef database update \
--project DevMetrics.Infrastructure \
--startup-project DevMetrics.Api
# View applied migrations
dotnet ef migrations list \
--project DevMetrics.Infrastructure \
--startup-project DevMetrics.ApiSymptom: SqliteException: database is locked
Cause: Two processes (e.g., two dotnet run instances) are writing to the same .db file simultaneously.
Fix: Stop all instances except one. SQLite is a single-writer database β DevMetrics is designed to run as a single process. For multi-instance deployments, migrate to PostgreSQL by replacing the SQLite provider.
Symptom: ArgumentException: 'path' does not contain a valid Git repository
Cause: The path passed to Add Repository doesn't contain a .git folder, or the path is a bare clone.
Fix: Verify with ls /your/path/.git. Bare clones are supported if the repo root contains HEAD and an objects/ directory directly.
Symptom: GitServiceHealthCheck reports Unhealthy immediately after startup.
Cause: The libgit2 native binary for your OS/architecture is missing from the publish output.
Fix:
- Ensure you published with
dotnet publish(notdotnet build) β publish copies native binaries. - If running on ARM64, add
--runtime linux-arm64to the publish command.
Symptom: Weekly report isn't arriving; no errors in logs.
Causes / fixes:
Email__Enabledisfalse(the default) β set it totrue.Email__Recipientsis empty β add at least one address.- Gmail requires an App Password, not your account password.
- Check the application logs for
Email |prefixed entries for detailed SMTP errors.
Symptom: /health returns Degraded for background-scan.
Cause: The last scan cycle reported Failed or PartialFailure (e.g., a repository path no longer exists).
Fix: Check the logs for ScanService | prefixed entries. Remove repositories whose paths are gone: DELETE /api/repositories/{id}.
DevMetrics.Api β ASP.NET Core Web API + Razor Pages + SignalR Hub
DevMetrics.Application β MediatR Commands/Queries + Background Services + Email
DevMetrics.Infrastructure β EF Core + SQLite + LibGit2Sharp (GitService)
DevMetrics.Core β Entities + Interfaces + DTOs (no dependencies)
DevMetrics.Tests β xUnit + Moq + FluentAssertions + WebApplicationFactory
The dependency rule flows strictly inward: Api β Application β Core β Infrastructure.







