Observability for TypeSafe Jev JS/TS with Langfuse
This notebook shows how to trace TypeSafe Jev System One calls from the JavaScript SDK with Langfuse. @typesafe-ai/sdk has no OpenTelemetry hook, and there is no @arizeai/openinference-instrumentation-typesafe. Wrap systemOne with Langfuse observe() and register LangfuseSpanProcessor so the generation actually exports.
What is TypeSafe Jev? Jev is TypeSafe's System One model. You send state plus typed Choice, Score, and Noul questions; it returns structured answers with probabilities. It does not generate text. Official Python and JavaScript SDKs wrap
POST /v1/systemone.
What is Langfuse? Langfuse is an open-source LLM engineering platform that helps teams trace, debug, and evaluate LLM applications. Use Langfuse Cloud or self-host it.
Step 1: Install Dependencies
Install the TypeSafe JavaScript SDK, the Langfuse tracing packages, and the OpenTelemetry Node SDK. Node.js 20 or newer is required.
npm install @typesafe-ai/sdk @langfuse/tracing @langfuse/otel @opentelemetry/sdk-nodeStep 2: Set Up Environment Variables
Get your Langfuse keys from the project settings in Langfuse Cloud or set up self-hosting. Get a TypeSafe API key from the TypeSafe console.
# Get keys from your project settings: https://langfuse.com/cloud
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU region (API host)
# Other Langfuse data regions include 🇺🇸 US: https://us.cloud.langfuse.com, 🇯🇵 Japan: https://jp.cloud.langfuse.com and ⚕️ HIPAA: https://hipaa.cloud.langfuse.com
export TYPESAFE_API_KEY="sk-..." # https://console.typesafe.ai/settings/keysStep 3: Initialize OpenTelemetry with Langfuse
observe() only creates spans. They do not reach Langfuse until you register LangfuseSpanProcessor on a Node OpenTelemetry SDK and start that SDK before any systemOne call.
// instrumentation.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";
export const sdk = new NodeSDK({
spanProcessors: [new LangfuseSpanProcessor()],
});
sdk.start();Import this file at the top of your app entry point so the processor is registered before you create observations.
Step 4: Wrap systemOne with observe()
Wrap client.systemOne with observe(). Use asType: "generation" so Langfuse stores the call as a generation. Keep the SystemOneRequest<Q> generic so TypeSafe still infers response.answers from your questions. observe() captures the request and response automatically. Pass { asType: "generation" } to updateActiveObservation() after the SDK returns so the generation also gets the resolved model name and token usage.
Ask Jev three questions about one ticket: a yes/no (Noul), a label (Choice), and a rubric (Score). The same shape covers tool routers, compaction gates, and eval verdicts. Pin jev-1.13.0 when a threshold depends on a specific model version; jev-latest moves when TypeSafe ships a new release.
// index.ts
import { sdk } from "./instrumentation";
import { observe, updateActiveObservation } from "@langfuse/tracing";
import {
TypeSafeClient,
noul,
choice,
score,
type Questions,
type SystemOneRequest,
} from "@typesafe-ai/sdk";
const client = new TypeSafeClient({ defaultModel: "jev-1.13.0" });
const systemOne = observe(
async <Q extends Questions>(request: SystemOneRequest<Q>) => {
const response = await client.systemOne(request);
updateActiveObservation(
{
model: response.model,
usageDetails: {
input: response.usage.input_tokens,
output: response.usage.output_tokens,
},
},
{ asType: "generation" },
);
return response;
},
{ name: "typesafe-system-one", asType: "generation" },
);
async function main() {
const response = await systemOne({
state: { document: "I was charged twice. Please fix this ASAP." },
questions: {
billing: noul("Is this ticket about billing?"),
tone: choice("What is the customer's tone?", {
calm: null,
frustrated: null,
angry: null,
}),
urgency: score("How urgent is this ticket?", [
"can wait",
"this week",
"today",
]),
},
});
console.log(response.model);
console.log(response.answers.billing.noul);
console.log(response.answers.tone.choice, response.answers.tone.confidence);
console.log(response.answers.urgency.score, response.answers.urgency.confidence);
}
main().finally(() => sdk.shutdown());Run the script with npx tsx index.ts. sdk.shutdown() flushes buffered spans. That call is required in short-lived scripts; a long-running server can skip it until process exit.
To write Jev verdicts back onto Langfuse traces as scores, see Using TypeSafe's Jev for evals.
Step 5: View Traces in Langfuse
After running the example, open Langfuse Cloud to see the System One generation: request state and questions, typed answers with probabilities, token usage, and latency.
![]()
Interoperability with the JS/TS SDK
You can use this integration together with the Langfuse SDKs to add additional attributes or group observations into a single trace.
The Context Manager allows you to wrap your instrumented code using context managers (with with statements), which allows you to add additional attributes to the trace. Any observation created inside the callback will automatically be nested under the active observation, and the observation will be ended when the callback finishes.
import { startActiveObservation, propagateAttributes } from "npm:@langfuse/tracing";
await startActiveObservation("context-manager", async (span) => {
span.update({
input: { query: "What is the capital of France?" },
});
// Propagate userId to all child observations
await propagateAttributes(
{
userId: "user-123",
sessionId: "session-123",
metadata: {
source: "api",
region: "us-east-1",
},
tags: ["api", "user"],
version: "1.0.0",
},
async () => {
// YOUR CODE HERE
const { text } = await generateText({
model: openai("gpt-5"),
prompt: "What is the capital of France?",
experimental_telemetry: { isEnabled: true },
});
}
);
span.update({ output: "Paris" });
});Learn more about using the Context Manager in the Langfuse SDK instrumentation docs.
The observe wrapper is a powerful tool for tracing existing functions without modifying their internal logic. It acts as a decorator that automatically creates a span or generation around the function call. You can use the propagateAttributes function to add attributes to the observation from within the wrapped function.
import { observe, propagateAttributes } from "@langfuse/tracing";
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
// An existing function
const processUserRequest = observe(
async (userQuery: string) => {
// Propagate attributes to all child observations
return await propagateAttributes(
{
userId: "user-123",
sessionId: "session-123",
metadata: {
source: "api",
region: "us-east-1",
},
tags: ["api", "user"],
version: "1.0.0",
},
async () => {
// YOUR CODE HERE
const { text } = await generateText({
model: openai("gpt-5"),
prompt: userQuery,
experimental_telemetry: { isEnabled: true },
});
return text;
}
);
},
{ name: "process-user-request" }
);
const result = await processUserRequest("some query");Learn more about using the Decorator in the Langfuse SDK instrumentation docs.
Troubleshooting
No traces appearing
First, enable debug mode in the JS/TS SDK:
export LANGFUSE_LOG_LEVEL="DEBUG"Then run your application and check the debug logs:
- OTel spans appear in the logs: Your application is instrumented correctly but traces are not reaching Langfuse. To resolve this:
- Call
forceFlush()at the end of your application to ensure all traces are exported. This is especially important in short-lived environments like serverless functions. - Verify that you are using the correct API keys and base URL.
- Call
- No OTel spans in the logs: Your application is not instrumented correctly. Make sure the instrumentation runs before your application code.
Unwanted observations in Langfuse
The Langfuse SDK is based on OpenTelemetry. Other libraries in your application may emit OTel spans that are not relevant to you. These still count toward your billable units, so you should filter them out. See Unwanted spans in Langfuse for details.
Missing attributes
Some attributes may be stored in the metadata object of the observation rather than being mapped to the Langfuse data model. If a mapping or integration does not work as expected, please raise an issue on GitHub.
Next Steps
Once you have instrumented your code, you can manage, evaluate and debug your application:
Manage prompts in Langfuse
Add evaluation scores
Run LLM-as-a-judge Evaluators
Create datasets
Create custom dashboards
Test queries in the Playground
Last updated on