17 KiB
AdsAnalyticsService - Service Documentation
1. Overview
Purpose: Ads Analytics microservice for the GoodGo Platform. Provides campaign performance metrics aggregation, custom report generation, breakdown analytics (by age/gender/device/placement), audience insights, performance recommendations, and admin-level platform-wide metrics.
Service Port: 5015 (Development, via launchSettings.json) / 8080 (Docker/Production)
Database: PostgreSQL (ads_analytics_service on Neon cloud)
- Connection string configured via
ConnectionStrings:DefaultConnectionorDATABASE_URLenv var - Current default points to Neon:
ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech
Framework: .NET 10.0 / C# 14
Architecture: Clean Architecture + CQRS (MediatR)
Solution Structure:
src/
AdsAnalyticsService.API/ # Web API + Application layer (CQRS handlers)
AdsAnalyticsService.Domain/ # Domain entities, SeedWork
AdsAnalyticsService.Infrastructure/ # EF Core DbContext, configs, migrations
tests/
AdsAnalyticsService.FunctionalTests/ # Integration tests (WebApplicationFactory)
AdsAnalyticsService.UnitTests/ # Unit tests (empty project, no test files yet)
2. API Endpoints
MetricsController
Base Route: api/v{version}/ads-analytics
| Method | Route | Description | Status |
|---|---|---|---|
GET |
campaigns/{id}/metrics?startDate&endDate |
Get aggregated campaign metrics for a date range. Defaults to last 30 days. Returns CampaignMetricsDto with impressions, clicks, conversions, spend, revenue, CTR, CPC, CPA, ROAS. |
Implemented |
GET |
adsets/{id}/metrics |
Get ad set metrics. | Placeholder (returns 501) |
GET |
ads/{id}/metrics |
Get ad metrics. | Placeholder (returns 501) |
ReportsController
Base Route: api/v{version}/ads-analytics/reports
| Method | Route | Description | Status |
|---|---|---|---|
GET |
/ |
List reports for an advertiser. Params: advertiserId (required), skip, take. Returns List<ReportListDto>. |
Implemented |
POST |
/ |
Create a new custom report. Body: CreateReportRequest. Query: advertiserId. Returns Guid (report ID). |
Implemented |
GET |
/{id} |
Get report by ID. | Placeholder (returns 501) |
POST |
/schedule |
Schedule a recurring report. Body: ScheduleReportRequest. |
Placeholder (returns 501) |
GET |
/{id}/export?format=csv |
Export report in given format. | Placeholder (returns 501) |
BreakdownController
Base Route: api/v{version}/ads-analytics/campaigns
| Method | Route | Description | Status |
|---|---|---|---|
GET |
/{id}/breakdown?by&startDate&endDate |
Get campaign breakdown by dimension. Valid by values: age, gender, device, placement. |
Mock data (returns hardcoded breakdown) |
InsightsController
Base Route: api/v{version}/ads-analytics/insights
| Method | Route | Description | Status |
|---|---|---|---|
GET |
/audience?campaignId&startDate&endDate |
Get audience insights (age, gender, location, user count, engagement rate). | Mock data |
GET |
/performance?advertiserId |
Get performance insights and recommendations (Low CTR, High CPA, Budget Underspend). | Mock data |
AdminMetricsController
Base Route: api/v{version}/admin/ads-analytics/metrics
| Method | Route | Description | Status |
|---|---|---|---|
GET |
/overview?startDate&endDate |
Platform-wide metrics overview (total impressions, clicks, spend, revenue, active campaigns/advertisers, avg CTR, avg ROAS). Defaults to last 30 days. | Implemented (queries real DB; active advertisers count is mocked) |
GET |
/top-campaigns?metric&limit |
Top performing campaigns. Valid metric values: spend, impressions, clicks, revenue, roas. Default limit: 10. |
Implemented (queries real DB) |
GET |
/anomalies |
Anomaly detection. | Placeholder (returns 501, planned for ML integration) |
AdminReportsController
Base Route: api/v{version}/admin/ads-analytics/reports
| Method | Route | Description | Status |
|---|---|---|---|
GET |
/ |
List all reports across all advertisers. Params: skip, take (default 50). |
Implemented |
DELETE |
/{id} |
Delete a report by ID (admin only). Returns 204 or 404. | Implemented |
Health Endpoints
| Method | Route | Description |
|---|---|---|
GET |
/health |
Full health check (includes PostgreSQL) |
GET |
/health/live |
Liveness probe (app is running, no dependency checks) |
GET |
/health/ready |
Readiness probe (includes PostgreSQL check) |
3. Commands
CreateReportCommand
- File:
src/AdsAnalyticsService.API/Application/Commands/CreateReportCommand.cs - Handler:
CreateReportCommandHandler.cs - Parameters:
AdvertiserId(Guid) - The advertiser who owns the reportName(string) - Report nameReportType(ReportType enum) - Campaign, AdSet, Ad, or AudienceStartDate(DateTime) - Report period startEndDate(DateTime) - Report period end
- Returns:
Guid(the newly created report's ID) - Behavior: Creates a new
Reportaggregate, persists it viaAdsAnalyticsServiceContext, and logs the creation. The report is created with statusPending. - Note: Handler injects
AdsAnalyticsServiceContextdirectly (no repository pattern used for this command).
4. Queries
GetCampaignMetricsQuery
- File:
src/AdsAnalyticsService.API/Application/Queries/GetCampaignMetricsQuery.cs - Handler:
GetCampaignMetricsQueryHandler.cs - Parameters:
CampaignId(Guid)StartDate(DateTime)EndDate(DateTime)
- Returns:
CampaignMetricsDto?(null if no metrics found)CampaignId,StartDate,EndDateTotalImpressions(long),TotalClicks(long),TotalConversions(long)TotalSpend(decimal),TotalRevenue(decimal)CTR(decimal, percentage),CPC(decimal),CPA(decimal),ROAS(decimal)
- Behavior: Queries
CampaignMetricstable filtered by campaign ID and date range, aggregates totals and calculates KPIs (CTR, CPC, CPA, ROAS).
GetReportsQuery
- File:
src/AdsAnalyticsService.API/Application/Queries/GetReportsQuery.cs - Handler: Inline in same file (
GetReportsQueryHandler) - Parameters:
AdvertiserId(Guid)Skip(int, default 0)Take(int, default 20)
- Returns:
List<ReportListDto>ordered byCreatedAtdescending- Each item:
Id,Name,ReportType,StartDate,EndDate,Status,CreatedAt
- Each item:
5. Domain Model
Aggregates
CampaignMetrics (Aggregate Root)
- File:
src/AdsAnalyticsService.Domain/AggregatesModel/MetricsAggregate/CampaignMetrics.cs - Extends:
Entity, implementsIAggregateRoot - Private Fields:
_campaignId,_date,_impressions,_clicks,_conversions,_spend,_revenue - Public Getters:
CampaignId,Date,Impressions,Clicks,Conversions,Spend,Revenue - Calculated Properties (not persisted):
CTR= clicks / impressions * 100CPC= spend / clicksCPA= spend / conversionsROAS= revenue / spend
- Behavior Methods:
RecordImpression()- Increments impressions counterRecordClick()- Increments clicks counterRecordConversion()- Increments conversions counterAddSpend(decimal amount)- Adds to spend totalAddRevenue(decimal amount)- Adds to revenue total
- Constructor:
CampaignMetrics(Guid campaignId, DateTime date)- Creates new metrics record, normalizes date to start of day
Report (Aggregate Root)
- File:
src/AdsAnalyticsService.Domain/AggregatesModel/ReportAggregate/Report.cs - Extends:
Entity, implementsIAggregateRoot - Private Fields:
_advertiserId,_name,_reportType,_startDate,_endDate,_status,_dataJson - Public Getters:
AdvertiserId,Name,ReportType,StartDate,EndDate,Status,DataJson,CreatedAt - Behavior Methods:
MarkAsProcessing()- Sets status to ProcessingComplete(string dataJson)- Sets data and status to CompletedFail()- Sets status to Failed
- Constructor:
Report(Guid advertiserId, string name, ReportType reportType, DateTime startDate, DateTime endDate)- Creates report with statusPendingandCreatedAt = DateTime.UtcNow
Enumerations
ReportType (enum)
Campaign = 1AdSet = 2Ad = 3Audience = 4
ReportStatus (enum)
Pending = 1Processing = 2Completed = 3Failed = 4
SeedWork (Base Classes)
- Entity: Base class with
Id(Guid),DomainEvents(IReadOnlyCollection), equality by ID - IAggregateRoot: Marker interface
- IRepository: Generic repository interface with
UnitOfWorkproperty - IUnitOfWork:
SaveChangesAsync(),SaveEntitiesAsync()(with domain event dispatch) - ValueObject: Immutable value comparison base class
- Enumeration: Type-safe enum pattern (not used by current aggregates)
Exceptions
- DomainException: Base domain exception class
- AdsAnalyticsDomainException: Service-specific domain exception (extends Exception, not DomainException)
6. Database Schema
Database: ads_analytics_service (PostgreSQL)
Migration: 20260117181438_InitialCreate
Table: campaign_metrics
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
uuid | NOT NULL | - | Primary key |
campaign_id |
uuid | NOT NULL | - | Reference to campaign (no FK constraint) |
date |
date | NOT NULL | - | Daily aggregation date |
impressions |
bigint | NOT NULL | 0 | Number of ad impressions |
clicks |
bigint | NOT NULL | 0 | Number of ad clicks |
conversions |
bigint | NOT NULL | 0 | Number of conversions |
spend |
decimal(18,2) | NOT NULL | 0 | Ad spend amount |
revenue |
decimal(18,2) | NOT NULL | 0 | Revenue generated |
Indexes:
PK_campaign_metrics- Primary key onididx_campaign_metrics_campaign_id- Index oncampaign_ididx_campaign_metrics_campaign_date- Unique composite index on (campaign_id,date) - enforces one metrics record per campaign per day
Table: reports
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
uuid | NOT NULL | - | Primary key |
advertiser_id |
uuid | NOT NULL | - | Reference to advertiser (no FK constraint) |
name |
varchar(200) | NOT NULL | - | Report name |
report_type |
integer | NOT NULL | - | Enum: 1=Campaign, 2=AdSet, 3=Ad, 4=Audience |
start_date |
date | NOT NULL | - | Report period start |
end_date |
date | NOT NULL | - | Report period end |
status |
integer | NOT NULL | - | Enum: 1=Pending, 2=Processing, 3=Completed, 4=Failed |
data_json |
jsonb | NULL | - | Report data stored as JSON |
created_at |
timestamp with time zone | NOT NULL | - | Creation timestamp |
Indexes:
PK_reports- Primary key onididx_reports_advertiser_id- Index onadvertiser_ididx_reports_status- Index onstatusidx_reports_created_at- Index oncreated_at
Idempotency: ClientRequest (entity exists but no EF configuration/table migration)
Id(Guid),Name(string),Time(DateTime)- Used via
IRequestManager/RequestManagerbut no corresponding table is created in the migration -- this will fail at runtime if invoked.
7. Integration Events
No integration events are defined. The service does not publish or consume any cross-service events via RabbitMQ or any other message broker.
No domain events are raised by either aggregate. The CampaignMetrics and Report entities have domain event support inherited from the Entity base class, but neither constructor nor behavior method calls AddDomainEvent().
8. Dependencies
NuGet Packages
API Layer (AdsAnalyticsService.API):
| Package | Version |
|---|---|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.2 |
| Swashbuckle.AspNetCore | 7.2.0 |
| Asp.Versioning.Mvc | 8.1.0 |
| Asp.Versioning.Mvc.ApiExplorer | 8.1.0 |
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
| AspNetCore.HealthChecks.Redis | 8.0.1 |
| Hellang.Middleware.ProblemDetails | 6.5.1 |
| Serilog.AspNetCore | 8.0.3 |
| Serilog.Sinks.Console | 6.0.0 |
| Serilog.Sinks.Seq | 8.0.0 |
Domain Layer (AdsAnalyticsService.Domain):
| Package | Version |
|---|---|
| MediatR.Contracts | 2.0.1 |
Infrastructure Layer (AdsAnalyticsService.Infrastructure):
| Package | Version |
|---|---|
| Microsoft.EntityFrameworkCore | 10.0.0 |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.0 |
| MediatR | 12.4.1 |
| Dapper | 2.1.35 |
| Microsoft.Extensions.Http.Polly | 9.0.0 |
| Polly | 8.5.0 |
| StackExchange.Redis | 2.8.16 |
Test Projects:
| Package | Version | Project |
|---|---|---|
| xunit | 2.9.2 | Both |
| FluentAssertions | 6.12.2 | Both |
| Moq | 4.20.72 | UnitTests only |
| Microsoft.AspNetCore.Mvc.Testing | 10.0.0 | FunctionalTests |
| Microsoft.EntityFrameworkCore.InMemory | 10.0.0 | FunctionalTests |
| Testcontainers.PostgreSql | 4.1.0 | FunctionalTests |
| coverlet.collector | 6.0.2 | Both |
Global Build Properties (Directory.Build.props):
net10.0,LangVersion 14.0,Nullable enable,TreatWarningsAsErrors trueMicrosoft.SourceLink.GitHub 8.0.0
External Service Dependencies
- PostgreSQL (Neon cloud) - Primary data store
- Redis - Configured in appsettings (
localhost:6379) but not used in any code (StackExchange.Redis is a NuGet dependency only) - No other service dependencies - This service does not call any other microservices
9. Configuration
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "postgresql://...@neon.tech/ads_analytics_service?sslmode=require"
},
"Redis": {
"ConnectionString": "localhost:6379"
},
"Jwt": {
"Secret": "your-super-secret-key-min-32-characters",
"Issuer": "goodgo-platform",
"Audience": "goodgo-services",
"AccessTokenExpiryMinutes": 15,
"RefreshTokenExpiryDays": 7
}
}
Environment Variables (from .env.example)
| Variable | Description | Default |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Runtime environment | Development |
DATABASE_URL |
PostgreSQL connection string (fallback) | localhost:5432 |
REDIS_URL |
Redis connection string | localhost:6379 |
REDIS_PASSWORD |
Redis password | (empty) |
JWT_SECRET |
JWT signing key | - |
JWT_ISSUER |
JWT issuer | goodgo-platform |
JWT_AUDIENCE |
JWT audience | goodgo-services |
API_PORT |
API port | 5000 |
LOG_LEVEL |
Serilog minimum level | Information |
SEQ_URL |
Seq logging endpoint | http://localhost:5341 |
Connection String Resolution Order
ConnectionStrings:DefaultConnectionfrom appsettingsDATABASE_URLenvironment variable (fallback)
MediatR Pipeline (order)
LoggingBehavior- Logs request name, elapsed time, errors (with Stopwatch)ValidatorBehavior- Runs FluentValidation validators (throwsValidationExceptionon failure)TransactionBehavior- Wraps Commands in DB transaction (skips Queries by name suffix check); usesExecutionStrategyfor retry
Docker Configuration
- Multi-stage build:
sdk:10.0(build) ->aspnet:10.0(runtime) - Non-root user:
dotnetuser(UID/GID 1001) - Exposed port: 8080
- Health check:
curl -f http://localhost:8080/health/live(30s interval, 3 retries)
Startup Behavior
- Auto-applies EF Core migrations on startup (catches and logs errors without crashing)
- Swagger UI available at
/swaggerin Development environment - CORS: Allow any origin/method/header (open policy)
10. Notable Gaps & Observations
- No authentication/authorization: No
[Authorize]attributes on any controller. JWT config exists in appsettings but is not wired up inProgram.cs. - No FluentValidation validators defined:
ValidatorBehavioris registered but noAbstractValidator<T>implementations exist for any command. - No repository interfaces/implementations: Both
CreateReportCommandHandlerand query handlers injectAdsAnalyticsServiceContextdirectly instead of using the repository pattern defined in SeedWork. - Mock data endpoints: BreakdownController and InsightsController return hardcoded mock data, not from the database.
- ClientRequest table missing:
IRequestManager/RequestManagerare registered in DI but theClientRequestentity has no EF configuration and no migration creates its table. - Redis not wired: Redis NuGet package is included and connection string is configured, but no Redis usage exists in the codebase.
- Dapper not used: Dapper NuGet package is included but never referenced in any query handler.
- No unit tests: The
AdsAnalyticsService.UnitTestsproject exists but contains no test files. - Domain events unused: Neither aggregate raises domain events despite inheriting the capability.
- No API response wrapper: Controllers return raw DTOs or anonymous objects, not the standard
{ success: bool, data: T }format documented in CLAUDE.md.