Skip to main content
Signals are metrics and flags that you attach to observability entities (spans, traces, sessions, or completions). They enable you to track business metrics, feature flags, and operational metrics alongside your traces.

Signal types

ZeroEval supports three signal types:
  • Boolean: True/false flags (e.g., cacheHit, isVipUser, hasErrors)
  • Numerical: Numeric values (e.g., itemCount, responseTime, score)
  • String: Text values that can be auto-detected as boolean or numerical
Signal types are auto-detected based on the value type. You can also explicitly specify the type.

Entity types

You can attach signals to four entity types:
  • Span: Metrics for a single operation
  • Trace: Metrics for an entire trace tree
  • Session: Metrics across multiple traces
  • Completion: Metrics for LLM completions

Adding signals to spans

Using span.addSignal()

Add signals directly to the current span:
import { tracer } from 'zeroeval';

const span = tracer.startSpan('processOrder');

// Add boolean signal
span.addSignal('isPremiumUser', true);

// Add numerical signal
span.addSignal('itemCount', 5);

// Add string signal (auto-detected as numerical)
span.addSignal('totalPrice', '99.99');

// Explicitly specify type
span.addSignal('discountApplied', 'true', 'boolean');

tracer.endSpan(span);

Using sendSpanSignal()

Send a signal to the currently active span:
import { sendSpanSignal, withSpan } from 'zeroeval';

await withSpan({ name: 'processPayment' }, async () => {
  const amount = 99.99;
  const success = await chargeCard(amount);
  
  // Send signals to the current span
  sendSpanSignal('paymentAmount', amount);
  sendSpanSignal('paymentSuccess', success);
});

Adding signals to traces

Trace signals apply to an entire trace (all spans sharing a traceId):
import { sendTraceSignal, withSpan } from 'zeroeval';

await withSpan({ name: 'handleRequest' }, async () => {
  let apiCalls = 0;
  
  await withSpan({ name: 'fetchData' }, async () => {
    apiCalls++;
  });
  
  await withSpan({ name: 'processData' }, async () => {
    apiCalls++;
  });
  
  // Signal applies to the entire trace
  sendTraceSignal('totalApiCalls', apiCalls);
  sendTraceSignal('cachingEnabled', true);
});

Adding signals to sessions

Session signals track metrics across multiple traces in the same session:
import { sendSessionSignal, withSpan } from 'zeroeval';

const sessionId = 'user-session-123';
let totalRequests = 0;

// First request
await withSpan({
  name: 'request1',
  sessionId
}, async () => {
  totalRequests++;
  sendSessionSignal('totalRequests', totalRequests);
});

// Second request
await withSpan({
  name: 'request2',
  sessionId
}, async () => {
  totalRequests++;
  sendSessionSignal('totalRequests', totalRequests);
});

// Add session-level metrics
sendSessionSignal('userSatisfied', true);
sendSessionSignal('sessionDurationMinutes', 15.5);

Sending signals to specific entities

For more control, send signals directly to any entity by ID:
import { sendSignal } from 'zeroeval';

// Send to a specific span
await sendSignal('span', 'span-uuid-123', 'retryCount', 3);

// Send to a specific trace
await sendSignal('trace', 'trace-uuid-456', 'totalDuration', 1234);

// Send to a specific session
await sendSignal('session', 'session-uuid-789', 'errorRate', 0.05);

// Send to a completion
await sendSignal('completion', 'completion-uuid-012', 'userRating', 5);

Bulk signals

Send multiple signals in a single API call:
import { sendBulkSignals } from 'zeroeval';

await sendBulkSignals([
  {
    entity_type: 'trace',
    entity_id: 'trace-123',
    name: 'apiCallCount',
    value: 5,
    signal_type: 'numerical'
  },
  {
    entity_type: 'session',
    entity_id: 'session-456',
    name: 'conversionSuccess',
    value: true,
    signal_type: 'boolean'
  },
  {
    entity_type: 'span',
    entity_id: 'span-789',
    name: 'cacheHit',
    value: false,
    signal_type: 'boolean'
  }
]);

Type detection

Automatic type detection

Signal types are automatically detected from the value:
import { detectSignalType } from 'zeroeval/observability/signals';

detectSignalType(true);      // 'boolean'
detectSignalType(42);        // 'numerical'
detectSignalType('true');    // 'boolean'
detectSignalType('123');     // 'numerical'
detectSignalType('hello');   // 'boolean' (default)

Explicit type specification

You can override auto-detection:
// Force string '123' to be treated as numerical
span.addSignal('orderId', '123', 'numerical');

// Force 'yes' to be treated as boolean
span.addSignal('confirmed', 'yes', 'boolean');

Common use cases

Business metrics

Track business KPIs alongside technical metrics:
import { withSpan, sendSpanSignal, sendTraceSignal } from 'zeroeval';

await withSpan({ name: 'processCheckout' }, async () => {
  const cart = await getCart();
  
  // Span-level metrics
  sendSpanSignal('cartValue', cart.total);
  sendSpanSignal('itemCount', cart.items.length);
  sendSpanSignal('hasCoupon', cart.coupon !== null);
  
  await processPayment(cart);
  
  // Trace-level metrics
  sendTraceSignal('revenueGenerated', cart.total);
  sendTraceSignal('conversionSuccess', true);
});

Feature flags

Track which features are enabled:
import { sendSpanSignal, withSpan } from 'zeroeval';

await withSpan({ name: 'renderPage' }, async () => {
  const flags = await getFeatureFlags();
  
  sendSpanSignal('newCheckoutEnabled', flags.newCheckout);
  sendSpanSignal('recommendationsEnabled', flags.recommendations);
  sendSpanSignal('darkModeEnabled', flags.darkMode);
  
  return renderWithFlags(flags);
});

Performance metrics

Track custom performance indicators:
import { tracer, sendSpanSignal } from 'zeroeval';

const span = tracer.startSpan('databaseQuery');

const startTime = Date.now();
const result = await db.query(sql);
const duration = Date.now() - startTime;

sendSpanSignal('queryDurationMs', duration);
sendSpanSignal('rowsReturned', result.rows.length);
sendSpanSignal('cacheHit', result.fromCache);
sendSpanSignal('indexUsed', result.usedIndex);

tracer.endSpan(span);

A/B testing

Track experiment variants and outcomes:
import { sendSessionSignal, withSpan } from 'zeroeval';

const sessionId = 'user-session-123';
const variant = Math.random() > 0.5 ? 'A' : 'B';

await withSpan({
  name: 'experimentSession',
  sessionId
}, async () => {
  sendSessionSignal('experimentVariant', variant);
  
  const result = await runExperiment(variant);
  
  sendSessionSignal('conversionSuccess', result.converted);
  sendSessionSignal('engagementScore', result.score);
});

Error tracking

Track error rates and types:
import { withSpan, sendTraceSignal, sendSessionSignal } from 'zeroeval';

const sessionId = 'session-123';
let errorCount = 0;

await withSpan({ name: 'batchProcess', sessionId }, async () => {
  for (const item of items) {
    try {
      await processItem(item);
    } catch (error) {
      errorCount++;
      sendTraceSignal('hasErrors', true);
      sendTraceSignal('errorType', error.name);
    }
  }
  
  sendSessionSignal('totalErrors', errorCount);
  sendSessionSignal('errorRate', errorCount / items.length);
});

Best practices

Choose clear, consistent names that indicate what you’re measuring:
// Good
sendSpanSignal('cacheHitRate', 0.95);
sendSpanSignal('isPremiumUser', true);
sendSpanSignal('responseTimeMs', 123);

// Avoid
sendSpanSignal('rate', 0.95);
sendSpanSignal('premium', true);
sendSpanSignal('time', 123);
Match the signal to the appropriate scope:
// Span signals - operation-specific
sendSpanSignal('rowsProcessed', 100);

// Trace signals - request-wide
sendTraceSignal('totalApiCalls', 5);

// Session signals - cross-request
sendSessionSignal('totalRevenue', 299.99);
Override auto-detection for ambiguous values:
// String IDs should be stored, not converted to numbers
span.addSignal('orderId', '00123', 'boolean');

// Binary strings should be boolean, not numerical
span.addSignal('isEnabled', '1', 'boolean');
Use bulk operations for better performance:
// Good - single API call
await sendBulkSignals([
  { entity_type: 'span', entity_id: span1, name: 'metric1', value: 1 },
  { entity_type: 'span', entity_id: span2, name: 'metric2', value: 2 },
  { entity_type: 'span', entity_id: span3, name: 'metric3', value: 3 }
]);

// Avoid - three API calls
await sendSignal('span', span1, 'metric1', 1);
await sendSignal('span', span2, 'metric2', 2);
await sendSignal('span', span3, 'metric3', 3);
Only track metrics you’ll actually use:
// Good - actionable metrics
sendSpanSignal('errorRate', errorRate);
sendSpanSignal('p95Latency', p95);

// Avoid - excessive detail
sendSpanSignal('timestamp1', Date.now());
sendSpanSignal('timestamp2', Date.now());
sendSpanSignal('timestamp3', Date.now());
// Use span timing instead
  • Spans - Learn about span structure and metadata
  • Tracing - Understand trace lifecycle
  • Sessions - Group traces into sessions

Build docs developers (and LLMs) love