Skip to content

Reference: Observability

The @rbx/observability package provides telemetry, metrics, span tracing, and correlation context for game instrumentation.

Installation

pnpm add @rbx/observability

Telemetry

Emit structured events for analytics and debugging.

import { emit, emitPlayer } from "@rbx/observability";

// Basic event
emit("game", "player_joined", { region: "us-east" });

// With player context (auto-enriched)
emitPlayer(player, "item_purchased", { itemId: "sword_001", price: 100 });

Event Structure

interface TelemetryEvent {
  category: string;
  name: string;
  level: "debug" | "info" | "warn" | "error";
  timestamp: number;
  clock: number;
  context: unknown;
  data: Record<string, unknown>;
}

Metrics

Track numeric measurements over time using direct class constructors.

import { Counter, Gauge, Histogram, time } from "@rbx/observability";

const requests = new Counter("requests_total", { endpoint: "handshake" });
const errors = new Counter("errors_total", { type: "validation" });

const playersOnline = new Gauge("players_online");

const requestSize = new Histogram("request_size_bytes");
const handlerLatency = new Histogram("request_duration_ms", { endpoint: "action" });

requests.inc();
playersOnline.set(42);
requestSize.observe(1024);

time(handlerLatency, () => {
  // ... do work ...
});

Deprecated factory functions

The createCounter(), createGauge(), and createHistogram() factory functions still work but are deprecated. Use new Counter(), new Gauge(), new Histogram() instead.

Metric Types

Type Use Case Example
Counter Cumulative totals Requests, errors, purchases
Gauge Current state Players online, memory usage
Histogram Distributions Request sizes, damage values
Timing Durations Response times, processing time

Span Tracing

Track operation timing and nested calls.

import { startSpan, withSpan } from "@rbx/observability";

// Manual span management
const span = startSpan("process_action");
span.setAttributes({ actionId: "jump" });
// ... do work ...
span.end();

// Automatic span management (recommended)
withSpan("load_player_data", (span) => {
  span.setAttribute("playerId", 123);
  // ... do work ...
});

// Nested spans create a trace tree
withSpan("handle_request", {}, () => {
  withSpan("validate_input", {}, () => {
    /* ... */
  });
  withSpan("process_logic", {}, () => {
    /* ... */
  });
  withSpan("send_response", {}, () => {
    /* ... */
  });
});

Correlation Context

Propagate request-scoped data across async operations.

import { initContext, getContext, setContext, getPlayerContext } from "@rbx/observability";

// Initialize once at startup
initContext();

// Update global context (server/client)
setContext({ tags: { region: "us-east" } });

// Get context anywhere in the call chain
const ctx = getContext();
print(ctx.serverId);

// Player contexts are per-user
const playerCtx = getPlayerContext(player);
print(playerCtx.playerId);

Automatic Context Enrichment

Telemetry events and spans automatically include correlation context:

setContext({ playerId: "123", sessionId: "abc" });

emit({
  name: "action_completed",
  category: "gameplay",
  data: { actionId: "jump" },
});
// Event automatically includes playerId and sessionId

Best Practices

1. Use Consistent Event Names

// Good - consistent naming
emit({ name: "player_joined", category: "lifecycle" });
emit({ name: "player_left", category: "lifecycle" });

// Avoid - inconsistent naming
emit({ name: "PlayerJoined", category: "lifecycle" });
emit({ name: "player-left", category: "lifecycle" });

2. Categorize Events

// Categories help with filtering and dashboards
const categories = {
  lifecycle: ["player_joined", "player_left", "round_started"],
  economy: ["item_purchased", "currency_earned", "trade_completed"],
  combat: ["damage_dealt", "player_killed", "ability_used"],
  errors: ["validation_failed", "timeout", "rate_limited"],
};

3. Use Spans for Operations

// Wrap significant operations in spans
withSpan("handle_remote", { remote: "DoAction" }, () => {
  withSpan("validate", {}, () => validateInput(data));
  withSpan("process", {}, () => processAction(data));
  withSpan("respond", {}, () => sendResponse(result));
});

Node/Vitest compatibility

@rbx/observability is written in a roblox-ts style, where strings/arrays often use Luau idioms like .size(). The implementation includes internal fallbacks so it can execute under Node-based unit tests (Vitest) without requiring global prototype polyfills.

Built-in metrics

Some packages emit standardized metrics via Counter / Histogram. For moderation sync propagation, runtime publishes:

  • moderation_sync_received_total (labels: topic)
  • moderation_sync_decode_errors_total (labels: topic)
  • moderation_sync_message_age_ms histogram (labels: topic)

4. Sample High-Volume Events

// Sample frequent events to reduce overhead
configureTelemetry({
  sampleRate: 0.01, // 1% for high-volume events
});

// Or sample manually
if (math.random() < 0.01) {
  emit({ name: "frame_update", category: "performance" });
}

Integration Example

import { emit, withSpan, setContext } from "@rbx/observability";
import { Counter, Histogram } from "@rbx/observability";

const actionsRejected = new Counter("action_rejected");
const actionsProcessed = new Counter("action_processed");
const actionDuration = new Histogram("action_duration_ms");

function handlePlayerAction(player: Player, action: ActionRequest) {
  // Set context for this request
  setContext({
    playerId: tostring(player.UserId),
    actionId: action.actionId,
  });

  return withSpan("handle_action", { action: action.actionId }, () => {
    const startTime = os.clock();

    // Validate
    const validation = withSpan("validate", {}, () => {
      return validateAction(action);
    });

    if (!validation.success) {
      actionsRejected.inc({ reason: "validation" });
      return { accepted: false };
    }

    // Process
    const result = withSpan("process", {}, () => {
      return processAction(player, action);
    });

    // Record metrics
    const duration = (os.clock() - startTime) * 1000;
    actionDuration.observe(duration, { action: action.actionId });
    actionsProcessed.inc({ action: action.actionId });

    // Emit event
    emit({
      name: "action_completed",
      category: "gameplay",
      data: { action: action.actionId, success: result.success },
    });

    return result;
  });
}