Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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 TrackingPixel entity with an auto-generated 16-char uppercase hex pixel code. Persists via ITrackingPixelRepository + UnitOfWork.SaveEntitiesAsync.
  • Validator: CreateTrackingPixelCommandValidator — requires AdvertiserId to 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 PixelEvent entity in-memory but does NOT persist it (noted as incomplete — only logs the event). Returns false if 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 Conversion entity and persists via IConversionRepository + 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> where ConversionDto { 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: ITrackingPixelRepositoryGetByAdvertiserIdAsync, 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: IConversionRepositoryGetByCampaignIdAsync, 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: IAttributionRepositoryGetByConversionIdAsync, 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, DomainEvents list, 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_id on advertiser_id
  • uix_tracking_pixels_pixel_code on pixel_code (UNIQUE)
  • ix_tracking_pixels_is_active on is_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_timestamp on (pixel_id, timestamp)
  • ix_pixel_events_ad_id_timestamp on (ad_id, timestamp)
  • ix_pixel_events_user_id_timestamp on (user_id, timestamp)
  • ix_pixel_events_event_type on event_type
  • ix_pixel_events_event_type_timestamp on (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_id on advertiser_id
  • ix_conversions_campaign_id_conversion_time on (campaign_id, conversion_time)
  • ix_conversions_user_id_conversion_time on (user_id, conversion_time)
  • ix_conversions_conversion_type on conversion_type
  • ix_conversions_conversion_time on conversion_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_id on conversion_id
  • ix_attributions_ad_id_attributed_at on (ad_id, attributed_at)
  • ix_attributions_campaign_id_attributed_at on (campaign_id, attributed_at)
  • ix_attributions_model on model
  • ix_attributions_model_campaign_id_attributed_at on (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)

  1. LoggingBehavior — logs request name, measures elapsed time with Stopwatch
  2. ValidatorBehavior — runs FluentValidation validators, throws ValidationException on failure
  3. TransactionBehavior — wraps Commands in DB transactions (skips Queries by name suffix check), uses ExecutionStrategy for 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: TrackPixelEventCommandHandler creates 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