Skip to main content

EVENT_SCHEMAS — analytics-service

Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · platform anchor: docs/04 Event-Driven Architecture, naming: docs/standards/NAMING §3

All subjects follow melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>. Service segment is analytics. Schemas are versioned per 04 §4 Versioning. JSON Schema files generated from TypeScript live in bin/event-schemas/.


1. Common envelope

Every event uses the platform EventEnvelope<T> from 04 §3:

interface EventEnvelope<T> {
id: string; // evt_<ulid>
subject: string; // melmastoon.analytics.…
occurredAt: string; // ISO-8601, UTC
tenantId: string; // tnt_…
correlationId: string; // cor_…
causationId?: string;
producedBy: { service: string; version: string; instance: string };
metadata: {
schemaVersion: number;
dataResidency: 'AF' | 'IN' | 'KSA' | 'EU';
orderingKey: string;
retentionClass: 'operational_2y' | 'operational_7y' | 'regulatory_10y_objectlock';
ai?: AIProvenance;
};
payload: T;
}

producedBy.service = "analytics-service". metadata.orderingKey defaults to tenantId for all subjects to preserve in-tenant ordering at the consumer side.


2. Subject taxonomy & retention

SubjectRetentionOrdering keyNotes
melmastoon.analytics.projection.refreshed.v1operational_2ytenantIdper-projection event for any tenant impacted (we emit one envelope per refresh, with affected windows)
melmastoon.analytics.projection.failed.v1operational_2ytenantId
melmastoon.analytics.metric.computed.v1operational_2ytenantIdhigh volume; downstream AI orchestrator subscribes selectively
melmastoon.analytics.dashboard.created.v1operational_7ytenantIdgovernance audit
melmastoon.analytics.dashboard.updated.v1operational_7ytenantId
melmastoon.analytics.dashboard.shared.v1operational_7ytenantIdtracks Looker Studio token issuance
melmastoon.analytics.query.executed.v1operational_2ytenantIdsampled (1% by default) to control volume
melmastoon.analytics.etl.started.v1operational_2yetl_<jobId>ordered per ETL job
melmastoon.analytics.etl.completed.v1operational_2yetl_<jobId>
melmastoon.analytics.etl.failed.v1operational_2yetl_<jobId>
melmastoon.analytics.data_quality.alert.v1operational_7ytenantIddrives notification + audit

3. melmastoon.analytics.projection.refreshed.v1

interface ProjectionRefreshedV1 {
projectionId: string; // prj_…
projectionKey: string; // 'fact_reservation'
schemaVersion: number;
targetTable: string; // 'analytics_curated.fact_reservation_v1'
windowFrom: string; // ISO-8601
windowTo: string;
rowsAffected: number;
bytesScanned: number;
bytesWritten: number;
slotMs: number;
warehouseJobId: string;
freshnessLagMinutes: number;
trigger: 'cron' | 'on_demand' | 'backfill' | 'event';
etlRunId: string; // etr_…
}

JSON schema excerpt:

{
"$id": "https://schemas.melmastoon.ghasi.io/analytics/projection/refreshed.v1.json",
"type": "object",
"required": ["projectionId","projectionKey","schemaVersion","targetTable","windowFrom","windowTo","rowsAffected","bytesScanned","bytesWritten","slotMs","warehouseJobId","freshnessLagMinutes","trigger","etlRunId"],
"properties": {
"projectionId": { "type": "string", "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$" },
"trigger": { "enum": ["cron","on_demand","backfill","event"] }
}
}

Example envelope:

{
"id": "evt_01H…",
"subject": "melmastoon.analytics.projection.refreshed.v1",
"occurredAt": "2026-04-22T10:05:00.000Z",
"tenantId": "tnt_platform",
"correlationId": "cor_01H…",
"producedBy": { "service": "analytics-service", "version": "abc1234", "instance": "etl-worker-2" },
"metadata": { "schemaVersion": 1, "dataResidency": "AF", "orderingKey": "tnt_platform", "retentionClass": "operational_2y" },
"payload": {
"projectionId": "prj_01H…",
"projectionKey": "fact_reservation",
"schemaVersion": 1,
"targetTable": "analytics_curated.fact_reservation_v1",
"windowFrom": "2026-04-21T00:00:00Z",
"windowTo": "2026-04-22T10:00:00Z",
"rowsAffected": 1284,
"bytesScanned": 12849184,
"bytesWritten": 482918,
"slotMs": 412,
"warehouseJobId": "bq-job-…",
"freshnessLagMinutes": 3,
"trigger": "cron",
"etlRunId": "etr_01H…"
}
}

Note: tenantId is tnt_platform because projections are platform-shared. Per-tenant impact is in row counts; subscribers that need tenant fan-out can read affected tenant ids by querying the curated table.


4. melmastoon.analytics.projection.failed.v1

interface ProjectionFailedV1 {
projectionId: string;
projectionKey: string;
windowFrom: string;
windowTo: string;
errorCode: string; // 'MELMASTOON.ANALYTICS.PROJECTION_SCHEMA_MISMATCH'
errorDetail: string;
retriable: boolean;
attempt: number;
etlRunId: string;
}

5. melmastoon.analytics.metric.computed.v1

interface MetricComputedV1 {
tenantId: string;
metricKey: string; // 'reservation.occupancy_pct'
metricVersion: number;
windowFrom: string;
windowTo: string;
filters: Record<string, unknown>;
dimensions: Array<{ key: string; value: string | number }>; // ordered
value: number;
unit: { kind: string; currency?: string; decimals: number };
provenance: {
bytesScanned: number;
slotMs: number;
warehouseJobId: string;
computedBy: 'etl' | 'on_demand' | 'ai_writeback';
};
}

Sampling: emitted every time for ETL-triggered computes; for on_demand we sample at 1 % to control volume (configurable).

Example payload:

{
"tenantId": "tnt_01H…",
"metricKey": "reservation.occupancy_pct",
"metricVersion": 1,
"windowFrom": "2026-04-21T00:00:00Z",
"windowTo": "2026-04-22T00:00:00Z",
"filters": { "property_id": "ppt_01H…" },
"dimensions": [{ "key": "date", "value": "2026-04-21" }],
"value": 71.34,
"unit": { "kind": "percent", "decimals": 2 },
"provenance": { "bytesScanned": 18324567, "slotMs": 1240, "warehouseJobId": "bq-job-…", "computedBy": "etl" }
}

6. melmastoon.analytics.dashboard.created.v1, .updated.v1, .shared.v1

interface DashboardCreatedV1 {
dashboardId: string;
ownerUserId: string;
scope: 'tenant' | 'property' | 'private';
propertyId?: string;
nameI18n: { default: string; values: Record<string, string> };
widgetIds: string[];
}

interface DashboardUpdatedV1 {
dashboardId: string;
changes: { field: string; before: unknown; after: unknown }[];
etag: number;
}

interface DashboardSharedV1 {
dashboardId: string;
shares: Array<
| { kind: 'user'; userId: string }
| { kind: 'role'; role: string }
| { kind: 'looker_studio_token'; tokenId: string; expiresAt: string }
>;
}

7. melmastoon.analytics.query.executed.v1

interface QueryExecutedV1 {
tenantId: string;
userId?: string; // omitted for service-to-service paths
scope: 'widget' | 'saved_query' | 'metric' | 'ad_hoc';
refId?: string; // wid_… | qry_… | met_…
bytesScanned: number;
bytesBilled: number;
slotMs: number;
cacheHit: boolean;
warehouseJobId: string;
durationMs: number;
}

Rate-limited by sampler at the publisher boundary.


8. melmastoon.analytics.etl.started.v1, .completed.v1, .failed.v1

interface EtlStartedV1 { etlJobId: string; etlRunId: string; projectionKey: string; windowFrom: string; windowTo: string; trigger: string; attempt: number; }
interface EtlCompletedV1 { etlJobId: string; etlRunId: string; projectionKey: string; rowsAffected: number; bytesScanned: number; bytesWritten: number; slotMs: number; durationMs: number; }
interface EtlFailedV1 { etlJobId: string; etlRunId: string; projectionKey: string; errorCode: string; errorDetail: string; retriable: boolean; attempt: number; }

Ordered per etl_<jobId> so consumers see the started → completed/failed sequence.


9. melmastoon.analytics.data_quality.alert.v1

interface DataQualityAlertV1 {
checkId: string;
checkKey: string; // 'fact_reservation.row_count_drift_24h'
table: string; // 'fact_reservation_v1'
rule: {
kind: 'row_count_drift' | 'freshness' | 'null_rate' | 'distinct_count' | 'business_rule' | 'schema_drift';
column?: string;
expression?: string;
windowMinutes?: number;
};
severity: 'warn' | 'critical';
observedValue: number;
expectedValue: number | null;
observedAt: string;
bigqueryJobId?: string;
affectedTenants?: string[]; // when the check is per-tenant
}

Routed to notification-service (tenant.admin), audit-service (Merkle anchor), and monitoring (Cloud Monitoring alert sink) consumers.


10. Consumed events

analytics-service consumes from two paths:

10.1 Pub/Sub-to-BigQuery managed sink (no application code)

All melmastoon.* topics with non-quarantined payloads are delivered directly into events_raw.<topic_unsuffixed> via managed Pub/Sub-to-BigQuery subscriptions. We own the subscription configuration as IaC; no service code runs in this path.

Per-subject mapping is declarative:

- subject_glob: melmastoon.*.*.*.v1
raw_table: events_raw.{{ replace(subject, '.', '_') }}
partition_field: ingestion_ts
cluster_fields: [tenant_id, data_residency]
retention_class_default: operational_2y

10.2 Application consumers (push subscriptions)

SubjectHandlerEffect
melmastoon.tenant.deleted.v1PurgeTenantUseCaseDrop view bindings, delete operational rows, anonymize regulated rows
melmastoon.tenant.region_changed.v1RebindAuthorizedViewsUseCaseUpdate tenant residency + view binding
melmastoon.ai.forecast.produced.v1IngestForecastWritebackUseCaseMERGE into fact_demand_forecast
melmastoon.tenant.created.v1BootstrapTenantAnalyticsUseCaseCreate authorized view binding, seed dq_check overrides
melmastoon.iam.user.permission_revoked.v1InvalidateDashboardSharesUseCaseDrop dashboard shares for the user

All handlers respect the inbox dedupe pattern ((subject, message_id) UNIQUE in Postgres analytics.inbox_processed).


11. Versioning policy

  • Additive payload fields ship in v1 with optional flag.
  • Removing or renaming a field, changing semantics, or changing dimension shape requires a new v2 topic and a coexistence window of at least one quarter.
  • Each event payload carries metadata.schemaVersion so consumers can pin precise sub-versions.
  • The bin/check-event-schemas.ts CI gate compiles all schemas with AJV and forbids breaking changes within a major version.

Cross-references: 04 §4 Versioning, docs/standards/NAMING §3, APPLICATION_LOGIC §4.