JS/TS v4 → v5

The JS/TS SDK v5 introduces the observation-centric data model. In this model, correlating attributes (userId, sessionId, metadata, tags) propagate to every observation rather than living only on the trace. This enables single-table queries without expensive joins, significantly improving query performance at scale.

This changes how you set trace attributes: instead of imperatively updating the trace with updateActiveTrace(), you use propagateAttributes() — a function that wraps a callback, automatically applying attributes to all child observations created within its scope.

⚠️

v5 changes default OpenTelemetry export behavior: Langfuse now applies a smart default span filter. If you previously expected all spans to be exported (including non-LLM spans), review the first breaking change below.

Breaking Changes

Smart default span filtering replaces export-all behavior

In previous versions, exporting all OpenTelemetry spans by default increased trace noise from infrastructure and non-LLM instrumentation (HTTP, DB, queues, framework internals). To keep traces focused and useful, v5 introduces a smart default span filter.

By default, v5 exports a span if any of these are true:

  • The span was created by Langfuse (langfuse-sdk)
  • The span has gen_ai.* attributes
  • The span instrumentation scope matches known LLM scope prefixes (for example openinference, langsmith, haystack, litellm)

Before v5, all spans were exported unless you implemented a custom shouldExportSpan function.

Keep pre-v5 “export everything” behavior

import { LangfuseSpanProcessor } from "@langfuse/otel";
 
const spanProcessor = new LangfuseSpanProcessor({
  publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
  secretKey: process.env.LANGFUSE_SECRET_KEY!,
  shouldExportSpan: () => true,
});

Compose custom rules with default behavior

shouldExportSpan is a full override in v5. If you want to extend (not replace) default filtering, compose with isDefaultExportSpan.

import { LangfuseSpanProcessor, isDefaultExportSpan } from "@langfuse/otel";
 
const spanProcessor = new LangfuseSpanProcessor({
  publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
  secretKey: process.env.LANGFUSE_SECRET_KEY!,
  shouldExportSpan: ({ otelSpan }) =>
    isDefaultExportSpan(otelSpan) ||
    otelSpan.instrumentationScope.name.startsWith("my_framework"),
});

Possible trace-tree side effects and how to debug

Filtering can break trace trees when intermediate or parent spans are dropped while child spans are still exported. If traces appear disconnected, enable SDK debug logging to inspect dropped spans, then allowlist the required scopes in your callback.

updateActiveTrace() decomposed into 3 functions

In the new model, correlating attributes (userId, sessionId, metadata, tags) must live on every observation, not just the trace. propagateAttributes() wraps a callback — the current and all child spans created inside the callback automatically inherit the attributes. Spans created before the callback are not retroactively updated.

v4:

import { updateActiveTrace, startActiveObservation } from "@langfuse/tracing";
 
await startActiveObservation("my-operation", async (span) => {
  updateActiveTrace({
    name: "user-workflow",
    userId: "user-123",
    sessionId: "session-456",
    tags: ["production"],
    public: true,
    metadata: { testRun: "server-export" },
    input: { query: "hello" },
    output: { response: "world" },
  });
});

v5:

import {
  propagateAttributes,
  startActiveObservation,
  setActiveTraceIO,
  setActiveTraceAsPublic,
} from "@langfuse/tracing";
 
await propagateAttributes(
  {
    traceName: "user-workflow", // was "name"
    userId: "user-123",
    sessionId: "session-456",
    tags: ["production"],
    metadata: { testRun: "server-export" },
  },
  async () => {
    await startActiveObservation("my-operation", async (span) => {
      setActiveTraceIO({
        input: { query: "hello" },
        output: { response: "world" },
      });
      setActiveTraceAsPublic();
    });
  },
);

Key differences:

Attributev4v5
nameupdateActiveTrace({name: ...})propagateAttributes({traceName: ...}, cb)
userId, sessionId, tags, versionupdateActiveTrace({...})propagateAttributes({...}, cb)
metadataupdateActiveTrace({metadata: any})propagateAttributes({metadata: Record<string,string>}, cb)
input, outputupdateActiveTrace({...})setActiveTraceIO({...}) (deprecated)
publicupdateActiveTrace({public: true})setActiveTraceAsPublic()
releaseupdateActiveTrace({release: ...})Removed — use LANGFUSE_RELEASE env var
environmentupdateActiveTrace({environment: ...})Removed — use LANGFUSE_TRACING_ENVIRONMENT env var
⚠️

setActiveTraceIO() is deprecated and exists only for backward compatibility with trace-level LLM-as-a-judge evaluators that rely on trace input/output. For new code, set input/output on the root observation directly.

.updateTrace().setTraceIO() + .setTraceAsPublic()

The same decomposition applies on all observation wrapper classes (LangfuseSpan, LangfuseGeneration, etc.).

v4:

import { startObservation } from "@langfuse/tracing";
 
const span = startObservation("my-op");
span.updateTrace({
  name: "my-trace",
  userId: "user-123",
  sessionId: "session-456",
  tags: ["prod"],
  public: true,
  input: { query: "hello" },
  output: { response: "world" },
});

v5:

import { propagateAttributes, startObservation } from "@langfuse/tracing";
 
propagateAttributes(
  {
    traceName: "my-trace",
    userId: "user-123",
    sessionId: "session-456",
    tags: ["prod"],
  },
  () => {
    const span = startObservation("my-op");
    span.setTraceIO({
      input: { query: "hello" },
      output: { response: "world" },
    });
    span.setTraceAsPublic();
    span.end();
  },
);
⚠️

.setTraceIO() is deprecated and exists only for backward compatibility with trace-level LLM-as-a-judge evaluators that rely on trace input/output.

@langfuse/langchain internal changes

The CallbackHandler now uses propagateAttributes() for trace-level attributes. This affects users who:

  • Subclass CallbackHandler
  • Depend on the internal span-creation behavior
  • Rely on traceMetadata accepting non-string values — non-string values are now serialized via JSON.stringify before being passed to propagateAttributes, which requires Record<string, string>

@langfuse/openai internal changes

The traceMethod wrapper now wraps the traced call in propagateAttributes() to set userId, sessionId, tags, and traceName, instead of calling .updateTrace() on the observation. (If you rely on attributes being set on parent observations as well, wrap the entire execution in with propagateAttributes).

Removed attributes

RemovedReplacement
releaseSet via LANGFUSE_RELEASE env var
environmentSet via LANGFUSE_TRACING_ENVIRONMENT env var
publicReplaced by setActiveTraceAsPublic() / .setTraceAsPublic()

Migration Checklist

  1. Audit traces/dashboards that depended on non-LLM OpenTelemetry spans: these may stop appearing with the v5 default filter
  2. If needed, set shouldExportSpan: () => true on LangfuseSpanProcessor to preserve pre-v5 “export all spans” behavior
  3. If you use custom filtering, compose with isDefaultExportSpan to keep the default LLM-focused behavior
  4. Search for updateActiveTrace → split into propagateAttributes() + setActiveTraceIO() (when relying on legacy trace-level LLM-as-a-judge configurations) + setActiveTraceAsPublic()
  5. Search for .updateTrace( → split into propagateAttributes() + .setTraceIO() + .setTraceAsPublic()
  6. Verify propagated metadata values are Record<string, string> with values ≤200 characters
  7. Replace release/environment attribute usage with env vars (LANGFUSE_RELEASE, LANGFUSE_TRACING_ENVIRONMENT)
Was this page helpful?