Skip to content

Fact Emission Patterns

Structured event tracking using z0’s Fact primitive and FACT_TYPES constants.

Prerequisites: core-concepts.md

Updated for v0.10.0: All SDK components emit Facts for state changes using typed FACT_TYPES constants.


The z0 SDK uses the Fact primitive not only for domain events but also for tracking its own internal behavior. This provides:

  • Unified observability - Same pattern for domain and SDK events
  • Type safety - Exported FACT_TYPES constants prevent typos
  • Discoverability - IDE autocomplete shows all available fact types
  • Audit trail - Every state change is recorded immutably
  • Config versioning - Track which config version triggered which events

All facts follow this structure:

interface Fact<T = unknown> {
id: string; // Unique fact ID (ULID)
type: string; // Event category (component name)
subtype?: string; // Refined action/event
timestamp: number; // When the event occurred (Unix ms)
entity_id?: string; // Entity this fact relates to
tenant_id?: string; // Tenant ownership
correlation_id?: string; // For tracking related events
data: T; // Event payload
}

SDK facts use the pattern: {component}.{action}

Examples:

  • projection.migration_started - ProjectionEngine began migration
  • meter.usage - MeterEngine tracked usage
  • circuit.state_changed - CircuitBreaker changed state
  • rate_limit.triggered - RateLimiter denied request
  • threshold.crossed - ThresholdMonitor detected crossing

This matches domain fact patterns and enables unified querying:

// Query all projection-related facts
const projectionFacts = await factManager.getFacts({ type: 'projection' });
// Query specific action
const migrations = await factManager.getFacts({
type: 'projection',
subtype: 'migration_started'
});

All SDK components export typed constants for their fact patterns. This provides compile-time safety and IDE autocomplete.

import {
PROJECTION_FACT_TYPES,
METER_FACT_TYPES,
CB_FACT_TYPES,
RL_FACT_TYPES,
THRESHOLD_MONITOR_FACT_TYPES,
SYSTEM_FACT_TYPES,
} from '@z0-app/sdk';

Without constants (error-prone):

// Typo not caught until runtime
if (fact.type === 'projection' && fact.subtype === 'migraton_started') {
// This never matches due to typo
}

With constants (compile-time safe):

import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
// Typo caught by TypeScript
if (fact.type + '.' + fact.subtype === PROJECTION_FACT_TYPES.MIGRATION_STARTED) {
// Type-safe, autocomplete available
}

Match facts using constants:

import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
const pattern = `${fact.type}.${fact.subtype}`;
switch (pattern) {
case PROJECTION_FACT_TYPES.MIGRATION_STARTED:
console.log('Migration started:', fact.data);
break;
case PROJECTION_FACT_TYPES.MIGRATION_COMPLETED:
console.log('Migration completed in', fact.data.duration_ms, 'ms');
break;
case PROJECTION_FACT_TYPES.MIGRATION_FAILED:
console.error('Migration failed:', fact.data.error_message);
break;
}

import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
PROJECTION_FACT_TYPES.MIGRATION_STARTED // 'projection.migration_started'
PROJECTION_FACT_TYPES.MIGRATION_COMPLETED // 'projection.migration_completed'
PROJECTION_FACT_TYPES.MIGRATION_FAILED // 'projection.migration_failed'

When emitted:

  • migration_started: Config storageVersion changes, migration begins
  • migration_completed: Migration succeeded (includes duration, bucket count)
  • migration_failed: Migration failed (original data intact, includes error)

Example:

import { ProjectionEngine, PROJECTION_FACT_TYPES } from '@z0-app/sdk';
const engine = new ProjectionEngine(config, {
factManager,
entityId: 'proj_daily_usage',
tenantId: 'system',
});
// Listen for migration events
factManager.on('fact', (fact) => {
const pattern = `${fact.type}.${fact.subtype}`;
if (pattern === PROJECTION_FACT_TYPES.MIGRATION_STARTED) {
console.log('Migrating from version', fact.data.from_version);
console.log('to version', fact.data.to_version);
}
if (pattern === PROJECTION_FACT_TYPES.MIGRATION_COMPLETED) {
console.log('Migration completed in', fact.data.duration_ms, 'ms');
console.log('Migrated', fact.data.buckets_migrated, 'buckets');
}
});
import { METER_FACT_TYPES } from '@z0-app/sdk';
METER_FACT_TYPES.USAGE // 'meter.usage'
METER_FACT_TYPES.BUDGET_CHECK // 'meter.budget_check'

When emitted:

  • usage: Every time usage is incremented
  • budget_check: When budget eligibility is checked (warning or denial)

Example:

import { MeterEngine, METER_FACT_TYPES } from '@z0-app/sdk';
const meter = new MeterEngine(config, sqlStorage, factManager);
// Increment usage - emits meter.usage fact
await meter.incrementUsage('entity_123', 5);
// Check budget - emits meter.budget_check fact
const allowed = await meter.checkBudget('entity_123', 10, 'hour');
import { CB_FACT_TYPES } from '@z0-app/sdk';
CB_FACT_TYPES.STATE_CHANGED // 'circuit.state_changed'
CB_FACT_TYPES.CONFIG_UPDATED // 'circuit.config_updated'

When emitted:

  • state_changed: On any state transition (CLOSED↔OPEN↔HALF_OPEN)
  • config_updated: When config changes via updateConfig()

Example:

import { CircuitBreaker, CB_FACT_TYPES } from '@z0-app/sdk';
const cb = new CircuitBreaker(config, factManager);
// Monitor state changes
factManager.on('fact', (fact) => {
if (`${fact.type}.${fact.subtype}` === CB_FACT_TYPES.STATE_CHANGED) {
if (fact.data.to_state === 'open') {
// Circuit opened - send alert
alerting.send({
severity: 'high',
message: `Circuit ${fact.data.circuit_id} opened after ${fact.data.failures_at_transition} failures`,
});
}
if (fact.data.from_state === 'half_open' && fact.data.to_state === 'closed') {
// Circuit recovered
alerting.send({
severity: 'info',
message: `Circuit ${fact.data.circuit_id} recovered`,
});
}
}
});
import { RL_FACT_TYPES } from '@z0-app/sdk';
RL_FACT_TYPES.TRIGGERED // 'rate_limit.triggered'
RL_FACT_TYPES.RECOVERED // 'rate_limit.recovered'

When emitted:

  • triggered: Request denied due to rate limit
  • recovered: Entity dropped back under rate limit

Example:

import { RateLimiter, RL_FACT_TYPES } from '@z0-app/sdk';
const limiter = new RateLimiter(config, factManager);
// Check rate limit
const allowed = await limiter.isAllowed('entity_123', 'api:/v1/users');
if (!allowed) {
// Fact emitted: rate_limit.triggered
// Includes: requests count, max_requests, window_ms
}
import { THRESHOLD_MONITOR_FACT_TYPES } from '@z0-app/sdk';
THRESHOLD_MONITOR_FACT_TYPES.CROSSED // 'threshold.crossed'
THRESHOLD_MONITOR_FACT_TYPES.RECOVERED // 'threshold.recovered'

When emitted:

  • crossed: Value dropped below threshold
  • recovered: Value rose back above threshold

Example:

import { ThresholdMonitor, THRESHOLD_MONITOR_FACT_TYPES } from '@z0-app/sdk';
const monitor = new ThresholdMonitor({
factManager,
monitorId: 'low_balance',
tenantId: 'tnt_acme',
});
const result = monitor.check(oldBalance, newBalance, threshold);
if (result === 'crossed') {
// Fact emitted: threshold.crossed
// Includes: threshold_value, old_value, new_value, monitor_id
}

All facts include config_version field when component is constructed from Config<T>:

import type { Config } from '@z0-app/sdk';
import { CircuitBreaker } from '@z0-app/sdk';
const config: Config<CircuitBreakerConfigSettings> = {
id: 'cb_api',
type: 'circuit_breaker',
version: 3, // This version included in facts
settings: { /* ... */ },
// ...
};
const cb = new CircuitBreaker(config, factManager);
// Execute operation - if state changes, fact includes config_version
await cb.execute(() => apiCall());
// Emitted fact:
// {
// type: 'circuit',
// subtype: 'state_changed',
// data: {
// circuit_id: 'cb_api',
// from_state: 'closed',
// to_state: 'open',
// config_version: 3, // <-- Tracked
// failures_at_transition: 5
// }
// }

Why track config_version?

  • Debug behavioral changes: “Why did this behave differently last week?”
  • Compliance: Regulatory requirement to track configuration
  • A/B testing: Compare behavior across config versions
  • Audit trail: Know exactly which config triggered which events

// All projection facts
const projectionFacts = await factManager.getFacts({
type: 'projection',
});
// All meter facts
const meterFacts = await factManager.getFacts({
type: 'meter',
});
import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
// All migration failures
const failures = await factManager.getFacts({
type: 'projection',
subtype: 'migration_failed',
});
// Using pattern matching
const allFacts = await factManager.getFacts();
const migrationFacts = allFacts.filter(f =>
`${f.type}.${f.subtype}` === PROJECTION_FACT_TYPES.MIGRATION_STARTED
);
// All facts for specific projection
const projectionEvents = await factManager.getFacts({
entity_id: 'proj_daily_usage',
});
// All facts for specific meter
const meterEvents = await factManager.getFacts({
entity_id: 'meter_api_calls',
});
// Facts in last hour
const recentFacts = await factManager.getFacts({
from: Date.now() - 3600_000,
to: Date.now(),
});

import { CB_FACT_TYPES } from '@z0-app/sdk';
async function getCircuitBreakerHealth() {
const facts = await factManager.getFacts({
type: 'circuit',
subtype: 'state_changed',
from: Date.now() - 86400_000, // Last 24h
});
const openCircuits = facts
.filter(f => f.data.to_state === 'open')
.map(f => ({
circuit_id: f.data.circuit_id,
opened_at: f.timestamp,
failures: f.data.failures_at_transition,
}));
return {
total_circuits: new Set(facts.map(f => f.data.circuit_id)).size,
open_count: openCircuits.length,
open_circuits: openCircuits,
};
}
import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
async function getMigrationHistory() {
const migrations = await factManager.getFacts({
type: 'projection',
});
const completed = migrations.filter(f =>
`${f.type}.${f.subtype}` === PROJECTION_FACT_TYPES.MIGRATION_COMPLETED
);
return completed.map(f => ({
projection_id: f.data.projection_id,
from_version: f.data.from_version,
to_version: f.data.to_version,
duration_ms: f.data.duration_ms,
buckets_migrated: f.data.buckets_migrated,
timestamp: f.timestamp,
}));
}
import { RL_FACT_TYPES } from '@z0-app/sdk';
async function getRateLimitViolations() {
const facts = await factManager.getFacts({
type: 'rate_limit',
subtype: 'triggered',
from: Date.now() - 3600_000, // Last hour
});
// Group by entity
const byEntity = facts.reduce((acc, f) => {
const entityId = f.entity_id || 'unknown';
if (!acc[entityId]) acc[entityId] = [];
acc[entityId].push(f);
return acc;
}, {} as Record<string, typeof facts>);
return Object.entries(byEntity)
.map(([entity_id, violations]) => ({
entity_id,
violation_count: violations.length,
latest: violations[violations.length - 1],
}))
.sort((a, b) => b.violation_count - a.violation_count);
}

// ❌ BAD - String literals (typo-prone)
if (fact.type === 'projection' && fact.subtype === 'migraton_started') {
// Typo not caught
}
// ✅ GOOD - Type-safe constants
import { PROJECTION_FACT_TYPES } from '@z0-app/sdk';
const pattern = `${fact.type}.${fact.subtype}`;
if (pattern === PROJECTION_FACT_TYPES.MIGRATION_STARTED) {
// Compile-time safety
}

All fact data should include:

  • Component identifier (meter_id, circuit_id, projection_id)
  • Config version (when using Config)
  • Relevant values (old/new state, counts, thresholds)
  • Tenant context (tenant_id)
// Example fact data structure
{
type: 'circuit',
subtype: 'state_changed',
entity_id: 'cb_api_gateway',
tenant_id: 'system',
data: {
circuit_id: 'cb_api_gateway', // Component ID
from_state: 'closed', // Old state
to_state: 'open', // New state
failures_at_transition: 5, // Context
config_version: 2, // Config tracking
}
}

Enable fact emission in production by passing factManager to components:

// Development (no facts)
const cb = new CircuitBreaker(config);
// Production (with facts)
const cb = new CircuitBreaker(config, factManager);

Always filter facts to reduce data transfer:

// ❌ BAD - Fetch all facts
const allFacts = await factManager.getFacts();
const filtered = allFacts.filter(/* ... */);
// ✅ GOOD - Filter at source
const filtered = await factManager.getFacts({
type: 'projection',
entity_id: 'proj_daily_usage',
from: Date.now() - 86400_000,
});

Set up alerts for critical fact patterns:

import { CB_FACT_TYPES, PROJECTION_FACT_TYPES } from '@z0-app/sdk';
const criticalPatterns = [
CB_FACT_TYPES.STATE_CHANGED,
PROJECTION_FACT_TYPES.MIGRATION_FAILED,
];
factManager.on('fact', (fact) => {
const pattern = `${fact.type}.${fact.subtype}`;
if (criticalPatterns.includes(pattern)) {
alerting.send({
severity: 'high',
pattern,
data: fact.data,
});
}
});

ConceptDescription
Fact Pattern{component}.{action} (e.g., ‘meter.usage’)
FACT_TYPESExported constants for type-safe matching
Config VersionTracked in fact data when using Config
QueryingFilter by type, subtype, entity_id, time range
ObservabilityUnified pattern for domain and SDK events

Available FACT_TYPES:

  • PROJECTION_FACT_TYPES - ProjectionEngine events
  • METER_FACT_TYPES - MeterEngine usage tracking
  • CB_FACT_TYPES - CircuitBreaker state changes
  • RL_FACT_TYPES - RateLimiter violations
  • THRESHOLD_MONITOR_FACT_TYPES - Threshold crossings
  • SYSTEM_FACT_TYPES - System-level events

By using typed constants and consistent patterns, fact emission provides type-safe, discoverable observability across all SDK components.