Add feature-usage tracking and dashboard charts

Instruments FeatureEvaluationService with System.Diagnostics.Metrics (Counter + IMeterFactory). Accumulates per-feature/day evaluation counts in-process, flushed every 30s to a new FeatureUsageDaily Postgres table via PostgreSQL ON CONFLICT upsert. Caches dashboard query results for 180s in IMemoryCache. New GET /api/v1/environments/{id}/usage endpoint returns top-10 features last day and 14-day per-day breakdown. Admin dashboard shows two ApexCharts bar charts: top features and evaluations by day with per-flag hover tooltip.
This commit is contained in:
2026-06-17 12:45:56 -07:00
parent 5dcd39f564
commit 5179f5479c
25 changed files with 1828 additions and 3 deletions

View File

@@ -1,9 +1,11 @@
using System.Diagnostics.Metrics;
using MicCheck.Api.Data;
using MicCheck.Api.Features;
using MicCheck.Api.Identities;
using MicCheck.Api.Segments;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Moq;
using NUnit.Framework;
using AppEnvironment = MicCheck.Api.Environments.Environment;
@@ -14,6 +16,7 @@ public class FeatureEvaluationServiceTests
{
private MicCheckDbContext _db = null!;
private FeatureEvaluationService _service = null!;
private FeatureUsageMetrics _usageMetrics = null!;
private const int EnvironmentId = 1;
private const int ProjectId = 1;
@@ -25,14 +28,22 @@ public class FeatureEvaluationServiceTests
.Options;
_db = new MicCheckDbContext(options);
var meterFactory = new Mock<IMeterFactory>();
meterFactory.Setup(f => f.Create(It.IsAny<MeterOptions>())).Returns(new Meter("test"));
_usageMetrics = new FeatureUsageMetrics(meterFactory.Object);
var cache = new FlagCache(new MemoryCache(new MemoryCacheOptions()));
_service = new FeatureEvaluationService(_db, new SegmentEvaluator(), cache);
_service = new FeatureEvaluationService(_db, new SegmentEvaluator(), cache, _usageMetrics);
SeedBaseData();
}
[TearDown]
public void TearDown() => _db.Dispose();
public void TearDown()
{
_db.Dispose();
_usageMetrics.Dispose();
}
private void SeedBaseData()
{