20 KiB
AdsTrackingService - Service Documentation
1. Overview
Purpose: Ads event tracking microservice for the GoodGo advertising platform. Handles tracking pixel management, client-side and server-side event tracking, conversion recording, and multi-model attribution analytics.
Port: 5014 (Development, via launchSettings.json), 8080 (Docker/Production)
Database: PostgreSQL — ads_tracking_service on Neon (ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech)
Schema: ads_tracking
Framework: .NET 10.0, C# 14
Architecture: Clean Architecture (API / Domain / Infrastructure) + CQRS via MediatR
Migration: Single migration 20260117182759_InitialCreate. Auto-applied on startup via dbContext.Database.MigrateAsync().
2. API Endpoints
PixelsController — api/v1/ads-tracking/pixels
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | /{advertiserId:guid} |
Get pixel code for an advertiser | Path: advertiserId (Guid) |
PixelCodeDto (200) or 404 |
| POST | / |
Create a new tracking pixel | Body: CreatePixelRequest { AdvertiserId } |
TrackingPixelResult (201) or 400 |
EventsController — api/v1/ads-tracking/events
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| POST | / |
Track a client-side pixel event | Body: TrackPixelEventRequest { PixelCode, AdId, UserId, EventType } |
202 Accepted or 400 |
| POST | /server |
Track a server-side event (no pixel required) | Body: TrackServerSideEventRequest { AdId, UserId, EventType } |
202 Accepted or 400 |
ConversionsController — api/v1/ads-tracking/conversions
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | / |
Get conversions with optional filtering | Query: campaignId?, userId?, from?, to?, skip=0, take=20 |
IEnumerable<ConversionDto> (200) |
| GET | /{id:guid}/attribution |
Get attribution details for a conversion | Path: id (Guid) |
AttributionDto (200) or 404 |
AdminPixelsController — api/v1/admin/ads-tracking/pixels
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | / |
List all tracking pixels (paginated) | Query: page=1, pageSize=20, isActive? |
List<PixelListDto> (200) — MOCK |
| GET | /{pixelId:guid}/events |
Get pixel event history | Query: from?, to?, page=1, pageSize=50 |
List<PixelEventDto> (200) — MOCK |
| GET | /{pixelId:guid}/stats |
Get pixel statistics | Query: from?, to? |
PixelStatsDto (200) — MOCK |
| PUT | /{pixelId:guid}/activate |
Activate a tracking pixel | Path: pixelId |
200 — MOCK |
| PUT | /{pixelId:guid}/deactivate |
Deactivate a tracking pixel | Path: pixelId |
200 — MOCK |
AdminConversionsController — api/v1/admin/ads-tracking/conversions
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | / |
List all conversions (admin, filtered) | Query: advertiserId?, campaignId?, conversionType?, from?, to?, page=1, pageSize=20 |
IEnumerable<ConversionDto> (200) |
| GET | /stats |
Get conversion statistics | Query: campaignId?, from?, to? |
ConversionStatsDto (200) — MOCK |
| GET | /{id:guid} |
Get conversion details by ID | Path: id (Guid) |
ConversionDetailDto (200) — MOCK |
AdminAttributionController — api/v1/admin/ads-tracking/attribution
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | /stats |
Get attribution statistics by model | Query: from?, to? |
AttributionStatsDto (200) — MOCK |
| GET | /campaigns/{campaignId:guid} |
Get attribution report for a campaign | Query: from?, to? |
CampaignAttributionReportDto (200) — MOCK |
Health Endpoints
| Method | Route | Description |
|---|---|---|
| GET | /health |
Full health check (includes PostgreSQL) |
| GET | /health/live |
Liveness probe (app running check only) |
| GET | /health/ready |
Readiness probe (includes all checks) |
3. Commands
CreateTrackingPixelCommand
- File:
Application/Commands/CreateTrackingPixelCommand.cs - Parameters:
Guid AdvertiserId - Returns:
TrackingPixelResult { PixelId, PixelCode, IsActive } - Behavior: Checks if a pixel already exists for the advertiser (returns existing if found). Otherwise creates a new
TrackingPixelentity with an auto-generated 16-char uppercase hex pixel code. Persists viaITrackingPixelRepository+UnitOfWork.SaveEntitiesAsync. - Validator:
CreateTrackingPixelCommandValidator— requiresAdvertiserIdto be non-empty.
TrackPixelEventCommand
- File:
Application/Commands/TrackPixelEventCommand.cs - Parameters:
string PixelCode,Guid AdId,Guid UserId,PixelEventType EventType,string? UserAgent,string? IpAddress - Returns:
bool(success/failure) - Behavior: Looks up the pixel by code, validates it exists and is active. Creates a
PixelEvententity in-memory but does NOT persist it (noted as incomplete — only logs the event). Returnsfalseif pixel not found or inactive.
RecordConversionCommand
- File:
Application/Commands/RecordConversionCommand.cs - Parameters:
Guid AdvertiserId,Guid CampaignId,Guid UserId,string ConversionType,decimal ConversionValue,string Currency = "VND" - Returns:
ConversionResult { ConversionId, ConversionTime } - Behavior: Creates a new
Conversionentity and persists viaIConversionRepository+UnitOfWork.SaveEntitiesAsync. - Note: This command has a handler but is not exposed via any controller endpoint. It is available for internal/cross-service use only.
4. Queries
GetPixelCodeQuery
- File:
Application/Queries/GetPixelCodeQuery.cs - Parameters:
Guid AdvertiserId - Returns:
PixelCodeDto? { Code, IsActive, CreatedAt }(nullable) - Behavior: Fetches pixel by advertiser ID via repository. Returns null if not found.
GetConversionsQuery
- File:
Application/Queries/GetConversionsQuery.cs - Parameters:
Guid? CampaignId,Guid? UserId,DateTime? From,DateTime? To,int Skip = 0,int Take = 20 - Returns:
IEnumerable<ConversionDto>whereConversionDto { Id, AdvertiserId, CampaignId, UserId, ConversionType, ConversionValue, Currency, ConversionTime } - Behavior: Filters by CampaignId (prioritized), then UserId, then time range. Default: last 30 days. Applies Skip/Take pagination.
GetAttributionDetailsQuery
- File:
Application/Queries/GetAttributionDetailsQuery.cs - Parameters:
Guid ConversionId - Returns:
AttributionDto? { Id, ConversionId, AdId, CampaignId, Model, AttributedValue, AttributedAt }(nullable) - Behavior: Fetches attribution record by conversion ID. Returns null if not found. Model is returned as string via
.ToString().
5. Domain Model
Aggregates
TrackingPixel (Aggregate Root)
- File:
Domain/AggregatesModel/TrackingPixelAggregate/TrackingPixel.cs - Fields:
_advertiserId(Guid),_pixelCode(string, 16 chars),_isActive(bool),CreatedAt(DateTime) - Behavior Methods:
Activate(),Deactivate() - Factory: Constructor generates pixel code via
Guid.NewGuid().ToString("N")[..16].ToUpper() - Repository:
ITrackingPixelRepository—GetByAdvertiserIdAsync,GetByPixelCodeAsync,GetActivePixelsAsync
PixelEvent (Entity, non-aggregate)
- File: Same file as TrackingPixel
- Fields:
_pixelId(Guid),_adId(Guid),_userId(Guid),_eventType(PixelEventType),_userAgent(string?),_ipAddress(string?),_timestamp(DateTime) - Note: Not an aggregate root. No dedicated repository. Currently created in-memory only (not persisted by handler).
Conversion (Aggregate Root)
- File:
Domain/AggregatesModel/ConversionAggregate/Conversion.cs - Fields:
_advertiserId(Guid),_campaignId(Guid),_userId(Guid),_conversionType(string),_conversionValue(decimal),_currency(string, default "VND"),_conversionTime(DateTime) - Static Factory Methods:
Conversion.Purchase(advertiserId, campaignId, userId, amount),Conversion.Lead(advertiserId, campaignId, userId) - Repository:
IConversionRepository—GetByCampaignIdAsync,GetByUserIdAsync,GetByTimeRangeAsync
Attribution (Aggregate Root)
- File:
Domain/AggregatesModel/AttributionAggregate/Attribution.cs - Fields:
_conversionId(Guid),_adId(Guid),_campaignId(Guid),_model(AttributionModel enum),_attributedValue(decimal),_attributedAt(DateTime) - Repository:
IAttributionRepository—GetByConversionIdAsync,GetByCampaignIdAsync,GetByAdIdAsync,GetByModelAsync
Enumerations
PixelEventType (enum)
Impression = 1, Click = 2, PageView = 3, ViewContent = 4,
AddToCart = 5, InitiateCheckout = 6, Purchase = 7, Lead = 8
AttributionModel (enum)
LastClick = 1, FirstClick = 2, Linear = 3, TimeDecay = 4
Value Objects
AttributionWindow
- File:
Domain/AggregatesModel/AttributionAggregate/AttributionWindow.cs - Properties:
ClickWindowDays(int),ViewWindowDays(int) - Validation: Neither can be negative; both cannot be zero simultaneously
- Predefined Windows:
SevenDayClick(7,0),OneDayView(0,1),SevenOne(7,1),TwentyEightOne(28,1) - Methods:
IsWithinWindow(eventTime, conversionTime, isClick)— checks if conversion falls within the attribution window - Note: Defined but not currently used by any command/query handler.
SeedWork (Base Classes)
- Entity: Base class with
Guid Id,DomainEventslist, equality by ID - IAggregateRoot: Marker interface
- IRepository: Generic interface with
UnitOfWork,GetByIdAsync,AddAsync,Update,Delete - IUnitOfWork:
SaveChangesAsync,SaveEntitiesAsync(dispatches domain events) - ValueObject: Immutable base with structural equality
- Enumeration: Type-safe enum pattern with
Id/Name, parsing utilities
Exceptions
- DomainException: Base domain exception
- AdsTrackingDomainException: Service-specific domain exception (neither currently thrown by any code)
6. Database Schema
All tables in schema ads_tracking. Single migration: 20260117182759_InitialCreate.
Table: tracking_pixels
| Column | Type | Constraints |
|---|---|---|
id |
uuid | PK, NOT NULL |
advertiser_id |
uuid | NOT NULL |
pixel_code |
varchar(16) | NOT NULL |
is_active |
boolean | NOT NULL |
created_at |
timestamp with time zone | NOT NULL |
Indexes:
ix_tracking_pixels_advertiser_idonadvertiser_iduix_tracking_pixels_pixel_codeonpixel_code(UNIQUE)ix_tracking_pixels_is_activeonis_active
Table: pixel_events
| Column | Type | Constraints |
|---|---|---|
id |
uuid | PK, NOT NULL |
pixel_id |
uuid | NOT NULL |
ad_id |
uuid | NOT NULL |
user_id |
uuid | NOT NULL |
event_type |
integer | NOT NULL |
user_agent |
varchar(500) | nullable |
ip_address |
varchar(45) | nullable |
timestamp |
timestamp with time zone | NOT NULL |
Indexes:
ix_pixel_events_pixel_id_timestampon (pixel_id,timestamp)ix_pixel_events_ad_id_timestampon (ad_id,timestamp)ix_pixel_events_user_id_timestampon (user_id,timestamp)ix_pixel_events_event_typeonevent_typeix_pixel_events_event_type_timestampon (event_type,timestamp)
Table: conversions
| Column | Type | Constraints |
|---|---|---|
id |
uuid | PK, NOT NULL |
advertiser_id |
uuid | NOT NULL |
campaign_id |
uuid | NOT NULL |
user_id |
uuid | NOT NULL |
conversion_type |
varchar(50) | NOT NULL |
conversion_value |
numeric(18,2) | NOT NULL |
currency |
varchar(3) | NOT NULL |
conversion_time |
timestamp with time zone | NOT NULL |
Indexes:
ix_conversions_advertiser_idonadvertiser_idix_conversions_campaign_id_conversion_timeon (campaign_id,conversion_time)ix_conversions_user_id_conversion_timeon (user_id,conversion_time)ix_conversions_conversion_typeonconversion_typeix_conversions_conversion_timeonconversion_time
Table: attributions
| Column | Type | Constraints |
|---|---|---|
id |
uuid | PK, NOT NULL |
conversion_id |
uuid | NOT NULL |
ad_id |
uuid | NOT NULL |
campaign_id |
uuid | NOT NULL |
model |
integer | NOT NULL |
attributed_value |
numeric(18,2) | NOT NULL |
attributed_at |
timestamp with time zone | NOT NULL |
Indexes:
ix_attributions_conversion_idonconversion_idix_attributions_ad_id_attributed_aton (ad_id,attributed_at)ix_attributions_campaign_id_attributed_aton (campaign_id,attributed_at)ix_attributions_modelonmodelix_attributions_model_campaign_id_attributed_aton (model,campaign_id,attributed_at)
Idempotency Table (implicit via ClientRequest entity)
The ClientRequest entity is registered with the DbContext but has no explicit IEntityTypeConfiguration. It would use EF Core conventions (table ClientRequest, columns Id, Name, Time).
7. Integration Events
None implemented. No integration events are published or consumed. There are no event handlers, no RabbitMQ configuration, and no cross-service event classes in the codebase.
The RecordConversionCommand exists as an internal command but is not exposed via any API endpoint, suggesting it may be intended for future cross-service invocation.
8. Dependencies
NuGet Packages
API Layer (AdsTrackingService.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 (AdsTrackingService.Domain)
| Package | Version |
|---|---|
| MediatR.Contracts | 2.0.1 |
Infrastructure Layer (AdsTrackingService.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 |
|---|---|---|
| Microsoft.NET.Test.Sdk | 17.12.0 | Both |
| xunit | 2.9.2 | Both |
| xunit.runner.visualstudio | 2.8.2 | Both |
| FluentAssertions | 6.12.2 | Both |
| Moq | 4.20.72 | UnitTests |
| 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 |
Build-level
| Package | Version |
|---|---|
| Microsoft.SourceLink.GitHub | 8.0.0 |
External Service Dependencies
- PostgreSQL (Neon): Primary datastore
- Redis: Connection string configured (
localhost:6379) but not used in application code (only health check package referenced) - No RabbitMQ: No message broker integration
- No external HTTP services: No outbound API calls
9. Configuration
Connection Strings (appsettings.json)
ConnectionStrings__DefaultConnection (or DATABASE_URL env var)
Resolution order in code: GetConnectionString("DefaultConnection") -> Configuration["DATABASE_URL"]
Redis
Redis__ConnectionString = localhost:6379
Referenced in config but not actively used in application code.
JWT Settings
Jwt__Secret = your-super-secret-key-min-32-characters
Jwt__Issuer = goodgo-platform
Jwt__Audience = goodgo-services
Jwt__AccessTokenExpiryMinutes = 15
Jwt__RefreshTokenExpiryDays = 7
Configured in appsettings.json but NOT consumed — there is no authentication middleware (app.UseAuthentication() / app.UseAuthorization() are absent from Program.cs). All endpoints are currently unauthenticated.
Environment Variables (.env.example)
| Variable | Default | Description |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Development | Runtime environment |
DATABASE_URL |
localhost PostgreSQL | Fallback connection string |
REDIS_URL |
localhost:6379 | Redis connection |
JWT_SECRET |
(placeholder) | JWT signing key |
JWT_ISSUER |
goodgo-platform | Token issuer |
JWT_AUDIENCE |
goodgo-services | Token audience |
API_PORT |
5000 | API port |
OTEL_EXPORTER_OTLP_ENDPOINT |
localhost:4317 | OpenTelemetry endpoint |
LOG_LEVEL |
Information | Serilog minimum level |
SEQ_URL |
localhost:5341 | Seq log sink |
RATE_LIMIT_PERMITS_PER_MINUTE |
100 | Rate limiting |
HEALTHCHECK_TIMEOUT_SECONDS |
5 | Health check timeout |
Serilog
Configured via appsettings.json with console sink. Seq sink package is included but not configured in settings. Development mode enables Debug-level logging for EF Core commands.
API Versioning
- Default: v1.0
- Readers: URL segment (
api/v{version}) + Header (X-Api-Version) - Reports API versions in responses
CORS
Open policy: AllowAnyOrigin, AllowAnyMethod, AllowAnyHeader.
Swagger
Available in Development environment at /swagger.
Docker
- Multi-stage build:
sdk:10.0(build) ->aspnet:10.0(runtime) - Non-root user:
dotnetuser(UID/GID 1001) - Port: 8080
- Health check:
curl -f http://localhost:8080/health/live(30s interval, 3 retries)
MediatR Pipeline (execution order)
LoggingBehavior— logs request name, measures elapsed time with StopwatchValidatorBehavior— runs FluentValidation validators, throwsValidationExceptionon failureTransactionBehavior— wraps Commands in DB transactions (skips Queries by name suffix check), usesExecutionStrategyfor retry
10. Maturity Assessment
Working
- Tracking pixel CRUD (create + get by advertiser)
- Client-side pixel event tracking endpoint (validation only, no persistence)
- Server-side event tracking endpoint (logging only)
- Conversion querying with filters (campaign, user, time range)
- Attribution detail lookup by conversion ID
- Database schema and migration
- Health check endpoints
- Functional tests (2 tests: pixel not found + health check)
Incomplete / Mock
- Admin pixel endpoints: All 5 endpoints return hardcoded/mock data
- Admin conversion stats: Returns hardcoded data
- Admin conversion details by ID: Returns hardcoded data
- Admin attribution stats + campaign report: Returns hardcoded data
- PixelEvent persistence:
TrackPixelEventCommandHandlercreates the entity but does not save it to any repository - RecordConversionCommand: Has handler but no controller endpoint exposes it
- AttributionWindow value object: Fully defined but not used anywhere
- Authentication/Authorization: JWT settings configured but no auth middleware applied — all endpoints are public
- Redis: Package referenced but not used in code
- Dapper: Package referenced but not used (all queries use EF Core)
- Polly: Package referenced but not used (only EF Core retry configured)
- Unit tests: Project exists but contains zero test files
- Integration events: None defined — no cross-service communication
- Domain events: Entity base class supports them but no domain events are raised by any aggregate