Skip to content

imann128/DevMetrics

Repository files navigation

DevMetrics

CI Version Docker

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.


What's New in v1.1.0

  • 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

Dashboard

DevMetrics dashboard showing commit activity, stat cards, and tracked repositories

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.


Features

  • 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 .git directories for immediate activity detection
  • REST API with Swagger UI for programmatic access
  • Health-check endpoints (/health, /health/live, /health/ready)

How DevMetrics Measures Developer Craftsmanship

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.

Code Quality Signals

Code Quality panel showing language distribution, metric cards, and top churn files

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.

Burst Analysis

Focus Bursts panel showing burst summary cards and top burst sessions

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.

Craftsmanship Comparison

Assigning labels to repositories in the tracked repositories table

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.

Craftsmanship comparison showing side-by-side metrics for Friend and Mine groups

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).


Why DevMetrics Is Relevant? Mapping it to Data Analytics, Telemetry, and Closed-Loop Systems

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.


Prerequisites

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

Quick Start

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/devmetrics

Mac / Linux:

docker run -p 5000:80 -v /home/username/projects/your-repo:/repos/my-repo imann122/devmetrics

Then 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.


Clone the Repository

git clone https://github.com/imann128/DevMetrics.git
cd DevMetrics

To 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:ro

The 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 --build

Or 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/swagger

Configuration

All settings live in appsettings.json.

Adding a repository

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.yml has:

- C:\Users\Hp\Documents\ogdc-project:/repos/ogdc-project:ro

then enter /repos/ogdc-project in 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"}'

Triggering your first scan

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/trigger

The 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.

Scan schedule

"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

Email reports

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:5000

Or 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.


Telemetry & Closed-Loop Features

Quality snapshot history

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.

History endpoint returning a JSON array of quality snapshots

Prometheus metrics

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.

Prometheus metrics endpoint showing per-repository gauges

Threshold alerting

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=500

Email 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.


API Security

API key authentication

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 32

Copy 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: Development

Then 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 ProblemDetails body, consistent with ErrorHandlingMiddleware. Every error response in the API has the same shape, including auth failures.

Rate limiting

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.

Prometheus and health endpoint exposure

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.


API Documentation

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

Testing

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:Html

Tests

DevMetrics 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.

Tests

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:Html

Coverage Report

The 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.


EF Core Migrations

# 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.Api

Troubleshooting

SQLite database locked

Symptom: 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.

Git repository not found

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.

LibGit2Sharp native library fails to load

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 (not dotnet build) β€” publish copies native binaries.
  • If running on ARM64, add --runtime linux-arm64 to the publish command.

Emails not sending

Symptom: Weekly report isn't arriving; no errors in logs.

Causes / fixes:

  1. Email__Enabled is false (the default) β€” set it to true.
  2. Email__Recipients is empty β€” add at least one address.
  3. Gmail requires an App Password, not your account password.
  4. Check the application logs for Email | prefixed entries for detailed SMTP errors.

Background service shows Degraded in health check

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}.


Architecture Overview

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.


About

A developer productivity dashboard that tracks local Git activity in real time β€” built with .NET 8, Clean Architecture, SignalR, and EF Core.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors