Skip to content

Analytics Domain

The analytics domain provides event tracking, querying, and metrics aggregation for TypeScript backends. It is structured in strict Clean Architecture layers with zero npm dependencies in the domain and application layers.

Terminal window
npx @backcap/cli add analytics

The AnalyticsEvent entity is the aggregate root. It is immutable by design — analytics are facts that cannot be edited.

import { AnalyticsEvent } from "./domains/analytics/domain/entities/analytics-event.entity";
const result = AnalyticsEvent.create({
id: crypto.randomUUID(),
trackingId: "site-project-abc",
name: "page_view",
properties: { page: "/home" },
userId: "user-1",
sessionId: "session-abc",
});
if (result.isOk()) {
const event = result.unwrap();
console.log(event.trackingId.value, event.name, event.occurredAt);
}
FieldTypeDescription
idstringUnique identifier (UUID)
trackingIdTrackingIdSite or project identifier value object
namestringEvent name (e.g., “page_view”, “click”)
propertiesRecord<string, unknown> | undefinedOptional event payload
userIdstring | undefinedOptional end-user identifier
sessionIdstring | undefinedOptional session identifier
occurredAtDateTimestamp of the event

AnalyticsEvent.create() returns Result<AnalyticsEvent, InvalidTrackingId>. The entity exposes no mutation methods.

import { TrackingId } from "./domains/analytics/domain/value-objects/tracking-id.vo";
const result = TrackingId.create("site-project-abc");
// Result<TrackingId, InvalidTrackingId>

Validates: non-empty alphanumeric string (allows hyphens) of 8–64 characters. Represents the site or project being tracked (not the end user).

Error ClassConditionMessage
InvalidTrackingIdValue fails length or character validationInvalid tracking ID: "<value>"
EventEmitted ByPayload
EventTrackedTrackEvent use caseeventId, trackingId, name, occurredAt

Records a new analytics event.

const result = await analyticsService.trackEvent({
trackingId: "site-project-abc",
name: "page_view",
properties: { page: "/home" },
userId: "user-1",
sessionId: "session-abc",
});
// Result<{ eventId: string; occurredAt: Date }, Error>

Filters and paginates recorded events.

const result = await analyticsService.queryEvents({
trackingId: "site-project-abc",
name: "page_view",
fromDate: new Date("2026-01-01"),
toDate: new Date("2026-12-31"),
limit: 50,
offset: 0,
});
// Result<{ events: Array<...>; total: number }, Error>

Aggregates metrics for a tracking ID within a date range.

const result = await analyticsService.getMetrics({
trackingId: "site-project-abc",
fromDate: new Date("2026-01-01"),
toDate: new Date("2026-12-31"),
});
// Result<{ totalEvents: number; uniqueUsers: number; eventBreakdown: Array<{ name, count }> }, Error>
export interface IAnalyticsStore {
record(event: AnalyticsEvent): Promise<void>;
query(filters: AnalyticsFilters): Promise<{ events: AnalyticsEvent[]; total: number }>;
aggregate(trackingId: string, fromDate: Date, toDate: Date): Promise<AnalyticsMetrics>;
}

The store is append-only (record). Aggregation is delegated to the store implementation.

import { createAnalyticsDomain, IAnalyticsService } from "./domains/analytics/contracts";
const analyticsService: IAnalyticsService = createAnalyticsDomain({ analyticsStore });
domains/analytics/
domain/
entities/analytics-event.entity.ts
value-objects/tracking-id.vo.ts
events/event-tracked.event.ts
errors/invalid-tracking-id.error.ts
application/
use-cases/track-event.use-case.ts
use-cases/query-events.use-case.ts
use-cases/get-metrics.use-case.ts
ports/analytics-store.port.ts
dto/track-event.dto.ts
dto/query-events.dto.ts
dto/get-metrics.dto.ts
contracts/
analytics.contract.ts
analytics.factory.ts
index.ts
shared/
result.ts