Migrate from Langfuse · 2026

Spanlens covers the same observability surface as Langfuse: traces, generations, prompts, evals, datasets. The integration is one line at the SDK level, so you can switch a single service in under 30 minutes. This page walks the swap end to end and is honest about the four spots where the two products diverge.

Why teams switch

  • Drop-in proxy. Spanlens does not require a callback handler on every chain. Point the OpenAI / Anthropic / Gemini base URL at the proxy and every call is logged, even from code paths you do not own (third-party libraries, background workers, MCP servers).
  • One key per project. No public / secret key split. The samesl_live_* key authenticates the proxy and the REST API.
  • Provider keys stored server-side. Your real OpenAI / Anthropic key is AES-256-GCM encrypted in our database and never lives in client code or env files. Langfuse stores them in your app.
  • Simpler pricing for SaaS. Spanlens bills on logged requests and does not charge separately for evals or observations.

See the full feature comparison on spanlens vs langfuse.

Step 1. Install the Spanlens SDK

pnpm add @spanlens/sdk
# or: npm install @spanlens/sdk
ts

Step 2. Swap the client constructor

The Langfuse OpenAI / Anthropic wrappers become a single Spanlens helper per provider. Same method signatures, same response types, drop-in.

OpenAI

Before (Langfuse):

import { OpenAI } from 'openai'
import { observeOpenAI } from 'langfuse'

const openai = observeOpenAI(new OpenAI())
ts

After (Spanlens):

import { createOpenAI } from '@spanlens/sdk/openai'

const openai = createOpenAI() // reads SPANLENS_API_KEY from env
ts

Anthropic

Before (Langfuse, manual span wrapping):

import Anthropic from '@anthropic-ai/sdk'
import { Langfuse } from 'langfuse'

const langfuse = new Langfuse()
const anthropic = new Anthropic()

const generation = langfuse.generation({ name: 'reply', model: 'claude-haiku-4-5' })
const msg = await anthropic.messages.create({ ... })
generation.end({ output: msg.content })
ts

After (Spanlens):

import { createAnthropic } from '@spanlens/sdk/anthropic'

const anthropic = createAnthropic()
const msg = await anthropic.messages.create({ ... })
// logged automatically, no end() call
ts

LangChain / LangGraph

Replace the Langfuse callback handler with the Spanlens one. The contract is the same.

// Before
import { CallbackHandler } from 'langfuse-langchain'
const handler = new CallbackHandler()

// After
import { SpanlensClient } from '@spanlens/sdk'
import { createSpanlensCallbackHandler } from '@spanlens/sdk/langchain'

const client = new SpanlensClient()
const handler = createSpanlensCallbackHandler({ client })

await chain.invoke({ input }, { callbacks: [handler] })
ts

See LangGraph integration for the full graph-topology mapping (chain.* spans become the LangGraph node tree).

Step 3. Update environment variables

LangfuseSpanlensNotes
LANGFUSE_PUBLIC_KEYnoneSpanlens uses a single secret key.
LANGFUSE_SECRET_KEYSPANLENS_API_KEYIssued per project from /projects. Format sl_live_*.
LANGFUSE_HOSTnot needed on cloudSelf-hosting? Pass baseURL to createOpenAI().
OPENAI_API_KEYregister in dashboardAdd it once under + Add provider key. It stays server-side.

Step 4. Data model mapping

Most concepts map 1:1. Two diverge: Spanlens does not have a separate generation entity (LLM calls are logged as requests), and Spanlens does not have first-class sessions; conversations are grouped via the x-spanlens-session header instead.

LangfuseSpanlensNotes
TraceTraceSame shape. name, metadata, status fields all map.
Observation (Span / Tool / Retrieval)SpanSpanlens span types are llm, tool, retrieval, embedding, custom.
Observation type GenerationRequest (LLM call) + Span (link)LLM calls live in their own column store (ClickHouse) for fast aggregation. Spans link via request_id.
ScoreEval resultSpanlens has runs (one execution) and results (one score). Same 0..1 normalization.
Sessionx-spanlens-session headerNo separate Session table. Use the header on each call and filter by session_id in /requests.
Userx-spanlens-user headerSame pattern. The /users view aggregates per user_id.
PromptPrompt + Prompt VersionVersions are immutable. Linked via x-spanlens-prompt-version header (use withPromptVersion() helper).
Dataset / ItemDataset / Dataset Item1:1. Items accept either variables or messages shape.

Step 5. Run both side-by-side during the cutover

Spanlens does not conflict with Langfuse: they instrument independently. The safest rollout is to leave the Langfuse callback in place, add Spanlens for a single service, and compare for a day or two.

// Both active
import { createOpenAI } from '@spanlens/sdk/openai'
import { observeOpenAI } from 'langfuse'

const openai = observeOpenAI(createOpenAI())
// Every call is now logged to BOTH backends. Pick whichever dashboard
// you trust, then remove the other when you are confident.
ts

Once Spanlens looks right in your dashboard for a representative slice of traffic, delete the Langfuse wrapper, drop the env vars, and uninstall the SDK.

What does not migrate 1:1

  • Public-key client-side ingestion. Spanlens does not have a publishable key. All ingest authenticates with the project secret key. If you were sending events directly from a browser, route them through your backend.
  • Self-hosted historical data. Spanlens does not import Langfuse history. Existing traces stay in Langfuse for read-only access; new traces flow into Spanlens from cutover onwards.
  • Prompt template variables in {{var}} syntax. Spanlens prompts store the raw template and a separate variables JSON list. The template syntax is whatever you choose to render in your app.

Verify the cutover

  1. Make any call from the migrated service.
  2. Open /requests. A new row should appear within a few seconds with model, tokens, cost, latency, full bodies.
  3. For traced workflows, check /traces. The span tree should mirror what Langfuse showed, with the LangGraph node topology preserved if you use the callback handler.
  4. Compare token counts and cost against Langfuse for one day. Differences greater than 1% usually indicate a model name not in the price table; email support@spanlens.io and we will seed it same day.

Next: Data model for the full schema, or LangGraph integration for graph-aware tracing.