Set up Sentry and Langfuse tracing in the Next.js project.
SetupVerified Sentry error capture end-to-end with debug logging.
VerifyAnalyzed security risk of `sendDefaultPii: true` capturing sensitive cookies and headers.
UnderstandDisabled `sendDefaultPii` and `includeLocalVariables` in client, server, and edge Sentry configs.
EditVerified safe defaults prevent PII leakage while preserving error capture.
VerifyI'll start by reading the Sentry skill file to follow the official setup steps.
The WebFetch summarized rather than returned verbatim. Let me get the actual detailed steps and inspect the project in parallel.
I have everything I need. This is Next.js 14 App Router, ESM, pnpm, no-semicolon style. The wizard is interactive (needs browser auth), so I'll do the manual setup adapted to the project's conventions. Let me isolate in a worktree and read the files I'll modify.
Let me check the middleware so I can exclude the tunnel route, and look at the full .gitignore env section.
Middleware matcher is path-scoped (won't catch /monitoring), so no tunnel exclusion needed there. Now let me install the SDK (pnpm) and create the config files. Starting the install in the background while I write files.
Now I'll create the Sentry config files, adapted to the project's no-semicolon, env-var-driven conventions.
Install completed (exit 0). Continuing with the error boundary and config wrapper.
Now wrap next.config.mjs. I'll read org/project from env vars (the SDK reads SENTRY_AUTH_TOKEN automatically) to keep credentials out of code per project conventions.
Now the env documentation. Adding the Sentry block to .env.example, the real .env.local, and gitignoring the build-plugin token file.
Now mirror the keys into .env.local (empty values, never committed) and gitignore the build-plugin token file.
Now let me verify it typechecks and the Sentry imports resolve.
Typecheck passes clean. Let me add a small verification endpoint (the wizard normally creates one) so the user can confirm errors reach Sentry, then run a production build to confirm withSentryConfig wraps correctly.
Build compiled successfully with withSentryConfig applied and types checked. The build then failed at page-data collection on /api/answer with Missing required environment variable: OPENAI_API_KEY — that's a pre-existing app requirement (the answer route reads the key at module load), entirely unrelated to Sentry. Sentry's webpack plugin and config wrapping passed cleanly.
Here's what I set up (Next.js 14 App Router, ESM, pnpm, matching your no-semicolon / env-var conventions):
Installed: @sentry/nextjs via pnpm.
New files:
instrumentation-client.ts — browser init: tracing, session replay (10% / 100%-on-error), logs, onRouterTransitionStart exportsentry.server.config.ts — Node runtime, includeLocalVariables: truesentry.edge.config.ts — edge runtimeinstrumentation.ts — register() routes by NEXT_RUNTIME, plus onRequestError for RSC errorsapp/global-error.tsx — App Router error boundaryapp/api/sentry-example/route.ts — GET /api/sentry-example verification endpoint (delete after testing)Modified:
next.config.mjs — wrapped with withSentryConfig (tunnelRoute /monitoring, widened uploads, org/project/authToken from env).env.example — documented NEXT_PUBLIC_SENTRY_DSN, SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN.env.local — same keys, blank values.gitignore — added .env.sentry-build-pluginPer your conventions I kept all credentials in env vars — nothing hardcoded. Source map upload only kicks in once SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN are set.
To finish (needs your Sentry account — I can't do these):
NEXT_PUBLIC_SENTRY_DSN and SENTRY_DSN in .env.local.SENTRY_ORG, SENTRY_PROJECT, and an auth token (project:releases + org:read scopes) for source maps.pnpm dev, hit http://localhost:3000/api/sentry-example, confirm the issue appears in Sentry, then delete that route.Work is isolated in the worktree-sentry-setup worktree; I didn't commit or push. Want me to commit it (and bring it onto a feature/sentry-setup branch per your git workflow)?
result: Sentry Next.js SDK wired into the project (client/server/edge config, instrumentation hooks, global-error boundary, withSentryConfig, env scaffolding) — typecheck + Sentry build wrapping pass; only remaining step is filling in DSN/auth-token from a Sentry account.
I'll explore the Langfuse skills repo to find the right skill, then apply it. Let me look at the repo structure.
Let me fetch the actual skill content and check the repo layout for instrumentation guidance.
Let me get the skill's file listing and clone it into the project's .claude/skills/.
Now let me install the skill into the project and read the SKILL.md plus the instrumentation reference.
Skill installed. Now let me read the SKILL.md and the instrumentation reference to follow its best practices.
The project uses the Vercel AI SDK (ai + @ai-sdk/openai), so the skill points me to the OpenTelemetry exporter integration. Following the "documentation first" principle, let me fetch the current Vercel AI SDK integration docs and explore the actual LLM call sites in parallel.
The doc URL 404'd. Let me find the correct current doc page via the search API and read the key LLM files.
Found the correct doc page. Let me fetch its full current content (Next.js specifics: span processor, registration, flush).
I have the current Langfuse v4 setup. Key consideration for this project: Sentry (just installed) already owns the global OpenTelemetry tracer provider. To follow best practices without clobbering Sentry, I'll give Langfuse its own NodeTracerProvider + span processor and pass an explicit tracer to each AI SDK call via experimental_telemetry.tracer — clean coexistence, no global-provider war.
Let me install the packages and verify the actual exports/types before writing code (grounding against installed source, not memory).
Installed (Langfuse v5.4.1). Let me verify the actual API surface — LangfuseSpanProcessor constructor options, @langfuse/tracing exports, and whether the AI SDK's experimental_telemetry accepts a tracer.
LANGFUSE_BASE_URL matches the existing .env.example — good. Now let me check @langfuse/tracing exports and confirm the AI SDK experimental_telemetry accepts a tracer.
Excellent — ai supports tracer, and @langfuse/tracing exports setLangfuseTracerProvider / getLangfuseTracer. That's the clean isolation path: register Langfuse's provider as Langfuse's dedicated provider (not the global OTel one Sentry owns), then pass getLangfuseTracer() explicitly to each AI SDK call. Let me confirm those signatures and the observe/updateActiveObservation shapes.
The isolation path is confirmed: getLangfuseTracer() falls back to the isolated provider set via setLangfuseTracerProvider(). Let me check ObserveOptions and LangfuseTraceAttributes (for userId/sessionId/tags + streaming span closure) before writing.
LangfuseTraceAttributes only carries input/output. Let me find where userId/sessionId/tags live in v5 (and the observe/startActiveObservation options).
Trace-level userId/sessionId/tags are set via propagateAttributes. Let me read its exact signature.
propagateAttributes is re-exported from @langfuse/core. Let me get its exact signature there.
I now have the full, verified API. Let me confirm startActiveObservation's signature (for the parent "answer" span), then I'll write the code.
I have the complete, verified API. Let me look at the ingestion pipeline and Trigger.dev task structure so I can instrument those call sites and place flushes correctly.
embedText is shared by ingest and the answer path's query embedding, so instrumenting it nests the query-embed under the answer trace automatically. Let me read the rest of the answer route and the retrieval pipeline entry to place the parent span correctly.
Now I have everything. Let me read the Trigger.dev tasks so I can initialize + flush tracing there too (ingestion runs outside Next.js, so instrumentation.ts won't fire there).
The Trigger tasks run outside Next.js, so I'll init + flush tracing there. One thing to verify before writing: this project pins Next 14.2, where after() may still be unstable_after. Let me check the installed Next's exports.
Let me confirm whether unstable_after exists in this Next version (the runtime require check is unreliable for ESM named exports).
unstable_after isn't available here, so I'll flush inside the streamText onFinish callback (and before the refusal return) instead — robust for next start/dev. Now writing the code, starting with the central observability module.
Now wire initTracing() into the Next.js instrumentation hook (Node runtime, after Sentry).
Now instrument the two answer-path LLM calls. First plan.ts:
Now synthesize.ts — add telemetry and an onFinish hook so the route can capture output, end the parent span, and flush:
Now embed.ts and enrich.ts:
Now the answer route — wrap the core logic in a parent "answer" span with propagateAttributes (userId, tags) and flush in onFinish/refusal paths:
Let me verify the LangfuseSpan handle has .update() / .end() and the valid level values before writing the route body.
Confirmed: span.update(...) is chainable, .end() exists, level: 'ERROR' is valid. Now rewriting the route's core logic.
Now the Trigger.dev ingest task — initialize tracing (instrumentation.ts doesn't run here), wrap the run in an ingest-source trace, and flush in finally.
All call sites instrumented. The .env.example already has LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY/LANGFUSE_BASE_URL, so no env changes needed. Let me typecheck.
@opentelemetry/api is transitive, not a direct dep. Since I import a type from it, I'll add it explicitly (it's already resolved at 1.9.0 in the tree).
Typecheck passes. Now let me run the build (expecting the same pre-existing OPENAI_API_KEY page-data error, but my code should compile) and the test suite.
Build compiles cleanly ("Compiled successfully"); the only failure is the same pre-existing OPENAI_API_KEY error during page-data collection (present before any of my changes — embed.ts requires it at module load). Tests: 9 passed. Let me prove my wiring is fully green by building with placeholder env vars so page-data collection can run.
Fully green build — page data collected, all routes bundled (including the new endpoints). Let me check git status to summarize the change set and confirm the skill location is tracked sensibly.
Everything is green. Let me record the non-obvious architectural decision (Langfuse/Sentry OTel coexistence) as a project memory for future sessions, then summarize.
Done. I installed the Langfuse skill from github.com/langfuse/skills, followed its instrumentation reference, and wired Langfuse tracing into the app following its best practices.
Verification: pnpm typecheck clean · pnpm test 9/9 pass · pnpm build fully green with placeholder env vars (all routes bundled, page data collected). Without placeholders the build stops at the same pre-existing OPENAI_API_KEY page-data error that predates these changes.
Installed the skill into .claude/skills/langfuse/ (SKILL.md + references). It pointed me to references/instrumentation.md, whose top rule is "use a framework integration over manual instrumentation." This app uses the Vercel AI SDK, so I used the OpenTelemetry exporter integration. I fetched the current Langfuse v5 docs (per the skill's "documentation first" rule) and grounded every API call against the installed .d.ts files rather than memory.
Key design decision — clean coexistence with Sentry. Sentry (set up last turn) already owns the global OpenTelemetry tracer provider. Rather than clobber it with provider.register(), I gave Langfuse its own isolated provider via setLangfuseTracerProvider() and pass getLangfuseTracer() explicitly to each AI SDK call through experimental_telemetry.tracer. Sentry keeps the global provider + context manager; Langfuse gets the LLM spans. No conflict.
Files:
lib/observability/langfuse.ts (new) — isolated provider, secret-masking hook, aiTelemetry() helper, initTracing()/flushTracing(). Opt-in: a complete no-op until both Langfuse keys are set.instrumentation.ts — initTracing() after Sentry in the Node runtime.app/api/answer/route.ts — wraps the read path in one answer trace (parent span via startActiveObservation, endOnExit:false) carrying user_id + tags + question/answer I/O. Plan, query-embed, and synthesis nest as child generations. Flushes in streamText onFinish (this Next 14.2 build has no after()).lib/retrieval/plan.ts, synthesize.ts, lib/ingestion/embed.ts, enrich.ts — descriptive functionIds on each call (plan-query, synthesize-answer, embed-text/texts, enrich-gloss).trigger/ingest.ts — initializes + flushes tracing itself (runs outside Next.js), grouping enrichment/embeddings under an ingest-source trace.Baseline requirements from the skill are met automatically by the integration (model name, token usage → cost, generation observation types) plus what I added: descriptive trace/span names, span hierarchy, user_id, tags, explicit trace I/O, and PII/secret masking.
To activate: set LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_BASE_URL in .env.local (the keys are already documented in .env.example). Then run a question through /ask and open the Traces view. Per the skill's guidance, explore those first before deciding what else to add — session_id is the obvious next addition once the Ask UI becomes multi-turn.
Work is in the sentry-setup worktree; I haven't committed or pushed. Want me to commit this (and the Sentry changes from earlier) onto a feature/observability branch?
result: Langfuse skill installed and LLM tracing wired into zrux via the Vercel AI SDK OTel integration — isolated tracer provider coexists with Sentry, answer path emits one nested answer trace (plan/embed/synthesis) with user_id+tags, ingestion traced too; typecheck + 9 tests + full build all green.
On it. I'll verify Sentry actually captures the error end-to-end (you added the DSNs), then commit both Sentry + Langfuse, push, and open a PR against main. Let me first check git/remote state and that the DSN env is present, and boot the dev server to test.
The worktree has its own .env.local (gitignored) with empty values — you likely edited the main checkout's .env.local. Let me check there.
The main checkout has the real DSNs. The worktree's .env.local is a separate gitignored copy with empty values. Let me copy just the Sentry DSN lines into the worktree env so the dev server can use them (still gitignored, won't be committed), then boot the server.
Now let me boot the dev server in the background and hit the Sentry test endpoint.
Let me wait for the server to be ready, then hit the Sentry test endpoint.
Server is ready. Let me hit the Sentry test endpoint and check the response plus server logs for the captured error.
The endpoint executed the capture path and flush(2000) completed (returning the success JSON). For definitive proof the event actually landed in Sentry, let me check if an auth token is available to query the Issues API; otherwise I'll enable Sentry debug logging to see the transport send.
No auth token, so I'll get transport-level proof another way: a small standalone script that sends a test event with Sentry debug logging on, using your DSN. This shows the actual envelope being sent — no committed-file changes.
Module resolution failed because the script was outside the project. Let me place it inside the worktree so node_modules resolves.
The command classifier is briefly unavailable. Let me wait and retry.
Init succeeded but my filter cut off before the send. Let me re-run and capture just the envelope-send and HTTP response lines.
Let me capture the full output and read the tail to see the send result.
The standalone .mjs hit an ESM/CJS interop quirk (captureException wasn't on the namespace) — not a real Sentry problem. The real route path is what matters, and it already ran capture + flush successfully. To get visible transport proof from the actual server, I'll add an env-gated debug flag to the server config (a clean keeper, off by default), restart with it on, and watch the envelope send. Let me find/stop the running dev server first.