Skip to main content

How Metadata Works

Metadata is arbitrary structured JSON data attached to traces or observations. Unlike tags (simple string labels), metadata is key-value data that enables precise filtering and rich analysis.

Understand Metadata Structure

Metadata is a JSON object with string keys and arbitrary values (strings, numbers, booleans, nested objects, arrays).Example metadata:
{
  "tenant_id": "acme-corp",
  "feature": "document-summarization",
  "experiment_version": "v2.3",
  "model_config": {
    "temperature": 0.7,
    "max_tokens": 1000
  },
  "user_tier": "enterprise",
  "region": "us-west-2"
}
Key properties:
  • Top-level keys: Identify dimensions (tenant, feature, version, etc.)
  • Nested objects: Group related data (model config, user details)
  • Merged on update: Adding new keys preserves existing metadata
  • Queryable: Filter traces using SQL-like queries on metadata fields
Avoid overwriting the same top-level key multiple times within a single trace. Metadata updates merge based on top-level keys, and rewriting a key produces undefined behavior. Add keys incrementally instead.

Add Metadata to Traces

Attach metadata when creating traces or update it dynamically as your code executes.Python (decorator pattern):
from abvdev import observe, ABV

abv = ABV(api_key="sk-abv-...", host="https://app.abv.dev")

@observe()
def process_request(user_id, tenant_id):
    # Add metadata to the current trace
    abv.update_current_trace(
        metadata={
            "tenant_id": tenant_id,
            "feature": "query-processing",
            "version": "1.2.3"
        }
    )

    # Process the request
    result = handle_query(user_id)
    return result

process_request(user_id="user-456", tenant_id="acme-corp")
Python (manual span creation):
with abv.start_as_current_span(name="process-order") as root_span:
    # Add metadata to the trace
    root_span.update_trace(metadata={"order_id": "order-123", "region": "us-west"})

    # Add metadata to the current span
    root_span.update(metadata={"stage": "validation"})

    # Create child span with metadata
    with root_span.start_as_current_generation(
        name="generate-summary",
        model="gpt-4o",
        metadata={"temperature": 0.7, "max_tokens": 500}
    ) as gen:
        # Update metadata later if needed
        gen.update(metadata={"completion_reason": "stop"})
JavaScript/TypeScript:
import { startActiveObservation, updateActiveTrace } from '@abvdev/tracing';

await startActiveObservation('process-request', async (span) => {
  // Add metadata to the trace
  updateActiveTrace({
    metadata: {
      tenant_id: 'acme-corp',
      feature: 'summarization',
      version: '1.2.3'
    }
  });

  // Add metadata to the observation
  span.update({
    metadata: { stage: 'processing' }
  });

  // Process request...
});

Update Metadata Incrementally

Metadata updates merge based on top-level keys. You can add new keys throughout the trace lifecycle without overwriting existing data.Example: Incremental updates
with abv.start_as_current_span(name="workflow") as span:
    # First update: Add initial context
    span.update_trace(metadata={"status": "started", "user_id": "user-123"})

    # Second update: Add more context (merges with existing)
    span.update_trace(metadata={"feature": "summarization", "model": "gpt-4"})

    # Third update: Add error details if something fails
    try:
        result = process_data()
    except Exception as e:
        span.update_trace(metadata={"error_type": "timeout", "error_message": str(e)})
        raise

# Final metadata: {"status": "started", "user_id": "user-123", "feature": "summarization", "model": "gpt-4", "error_type": "timeout", "error_message": "..."}
Do not write the same top-level key multiple times:
# BAD: Overwrites "status" key unpredictably
span.update_trace(metadata={"status": "started"})
span.update_trace(metadata={"status": "processing"})  # Undefined behavior
span.update_trace(metadata={"status": "completed"})   # Undefined behavior
Instead, use different keys or nested objects:
# GOOD: Different keys for each stage
span.update_trace(metadata={"stage_1": "started"})
span.update_trace(metadata={"stage_2": "processing"})
span.update_trace(metadata={"stage_3": "completed"})

Query Traces by Metadata

In the ABV Dashboard, filter traces using SQL-like queries on metadata fields.Query examples:
  • metadata.tenant_id = "acme-corp" → All traces for tenant “acme-corp”
  • metadata.feature = "summarization" → All traces for the summarization feature
  • metadata.version = "v2.3" AND environment = "production" → Traces for specific version in production
  • metadata.user_tier = "enterprise" → Traces for enterprise users only
  • metadata.model_config.temperature > 0.5 → Traces with high temperature settings
Dashboard workflow:
  1. Navigate to the Traces view
  2. Click “Add Filter”
  3. Select “Metadata” and enter your query
  4. Results update instantly
Export filtered traces:
  • Export to CSV for analysis
  • Create datasets for evaluations
  • Generate reports for stakeholders

Aggregate Metrics by Metadata

Use metadata dimensions to aggregate costs, latency, and quality metrics for business analysis.Example analyses:
  • Cost by tenant: Group traces by metadata.tenant_id to calculate per-tenant LLM costs
  • Latency by feature: Filter by metadata.feature to identify slow features
  • Quality by experiment: Compare metadata.experiment_version to measure A/B test results
  • Error rate by region: Analyze metadata.region to detect regional issues
Dashboard aggregations:
  • Navigate to Metrics → Custom Dashboards
  • Create charts grouped by metadata fields
  • Track trends over time for specific dimensions
  • Set alerts based on metadata filters (e.g., “alert when enterprise tier costs exceed threshold”)

Why Use Metadata?

Attach tenant_id to isolate costs, performance, and errors by customer.
abv.update_current_trace(metadata={"tenant_id": tenant_id, "subscription_tier": "enterprise"})
Filter by metadata.tenant_id to calculate per-tenant costs or detect tenant-specific errors.
Tag experiment variants to compare costs, latency, and quality.
abv.update_current_trace(metadata={"experiment_variant": "v2", "experiment_id": "summarization-test"})
Compare metrics by variant to make data-driven rollout decisions.
Attach deployment version to correlate issues with releases.
abv.update_current_trace(metadata={"deployment_version": "v1.2.3", "git_commit_sha": "abc123"})
Filter by version to compare error rates and identify regressions.
Track model configurations to compare cost, quality, and latency.
abv.update_current_trace(metadata={"model_provider": "openai", "prompt_version": "v3"})
Filter by metadata.model_provider to quantify tradeoffs and optimize selection.
Segment traces by user attributes to analyze costs by cohort.
abv.update_current_trace(metadata={"subscription_tier": "enterprise", "region": "us-west"})
Group by tier or region to optimize model selection and justify pricing.
Categorize errors for faster debugging.
abv.update_current_trace(metadata={"error_type": "timeout", "timeout_seconds": 30})
Filter by metadata.error_type to identify patterns and set alerts.

Implementation Guide

Use the @observe() decorator to automatically trace functions. Update metadata with abv.update_current_trace() and abv.update_current_span().Setup:
pip install abvdev
Basic usage:
from abvdev import observe, ABV

abv = ABV(api_key="sk-abv-...", host="https://app.abv.dev")

@observe()
def process_data(tenant_id, feature_name):
    # Add metadata to the trace
    abv.update_current_trace(
        metadata={
            "tenant_id": tenant_id,
            "feature": feature_name,
            "version": "1.2.3"
        }
    )

    # Add metadata to the current span
    abv.update_current_span(
        metadata={"processing_stage": "initial"}
    )

    # Process data...
    return "Processing complete"

process_data(tenant_id="acme-corp", feature_name="summarization")
Incremental updates:
@observe()
def complex_workflow(data):
    # Initial metadata
    abv.update_current_trace(metadata={"status": "started"})

    step1_result = process_step1(data)
    abv.update_current_trace(metadata={"step1_duration_ms": step1_result.duration})

    step2_result = process_step2(step1_result)
    abv.update_current_trace(metadata={"step2_duration_ms": step2_result.duration})

    # Final metadata
    abv.update_current_trace(metadata={"status": "completed"})
    return step2_result
Create spans manually and attach metadata at the trace or span level.Trace-level metadata:
from abvdev import ABV

abv = ABV(api_key="sk-abv-...", host="https://app.abv.dev")

# Add metadata to the trace
with abv.start_as_current_span(name="process-request") as root_span:
    # Set trace metadata
    root_span.update_trace(metadata={
        "request_id": "req_12345",
        "tenant_id": "acme-corp",
        "feature": "document-processing"
    })

    # Set span metadata
    root_span.update(metadata={"stage": "parsing"})

    # Your code here
    result = process_document()
Span-level metadata:
with abv.start_as_current_span(name="workflow") as root_span:
    root_span.update_trace(metadata={"workflow_id": "wf-123"})

    # Child span with its own metadata
    with root_span.start_as_current_generation(
        name="generate-summary",
        model="gpt-4o",
        metadata={"temperature": 0.7, "max_tokens": 1000}
    ) as gen:
        # Update generation metadata
        gen.update(metadata={"completion_reason": "stop", "tokens_used": 850})
Use the @abvdev/tracing package to add metadata to traces and observations.Setup:
npm install @abvdev/tracing @abvdev/otel @opentelemetry/sdk-node dotenv
Configuration (instrumentation.ts):
import dotenv from 'dotenv';
dotenv.config();

import { NodeSDK } from '@opentelemetry/sdk-node';
import { ABVSpanProcessor } from '@abvdev/otel';

const sdk = new NodeSDK({
  spanProcessors: [
    new ABVSpanProcessor({
      apiKey: process.env.ABV_API_KEY,
      baseUrl: process.env.ABV_BASE_URL,
      exportMode: 'immediate',
      flushAt: 1,
      flushInterval: 1
    })
  ]
});

sdk.start();
Add metadata:
import './instrumentation';  // Must be first import
import { startActiveObservation, updateActiveTrace } from '@abvdev/tracing';

async function main() {
  await startActiveObservation('process-request', async (span) => {
    // Add trace metadata
    updateActiveTrace({
      metadata: {
        tenant_id: 'acme-corp',
        feature: 'summarization',
        version: '1.2.3'
      }
    });

    // Add span metadata
    span.update({
      metadata: { stage: 'processing' }
    });

    // Process request...
  });
}

main();
Wrap existing functions with automatic tracing and metadata updates.Example:
import './instrumentation';
import { observe, updateActiveTrace } from '@abvdev/tracing';

// Original function
async function fetchData(source: string, tenantId: string) {
  // Add metadata
  updateActiveTrace({
    metadata: {
      tenant_id: tenantId,
      data_source: source,
      timestamp: new Date().toISOString()
    }
  });

  // Fetch data
  const response = await fetch(source);
  return await response.json();
}

// Wrap with observe
const tracedFetchData = observe(fetchData, {
  name: 'fetch-data-operation'
});

// Use traced version
async function main() {
  const result = await tracedFetchData('https://api.example.com/data', 'acme-corp');
}

main();
Create spans manually with metadata attached.Example:
import './instrumentation';
import { startObservation } from '@abvdev/tracing';

const span = startObservation('manual-operation', {
  input: { query: 'Process this data' },
  metadata: {
    tenant_id: 'acme-corp',
    feature: 'data-processing',
    priority: 'high'
  }
});

// Update trace metadata
span.updateTrace({
  metadata: {
    workflow_id: 'wf-123',
    user_id: 'user-456'
  }
});

// Update span metadata
span.update({
  metadata: { processing_stage: 'validation' }
});

span.end();

Related Features