<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sentry Blog</title><description>Product, Engineering, and Marketing updates from the developers of Sentry.</description><link>https://blog.sentry.io/</link><language>en-us</language><item><title>Works on my machine: how we use AI to reproduce reported bugs</title><link>https://blog.sentry.io/ai-bug-reproduction/</link><guid isPermaLink="true">https://blog.sentry.io/ai-bug-reproduction/</guid><description>How Sentry&apos;s SDK team built a Claude skill to auto-reproduce bug reports, reducing triage time across 159 packages.</description><pubDate>Mon, 08 Jun 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Sentry&amp;#39;s SDK teams maintain and support SDKs for a vast ecosystem of languages and frameworks. See our &lt;a href=&quot;https://github.com/getsentry/sentry-release-registry/tree/master/sdks&quot;&gt;release registry&lt;/a&gt; for a source of truth. We&amp;#39;re currently at 159 published packages across the entire ecosystem. If you use it, we probably support it.&lt;/p&gt;
&lt;p&gt;All of these SDKs are open source and have their own &lt;a href=&quot;https://github.com/getsentry/sentry-python&quot;&gt;GitHub&lt;/a&gt; &lt;a href=&quot;https://github.com/getsentry/sentry-javascript/&quot;&gt;repositories&lt;/a&gt; that we maintain on a daily basis. And like any other open source project, we get tons of bug reports and issues on these.&lt;/p&gt;
&lt;p&gt;In this post, I&amp;#39;ll talk about a Claude skill we&amp;#39;ve been leveraging to help make our reproduction flow smoother and reduce triage time and fatigue.&lt;/p&gt;
&lt;h2&gt;Bug triage flow&lt;/h2&gt;
&lt;p&gt;Sometimes bugs are easy to fix - could have been a missing null check, a missing conditional branch or some other small oversight.&lt;/p&gt;
&lt;p&gt;Other times, they aren&amp;#39;t so easy for a plethora of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tedious setup, or &amp;quot;boilerplate&amp;quot;, just to get the environment ready&lt;/li&gt;
&lt;li&gt;Esoteric code paths&lt;/li&gt;
&lt;li&gt;Legacy versions&lt;/li&gt;
&lt;li&gt;Edge case interactions no one thought of&lt;/li&gt;
&lt;li&gt;Data races and other concurrency problems&lt;/li&gt;
&lt;li&gt;Forked libraries with different contracts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Boilerplate&lt;/h3&gt;
&lt;p&gt;Particularly for our SDK bugs, the boilerplate factor is quite annoying. Let&amp;#39;s take a &lt;a href=&quot;https://github.com/getsentry/sentry-python/issues/5955&quot;&gt;recent example&lt;/a&gt;. To reproduce this, we would need to setup the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Python venv with the correct version&lt;/li&gt;
&lt;li&gt;A new Django boilerplate app with the correct version&lt;/li&gt;
&lt;li&gt;A Sentry SDK with the correct version&lt;/li&gt;
&lt;li&gt;Create a Django View that reproduces and showcases the exact problem which is applicable only to HTTPS proxies&lt;/li&gt;
&lt;li&gt;Run everything, trigger the view and hope that it shows the problem in question&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this is necessary just to acknowledge that the problem the original user reported is real and replicable. Once reproduced, it&amp;#39;s typically much easier to roll out the actual fix.&lt;/p&gt;
&lt;h3&gt;Reproduction papertrail&lt;/h3&gt;
&lt;p&gt;Another recurring discussion within the teams was how to keep track of all these one-off boilerplate apps that we used to test SDK logic, and reproduce/fix problems.&lt;/p&gt;
&lt;p&gt;Ideally we would have a shared repository of these apps with backlinks to the issues, but no one wanted the burden of maintaining yet another collection of apps on top of everything else we already do. Several SDK engineers had their own ad-hoc collection of apps they used for their day-to-day SDK development.&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;repro&lt;/code&gt; skill + repository&lt;/h2&gt;
&lt;p&gt;Enter LLMs. Turns out LLMs are pretty good at doing some of the tedious stuff mentioned above.&lt;/p&gt;
&lt;p&gt;Even if they cannot get to the root of a hairy problem, they at least set up the boilerplate and give me a playground with all the correct parameters which I can move forward with, massively reducing tedium.&lt;/p&gt;
&lt;p&gt;So I wrote up and iterated on a &lt;a href=&quot;https://github.com/getsentry/repro/blob/main/.claude/skills/repro/SKILL.md&quot;&gt;Claude skill&lt;/a&gt; that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Takes a GitHub issue URL as input&lt;/li&gt;
&lt;li&gt;Parses the SDK language, issue number&lt;/li&gt;
&lt;li&gt;Gathers metadata on language version, framework version, SDK version&lt;/li&gt;
&lt;li&gt;Makes a new directory and branch from the &lt;code&gt;language/issue-number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Attempts to create a minimal reproduction using standard tooling for the language (&lt;code&gt;uv&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;bundle&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Tries to run the reproduction, bails out if it&amp;#39;s too complicated&lt;/li&gt;
&lt;li&gt;Writes up clear instructions for running the reproduction&lt;/li&gt;
&lt;li&gt;Makes a PR&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optionally&lt;/strong&gt; adds a backlink to the PR to the original user issue (using Claude&amp;#39;s &lt;a href=&quot;https://code.claude.com/docs/en/agent-sdk/user-input&quot;&gt;&lt;code&gt;AskUserQuestion&lt;/code&gt; tool&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that we only ask the LLM to &lt;em&gt;attempt&lt;/em&gt; a reproduction and stop if too complicated. This sort of logic is very effective when working with agents since if we ask too much of them, they will often stumble. If we give them an out, they&amp;#39;re more likely to explain the challenge than just stumble through it.&lt;/p&gt;
&lt;h3&gt;Example run on the Python issue&lt;/h3&gt;
&lt;p&gt;Continuing with the above Python example, the skill created &lt;a href=&quot;https://github.com/getsentry/repro/pull/41&quot;&gt;this reproduction&lt;/a&gt;. We can see that it created a minimal Django app and gave very clear instructions to run the reproduction. Using this basic setup, I was able to roll out &lt;a href=&quot;https://github.com/getsentry/sentry-python/pull/5963&quot;&gt;the subsequent fix&lt;/a&gt; very rapidly. I probably saved a few hours of figuring out how to setup Django with an HTTPS proxy correctly and then examining how that interacts with our SDK logic.&lt;/p&gt;
&lt;h2&gt;Lessons on writing skills&lt;/h2&gt;
&lt;p&gt;Skills are very generic Markdown files so it&amp;#39;s a bit opaque how to make them reliable and avoid having them go off the rails.&lt;/p&gt;
&lt;p&gt;Some insights I have from writing this one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use CLIs to interact with other systems; here we&amp;#39;re using the &lt;a href=&quot;https://github.com/getsentry/repro/blob/e7bbd6e958bbe4e7ab070c0142e2c4dc4fa938ad/.claude/skills/repro/SKILL.md?plain=1#L31-L35&quot;&gt;&lt;code&gt;gh&lt;/code&gt; CLI&lt;/a&gt; to perform GitHub operations&lt;/li&gt;
&lt;li&gt;Split out the work to be done into clear steps&lt;/li&gt;
&lt;li&gt;Add an &lt;a href=&quot;https://github.com/getsentry/repro/blob/e7bbd6e958bbe4e7ab070c0142e2c4dc4fa938ad/.claude/skills/repro/SKILL.md?plain=1#L165-L169&quot;&gt;&lt;code&gt;Error Handling&lt;/code&gt;&lt;/a&gt; section explaining what&amp;#39;s not allowed and what to do with bad inputs&lt;/li&gt;
&lt;li&gt;Use other in-built tools such as &lt;code&gt;AskUserQuestion&lt;/code&gt; for user input or validation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Full automation?&lt;/h2&gt;
&lt;p&gt;We will play around with fully automating this flow on GitHub issues in the future. A major concern voiced by several engineers here is increased bot noise. We&amp;#39;re already drowning in bot communication on several fronts so we want to be careful how many of these we enable automatically. The right amount of automation in any given problem space is not always full automation and a pair of human eyes in the right places are absolutely necessary.&lt;/p&gt;
</content:encoded></item><item><title>Errors, traces, logs, metrics: when to reach for what</title><link>https://blog.sentry.io/errors-traces-logs-metrics-when-to-reach-for-what/</link><guid isPermaLink="true">https://blog.sentry.io/errors-traces-logs-metrics-when-to-reach-for-what/</guid><description>Errors, traces, logs, and metrics overlap enough that it&apos;s hard to know which to use. Here&apos;s when to reach for each signal, with a real debugging walkthrough.</description><pubDate>Fri, 05 Jun 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When should I reach for a log, a trace, or a metric? I hit that question constantly when I instrument code, and I watch coding agents hit it too. It sounds like it should be obvious. Errors, traces, logs, and metrics are the four kinds of telemetry most apps run on, four tools in one box, and they overlap enough that the honest answer is every developer&amp;#39;s favourite: &lt;em&gt;it depends&lt;/em&gt;. You can stuff context into span attributes instead of logging it. You can count log events instead of emitting a metric. You can add a duration to a log and call it a span.&lt;/p&gt;
&lt;p&gt;[I had a spiderman meme here but legal told me it would be infringing so I removed it]&lt;/p&gt;
&lt;p&gt;But the fact that you &lt;em&gt;can&lt;/em&gt; doesn&amp;#39;t mean you &lt;em&gt;should&lt;/em&gt;. Each signal exists because it answers a different question, and feeds a different workflow once it lands. Left without solid guidelines, the default is to reach for whatever&amp;#39;s most familiar or already there, and miss what the other kinds are for.&lt;/p&gt;
&lt;p&gt;This post is the guidance I wanted to have, for myself and my robots. Want just the skill? &lt;a href=&quot;#getting-started&quot;&gt;Skip to the end&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In Sentry, errors, traces, logs, and metrics all come from one SDK, included on every plan. Errors and &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;tracing&lt;/a&gt; have been around for years (&lt;a href=&quot;https://blog.sentry.io/the-story-of-sentry/&quot;&gt;2012&lt;/a&gt; and &lt;a href=&quot;https://blog.sentry.io/see-slow-faster-with-performance-monitoring/&quot;&gt;2020&lt;/a&gt;), &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;structured logs landed last year&lt;/a&gt;, and &lt;a href=&quot;https://sentry.io/product/metrics/&quot;&gt;Application Metrics&lt;/a&gt; completed the set back in May of this year. If you&amp;#39;ve had your application instrumented with Sentry for a while, errors and traces are probably already flowing, with logs and metrics left as tools for you to complete your telemetry story.&lt;/p&gt;
&lt;h2&gt;Errors, traces, logs, metrics: one question each&lt;/h2&gt;
&lt;h4&gt;&lt;a href=&quot;https://docs.sentry.io/product/issues/&quot;&gt;Errors&lt;/a&gt;: &amp;quot;What just broke?&amp;quot;&lt;/h4&gt;
&lt;p&gt;A stack trace and an exception type, grouped into an Issue that gets deduplicated, assigned, and tracked until it&amp;#39;s resolved. If your code threw an exception, it&amp;#39;s an error.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://docs.sentry.io/product/explore/trace-explorer/&quot;&gt;Traces&lt;/a&gt;: &amp;quot;Did the request flow the way it was supposed to?&amp;quot;&lt;/h4&gt;
&lt;p&gt;A trace is a waterfall of timed spans. It&amp;#39;s how you follow a request across your services and see where the time went: the DB query that dragged, the API call that timed out, the LLM tool call that took 8 seconds instead of 200ms.&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://docs.sentry.io/product/explore/metrics/&quot;&gt;Metrics&lt;/a&gt;: &amp;quot;How&amp;#39;s this trending over time?&amp;quot;&lt;/h4&gt;
&lt;p&gt;Counters, gauges, and distributions, each kept as an individual measurement you can slice by any attribute and drill from an aggregate back into the samples (and the trace) behind it. Not just &amp;quot;12,000 checkouts this week,&amp;quot; but 8,400 from the US, 2,600 from the EU, and 1,000 from everywhere else, and how that line moved across the last deploy. Metrics are a historical signal as much as a right-now one, which makes them an easy candidate for dashboards and alerts (but you can still set up alerts on pretty much all signals from Sentry).&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://docs.sentry.io/product/explore/logs/&quot;&gt;Logs&lt;/a&gt;: &amp;quot;What was happening at this point in the code?&amp;quot;&lt;/h4&gt;
&lt;p&gt;The state of the system at one specific moment, captured as a structured event: config values, feature flags, the inputs and outputs of a function, the user ID. Logs are the trail through a function&amp;#39;s decision tree: the markers you drop at the points where the code makes a choice, so that later, a human or an agent can follow the reasoning. They fill in the &lt;em&gt;why&lt;/em&gt; once errors and traces have told you what broke and where the time went.&lt;/p&gt;
&lt;h2&gt;A real(ish) world example&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s say you run a storefront with a React frontend and a Python API. Support starts forwarding tickets: the product recommendations on the account page look generic for a chunk of logged-in customers: bestsellers, not the personalized picks they&amp;#39;re used to. The vibes are off.&lt;/p&gt;
&lt;h3&gt;Did anything crash?&lt;/h3&gt;
&lt;p&gt;First place I&amp;#39;d look is Issues. No exception in the React app, no failed request, every call to &lt;code&gt;/recommendations/{user_id}&lt;/code&gt; came back 200. As far as error tracking is concerned, the app is perfectly healthy.&lt;/p&gt;
&lt;h3&gt;Was anything slow, or did the request go off-path?&lt;/h3&gt;
&lt;p&gt;Pull a trace for one of the affected requests. The route and the database queries are auto-instrumented; I added a few &lt;a href=&quot;https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/#add-spans-to-a-transaction&quot;&gt;named spans&lt;/a&gt; for the recommendation steps:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/errors-traces-logs-metrics-when-to-reach-for-what/recommendations-trace-waterfall.png&quot; alt=&quot;An affected request&apos;s trace in Sentry: an http.server span for the GET /recommendations route over child spans for the user lookup, the ranking_v2 flag check, the empty recommendations_v2 query, the fallback to popular items, and ranking.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The request loaded the user, evaluated the &lt;code&gt;ranking_v2&lt;/code&gt; flag, queried &lt;code&gt;recommendations_v2&lt;/code&gt;, fell back to popular items, and ranked them. The path is right and the timing&amp;#39;s fine. That &lt;code&gt;recommendations_v2&lt;/code&gt; query &lt;em&gt;succeeded&lt;/em&gt; (returning zero rows is a perfectly successful query), so the code did what it was built to do and fell back. The trace tells me the request flowed as designed. It can&amp;#39;t tell me the design just quietly failed this user. On the surface, everything is fine.&lt;/p&gt;
&lt;h3&gt;Can we dig a little deeper?&lt;/h3&gt;
&lt;p&gt;Search the logs for the user from the ticket, and the structured log from inside the handler will give you the state at the moment it decided to fall back.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/errors-traces-logs-metrics-when-to-reach-for-what/recommendations-logs-user-search.png&quot; alt=&quot;The recommendations lookup log for user.id 124 in Sentry, expanded to show its attributes: the ranking_v2 flag is on, source_table is recommendations_v2, candidate_count is 0, and outcome is fallback.&quot;&gt;&lt;/p&gt;
&lt;p&gt;This user got bucketed into the &lt;code&gt;ranking_v2&lt;/code&gt; feature flag, which reads personalized picks from a new &lt;code&gt;recommendations_v2&lt;/code&gt; table. The table shipped, but the rows were never backfilled, so the lookup came back empty. To the code, an empty result is a perfectly valid &amp;quot;no personalized recs for this user,&amp;quot; the same thing a brand-new user with no history would get. So it falls back to bestsellers and returns 200.&lt;/p&gt;
&lt;p&gt;Why not just attach this data on the span? You could set &lt;code&gt;outcome&lt;/code&gt; and &lt;code&gt;candidate_count&lt;/code&gt; as span attributes. But traces might be sampled, and the one request a customer is complaining about &lt;em&gt;usually&lt;/em&gt; ends up being the one that&amp;#39;s sampled out (at least with my luck). A span attribute is great for reading a trace you&amp;#39;ve found; it can&amp;#39;t help you find one. Logs aren&amp;#39;t sampled.&lt;/p&gt;
&lt;h3&gt;How many people hit it?&lt;/h3&gt;
&lt;p&gt;One affected customer is a support ticket. Knowing whether it&amp;#39;s a small subset of users or a significant chunk is the difference between fixing it Monday and paging someone tonight. A &lt;code&gt;recommendations.served&lt;/code&gt; counter, tagged with &lt;code&gt;ranking_version&lt;/code&gt; and &lt;code&gt;outcome&lt;/code&gt;, draws the line:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/errors-traces-logs-metrics-when-to-reach-for-what/recommendations-metric-rate.png&quot; alt=&quot;Sentry&apos;s Application Metrics explorer showing the recommendations.served counter with two queries (one filtered to outcome:personalized, one for the total) and an equation A / B * 100 grouped by ranking_version, producing a personalized rate of 97.9% for v1 and 3.3% for v2.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The v2 path is serving almost nothing but fallbacks, v1 is normal, and the drop lines up with the flag rollout. Scope and trigger, without opening a single trace.&lt;/p&gt;
&lt;p&gt;No one signal cracked it; each ruled something out. No Issues in the feed meant it wasn&amp;#39;t a crash. The metric said it wasn&amp;#39;t a one-off: the whole &lt;code&gt;v2&lt;/code&gt; cohort was falling back. The trace, where one was sampled, showed the path running exactly as designed, which is why it slipped through. The log, pulled up by the &lt;code&gt;user_id&lt;/code&gt; from the ticket, said &lt;em&gt;why&lt;/em&gt;, and I never needed the trace to get to it.&lt;/p&gt;
&lt;h2&gt;When to reach for what&lt;/h2&gt;
&lt;p&gt;I use this as a gut check:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want to know&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Something crashed, show the stack trace&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Errors&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How long did this take? Which step is slow?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did the request flow through the steps I expected?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Traces&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What was the state when the code made this decision?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What did this function receive and return?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Logs&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How often does X happen? Is the rate normal?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Did something change after the deploy?&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Metrics&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The tricky cases are the overlaps, and of course there is nuance to all of this because the same value can show up in more than one signal.&lt;/p&gt;
&lt;h4&gt;Span attribute or metric?&lt;/h4&gt;
&lt;p&gt;If it&amp;#39;s context about &lt;em&gt;one request&amp;#39;s flow through the system&lt;/em&gt; and you want it while reading that trace, it&amp;#39;s a span attribute. It rides on the span in the waterfall. If it&amp;#39;s a standalone value you want to chart, alert on, or slice over time across &lt;em&gt;all&lt;/em&gt; requests, it&amp;#39;s a metric. The same number can warrant both: &lt;code&gt;candidate_count&lt;/code&gt; as a span attribute lets me read one request; &lt;code&gt;recommendations.served&lt;/code&gt; as a metric lets me watch the rate. One is for inspecting a single flow, the other for watching the aggregate.&lt;/p&gt;
&lt;h4&gt;Log or span?&lt;/h4&gt;
&lt;p&gt;The span is the timed node in the flow, and most of them are auto-instrumented, so you rarely write them. The log is the decision-point state &lt;em&gt;inside&lt;/em&gt; that node, and you always write it on purpose. Span answers &lt;em&gt;where&lt;/em&gt; and &lt;em&gt;how long&lt;/em&gt;; log answers &lt;em&gt;what was true and why&lt;/em&gt;.&lt;/p&gt;
&lt;h4&gt;Log or metric?&lt;/h4&gt;
&lt;p&gt;A log is one request&amp;#39;s story, the needle. A metric is the aggregate, the question of whether the haystack is normal. When you want to find the specific request that went wrong, that&amp;#39;s a log. When you want to know how many requests went wrong, that&amp;#39;s a metric.&lt;/p&gt;
&lt;h4&gt;Error or log?&lt;/h4&gt;
&lt;p&gt;If it needs a stack trace and should be tracked as an Issue, it&amp;#39;s an error. If it&amp;#39;s an unexpected-but-handled condition worth recording, it&amp;#39;s a log. If it&amp;#39;s truly non-critical, &lt;code&gt;logger.warning(exc_info=True)&lt;/code&gt; captures the traceback in logs without creating noise in your error feed.&lt;/p&gt;
&lt;h2&gt;What the instrumentation looks like&lt;/h2&gt;
&lt;p&gt;Everything above came out of one endpoint: the &lt;code&gt;GET /recommendations/{user_id}&lt;/code&gt; route from the walkthrough, the function that loads the user, checks the &lt;code&gt;ranking_v2&lt;/code&gt; flag, queries &lt;code&gt;recommendations_v2&lt;/code&gt;, and falls back to popular items when it comes back empty. Here&amp;#39;s that same handler with the instrumentation in place.&lt;/p&gt;
&lt;p&gt;Most of it you don&amp;#39;t write. The FastAPI integration traces the request, the database integration traces every query, so you get the path and the timing without a single hand-written span.&lt;/p&gt;
&lt;p&gt;What you do place by hand are the deliberate signals: a span attribute or two to enrich the flow, the decision-point log, and the metric.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;
from sentry_sdk import logger

# The route is auto-instrumented. FastAPI gives you the request span;
# the DB integration gives you a span for every query below. You write none of it.
@app.get(&amp;quot;/recommendations/{user_id}&amp;quot;)
def get_recommendations(user_id: int):
    user = db.get_user(user_id)                          # auto-instrumented db span
    use_v2 = flag_enabled(&amp;quot;ranking_v2&amp;quot;, user)
    ranking_version = &amp;quot;v2&amp;quot; if use_v2 else &amp;quot;v1&amp;quot;

    candidates = db.personalized_recs(user_id, version=ranking_version)  # auto db span
    outcome = &amp;quot;personalized&amp;quot; if candidates else &amp;quot;fallback&amp;quot;
    items = candidates or db.popular_items()             # auto db span on the fallback

    # SPAN ATTRIBUTE: context about THIS request&amp;#39;s flow, read inside the trace.
    # It rides on the auto-instrumented request span; no new span needed.
    span = sentry_sdk.get_current_span()
    span.set_data(&amp;quot;ranking_version&amp;quot;, ranking_version)
    span.set_data(&amp;quot;recommendation.outcome&amp;quot;, outcome)

    # LOG: the trail through the decision tree, the state at the moment the
    # code chose personalized vs. fallback. The only signal that records *why*.
    logger.info(
        &amp;quot;recommendations lookup&amp;quot;,
        attributes={
            &amp;quot;user_id&amp;quot;: user_id,
            &amp;quot;ranking_version&amp;quot;: ranking_version,
            &amp;quot;flag.ranking_v2&amp;quot;: use_v2,
            &amp;quot;source_table&amp;quot;: f&amp;quot;recommendations_{ranking_version}&amp;quot;,
            &amp;quot;candidate_count&amp;quot;: len(candidates),
            &amp;quot;outcome&amp;quot;: outcome,
        },
    )

    # METRIC: the rate across all requests, sliceable by version and outcome.
    sentry_sdk.metrics.count(
        &amp;quot;recommendations.served&amp;quot;,
        1,
        attributes={&amp;quot;ranking_version&amp;quot;: ranking_version, &amp;quot;outcome&amp;quot;: outcome},
    )

    return items
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three deliberate touches, each carrying a piece the others can&amp;#39;t. The span attribute tags the request&amp;#39;s flow with the ranking path so it&amp;#39;s right there when I open the trace. The log records what the function decided and why, at the instant it decided. The metric counts the outcome with enough dimension to slice it later.&lt;/p&gt;
&lt;p&gt;If you &lt;em&gt;do&lt;/em&gt; want a sub-operation timed in the waterfall (say the ranking step, or a call to an external recommender), you can wrap it in a custom span with &lt;a href=&quot;https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/&quot;&gt;&lt;code&gt;sentry_sdk.start_span&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Beyond what you write, the SDK fills in even more on its own. Frontend SDKs tag everything with the browser, OS, and release. Call &lt;code&gt;sentry_sdk.set_user()&lt;/code&gt; once and that user follows the errors, spans, logs, and metrics for the request. And because all four come from the same SDK, they share a &lt;code&gt;trace_id&lt;/code&gt; and correlate on their own: every log carries the trace it belongs to, and you can jump from a metric spike straight into the traces behind it, without gluing four vendors together to get there.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/errors-traces-logs-metrics-when-to-reach-for-what/trace-connected-waterfall.png&quot; alt=&quot;Sentry trace view for the GET /recommendations route: the http.server route span and the database query spans are auto-instrumented, alongside a few custom spans for the recommendation steps, with Waterfall, Logs, and Application Metrics tabs all hanging off the same trace.&quot;&gt;&lt;/p&gt;
&lt;p&gt;All of this is ready for you to use and included in every plan. The deliberate signals (the span attributes, the decision-point logs, the metrics) are the ones you place yourself, and they only help if you do it ahead of time, at the spots where your code makes a decision worth questioning later.&lt;/p&gt;
&lt;h2&gt;Right tool for the job&lt;/h2&gt;
&lt;p&gt;The split above isn&amp;#39;t just conceptual. It&amp;#39;s baked into the APIs, and each one is tuned for its job. The &lt;strong&gt;Metrics API&lt;/strong&gt; is built for emitting counts and measures you&amp;#39;ll aggregate. The &lt;strong&gt;span API&lt;/strong&gt; is built for measuring durations and the shape of a request. The &lt;strong&gt;log API&lt;/strong&gt; integrates with your favourite &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;structured logging&lt;/a&gt; library, so the lines you already write become queryable events. Reaching for the API that matches the workflow usually means reaching for the one that matches the &lt;em&gt;kind&lt;/em&gt; of value you have: a count, a duration, or a moment.&lt;/p&gt;
&lt;p&gt;Sampling falls out of the same logic. Traces are best as a &lt;a href=&quot;https://docs.sentry.io/platforms/python/tracing/configure-sampling/&quot;&gt;&lt;em&gt;sampled representation&lt;/em&gt;&lt;/a&gt; of your traffic: you don&amp;#39;t need every request to understand where time goes, so a percentage is plenty (and cheaper). Logs are the opposite: you keep all of them, because the entire point is to find the one rare request that went sideways, and you can&amp;#39;t find what you sampled away. Metrics aren&amp;#39;t sampled either; like logs, you filter them with &lt;a href=&quot;https://docs.sentry.io/platforms/python/metrics/#before_send_metric&quot;&gt;&lt;code&gt;before_send_metric&lt;/code&gt;&lt;/a&gt;. Match the retention to the question: a representative sample for &amp;quot;where does time go,&amp;quot; every single event for &amp;quot;what happened to &lt;em&gt;this&lt;/em&gt; request.&amp;quot;&lt;/p&gt;
&lt;h2&gt;You&amp;#39;re not the only one debugging your codebase anymore&lt;/h2&gt;
&lt;p&gt;Cody from &lt;a href=&quot;https://modem.dev/&quot;&gt;Modem&lt;/a&gt; instrumented his AI agent to find out where it was spending time. He worked with Codex to wrap the async work and the logical chunks (everything that runs before the call to the model, say) in spans. Cache hits and time-to-first-token became metrics he could watch over time. Values that only meant something next to a specific operation stayed as span attributes, and the lightweight &amp;quot;this happened here&amp;quot; markers became logs. The span-attribute-versus-metric call wasn&amp;#39;t always obvious to him; his rule was that if a value only made sense in the context of a span, it lived on the span.&lt;/p&gt;
&lt;p&gt;With the tracing in place, he pointed Codex at the Sentry data through the MCP server, feeding it real runs from his Playwright tests in development, and gave it one goal: optimize the code path. The agent read the spans, found work that could run in parallel, and rewrote the code to stop awaiting results until they were actually needed.&lt;/p&gt;
&lt;p&gt;It could do that because a trace is a structured dependency tree with timing on every node, a format an agent can reason about directly. Hand it the same information as a stream of log lines and it would have to reconstruct the call graph from timestamps and string matching first.&lt;/p&gt;
&lt;h2&gt;But what about wide events?&lt;/h2&gt;
&lt;p&gt;There&amp;#39;s a popular argument that the four signals are overkill: emit one rich, wide event per request and derive the rest later. It&amp;#39;s half right.&lt;/p&gt;
&lt;p&gt;Emit wide, absolutely. The best version of any signal is a structured event packed with context (the flag that was on, the user, the inputs and the outputs), not a bare number or a one-line string.&lt;/p&gt;
&lt;p&gt;But the shape you emit is the shape you get to work with. One fat event in a columnar store charts fine after the fact, but it can&amp;#39;t group itself into a deduplicated Issue, render itself as a waterfall, or fire a real-time alert on a threshold you haven&amp;#39;t defined yet. Those are workflows, and each needs its data in a particular shape.&lt;/p&gt;
&lt;p&gt;So emit wide, into the signal whose workflow you actually need. That&amp;#39;s why the handler emits both a metric and a log: same decision, same trace, two shapes, because watching a rate and reconstructing one request are different jobs.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;Logs and metrics are the two you probably haven&amp;#39;t turned on yet — they’re relatively new to Sentry, and people are still just finding them. Both are included on every plan.&lt;/p&gt;
&lt;p&gt;You don&amp;#39;t have to wire them up by hand. Point your coding agent at &lt;a href=&quot;https://skills.sentry.dev/&quot;&gt;Sentry&amp;#39;s setup skills&lt;/a&gt; for your stack and it installs the SDK, turns on tracing, logs, and metrics, and drops instrumentation at the decision points. Then aim it at your Sentry data through the &lt;a href=&quot;https://mcp.sentry.dev/&quot;&gt;MCP server&lt;/a&gt; and give it something real: your slowest trace, your newest issue.&lt;/p&gt;
&lt;p&gt;Prefer to grab just the decision framework? It&amp;#39;s a skill of its own:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add getsentry/sentry-for-ai --skill sentry-instrumentation-guide
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The telemetry you emit to debug is the same telemetry it reads to help.&lt;/p&gt;
</content:encoded></item><item><title>How we cut build times by two-thirds by deleting our CMS</title><link>https://blog.sentry.io/cut-build-times-delete-cms/</link><guid isPermaLink="true">https://blog.sentry.io/cut-build-times-delete-cms/</guid><description>Sentry replaced its CMS with Astro, Markdown, and Claude Code skills — cutting build times from 14 to under 4 minutes and eliminating API failures.</description><pubDate>Thu, 28 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;At Sentry, we&amp;#39;re obsessed with things not breaking. It&amp;#39;s kind of our whole deal. But for a while, our own marketing site was testing that obsession.&lt;/p&gt;
&lt;p&gt;Much of what you see on &lt;a href=&quot;https://sentry.io&quot;&gt;sentry.io&lt;/a&gt; (the marketing site, blog, open source microsite, etc.) were running on a fleet of legacy Gatsby sites powered by a traditional headless CMS. On paper, it worked. In practice, we were juggling a fragile web of plugins, restrictive schemas, and external API dependencies that loved to fail right when we needed to ship.&lt;/p&gt;
&lt;p&gt;So, we did what any sane engineering team would do: we ripped it out and replaced it with Astro, Markdown, and AI-driven automation.&lt;/p&gt;
&lt;h2&gt;The problem: the &amp;quot;headless&amp;quot; headache&lt;/h2&gt;
&lt;p&gt;Our old stack was starting to feel like a Rube Goldberg machine.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The build bottleneck:&lt;/strong&gt; Gatsby&amp;#39;s consolidated data layer was convenient, but as our content grew, our build times ballooned to ~14 minutes/build. At an average of 95 builds/day, this ended up being around 22 build hours used daily.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The CMS tax:&lt;/strong&gt; We managed the content of ~2500 pages in a single CMS instance. We have different page schemas and connected component schemas without the ability to have conditional fields, so we ended up buying a conditional fields plugin to avoid hitting the schema limit. This made for an additional annual subscription on top of our monthly subscription, and scalability was still limited.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External fragility:&lt;/strong&gt; Every build relied on external CMS and marketing automation system APIs via Gatsby plugins. During the last month before we started the rebuild, an issue with the CMS&amp;#39;s Gatsby plugin would fail 3-5 times a day (to which there was no resolution, even after submitting a support ticket) and the marketing automation API would also fail multiple times a day due to rate limits (more on that below).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The solution: Astro and the power of &amp;quot;just files&amp;quot;&lt;/h2&gt;
&lt;p&gt;We migrated the framework to Astro. We chose it because it&amp;#39;s built for the modern web — fast by default and incredibly flexible.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vite-powered speed:&lt;/strong&gt; Moving to Vite meant our local development and production builds finally felt like they belonged in 2026. We reduced our build times from ~14 minutes to less than 4 minutes, resulting in a savings of ~15.8 build hours daily.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Framework agnostic:&lt;/strong&gt; Astro lets us use the best tool for the job. If a component works better in React, we use React. If it&amp;#39;s a simple static partial, it&amp;#39;s just HTML/CSS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vercel for the heavy lifting:&lt;/strong&gt; We offloaded image processing to Vercel, ensuring our assets are optimized without dragging down the build process.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But the biggest shift wasn&amp;#39;t the framework — it was how we handled content. We ditched the headless CMS UI for Markdown and Frontmatter.&lt;/p&gt;
&lt;h2&gt;AI-native content management (without the SaaS bloat)&lt;/h2&gt;
&lt;p&gt;Instead of paying for an &amp;quot;AI Add-on&amp;quot; from a CMS provider, we built a direct integration with Claude Skills.&lt;/p&gt;
&lt;p&gt;Now, when someone needs to update the site, they don&amp;#39;t log into a bloated dashboard. They use a skill-driven workflow that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Guides the user through a process that precisely updates the Markdown files and Frontmatter directly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generates a live preview.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Drafts a Pull Request for review.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Why go custom instead of using a CMS-integrated AI?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero dependencies:&lt;/strong&gt; Content lives in the repo. No API outages mean no failed builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unlimited schemas:&lt;/strong&gt; With Frontmatter, we define the structure. If we need a new field or schema type, we just add it. No subscription tiers, no restrictions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &amp;quot;Sentry&amp;quot; way:&lt;/strong&gt; For a company with a developer-first culture that values the deeply technical, managing content as code feels right. It&amp;#39;s version-controlled, peer-reviewed, and lives right next to the components that render it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The process: how we did it&lt;/h2&gt;
&lt;p&gt;Moving a site with ~2500 pages between our marketing site and blog is a massive undertaking. We had a team of 2.5 developers and a two-month window to get it done.&lt;/p&gt;
&lt;p&gt;Because of the small team size and large site volume, we relied on Claude Code for much of the coding. Our developers spent the bulk of their time on planning, scoping, and developing requirements, then reviewing the code, directing changes, and fine-tuning the output.&lt;/p&gt;
&lt;h3&gt;Scoping&lt;/h3&gt;
&lt;p&gt;This was easier for this project for 2 reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We use a monorepo for these websites so the bots had the full context of what was being built and migrated&lt;/li&gt;
&lt;li&gt;We did not implement any net-new design&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since we&amp;#39;ve been working in the existing codebase for over a few years, we had some ideas of where we could easily remove code bloat. We took a deeper look at the pages in certain directories to validate, and based on the content, eliminate bespoke pages with templates. As a result, we consolidated ~200 pages into 3 templates, making the site DRY and significantly easier to maintain.&lt;/p&gt;
&lt;h3&gt;Building with bots&lt;/h3&gt;
&lt;p&gt;We started with the data. Since our headless CMS was plugged into our Gatsby site and pre-existing parts of the Astro site, and our headless CMS provided JSON files of each schema available, we provided the planning agent with existing CMS schema and had it duplicated in Frontmatter. Since we were already connected to the CMS&amp;#39;s API, we had an agent swarm pull down the data, map it to Frontmatter files, and pull down the images and save them locally as either an &lt;code&gt;asset&lt;/code&gt; (for all non-meta image images needing image optimization) or in the &lt;code&gt;public&lt;/code&gt; directory for SEO images.&lt;/p&gt;
&lt;p&gt;Once the data was in place, we provided plan agents with the location of the existing template files in Gatsby, directions on where to place it in the new framework, what data it was using, and asked the agent to interview us on any missing information. From there, the planning agent would pass along the build tests to general purpose agents for the build, which was passed on to general purpose agents for testing.&lt;/p&gt;
&lt;p&gt;After that round, our team would review &amp;amp; fix any regressions, which was followed with AI PR reviews using both Sentry&amp;#39;s &lt;a href=&quot;https://sentry.io/product/seer/ai-code-review/&quot;&gt;Seer AI code review tool&lt;/a&gt; and Cursor&amp;#39;s Bugbot (along with other quality checks built into the repo, including secret scanning and our standard automated tests).&lt;/p&gt;
&lt;h3&gt;Testing with bots&lt;/h3&gt;
&lt;p&gt;As part of our development process, we experimented with Claude running visual regression tests with Playwright and a homegrown MCP we built to compare visual elements from our Gatsby site to the new Astro replacement.&lt;/p&gt;
&lt;h4&gt;The DOM-inspector MCP&lt;/h4&gt;
&lt;p&gt;Big shoutout to Dylan Coots on our team who built a DOM Inspector MCP that uses Puppeteer (headless Chrome) to connect to a locally running dev server and programmatically inspect, measure, and interact with elements on the page. It was designed to find UI layout issues like spacing shifts, element dimensions, and computed styles that can be passed along to a bot to fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core Architecture&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;DOMInspector&lt;/code&gt; class&lt;/strong&gt; — the central object that owns the Puppeteer browser/page lifecycle. It has two modes: a fully-owned browser (launched by the class) and a session-managed mode where an external page is passed in via the static &lt;code&gt;fromPage()&lt;/code&gt; factory method.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Browser management&lt;/strong&gt; — launches a headless Chrome instance with memory-constrained flags (&lt;code&gt;--max-old-space-size=256&lt;/code&gt;, limited renderer processes) and handles graceful shutdown with a 5-second timeout before force-killing the process by PID if needed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DOM inspection methods&lt;/strong&gt; — includes &lt;code&gt;inspectElement()&lt;/code&gt; (dimensions + computed CSS), &lt;code&gt;measureDistance()&lt;/code&gt; (pixel/rem gap between two elements including which CSS property creates it), &lt;code&gt;measureLayoutShift()&lt;/code&gt; (reloads the page and diffs element positions before/after a transition), and &lt;code&gt;debugPage()&lt;/code&gt; (scans the DOM for common component patterns when a selector isn&amp;#39;t found).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Interaction &amp;amp; navigation&lt;/strong&gt; — &lt;code&gt;interactWithElement()&lt;/code&gt; clicks a selector and measures before/after state; &lt;code&gt;navigateToUrl()&lt;/code&gt; navigates to a new URL and clears stale console logs; &lt;code&gt;setViewport()&lt;/code&gt; supports named presets (mobile/tablet/desktop/large) or any custom pixel width with auto-calculated height.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Utility methods&lt;/strong&gt; — &lt;code&gt;screenshot()&lt;/code&gt; (full page or scoped to a selector, returned as base64), &lt;code&gt;evaluateJs()&lt;/code&gt; (runs arbitrary async JS in the page context), &lt;code&gt;waitForSelector()&lt;/code&gt;, &lt;code&gt;getPageContent()&lt;/code&gt; (text/HTML/outerHTML with truncation safety), and &lt;code&gt;getConsoleLogs()&lt;/code&gt; (buffered, capped at 500 entries).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI entrypoint&lt;/strong&gt; — &lt;code&gt;main()&lt;/code&gt; only runs when the file is executed directly (not &lt;code&gt;require()&lt;/code&gt;&amp;#39;d), accepts &lt;code&gt;--url&lt;/code&gt; and &lt;code&gt;--port&lt;/code&gt; flags, and runs a quick inspection of a hardcoded component (&lt;code&gt;.WhoSentYouWrapper&lt;/code&gt;) as a smoke test.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;extractUrlFromText()&lt;/code&gt;&lt;/strong&gt; — a helper exported alongside the class for parsing localhost URLs out of natural-language strings, suggesting this is meant to be called from an MCP server that receives user text prompts.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;What worked for us&lt;/h4&gt;
&lt;p&gt;The Playwright visual regression tests and the DOM Inspector MCP worked best together. Our visual testing workflow started with Playwright tests for each template to identify visual regressions. The results would be passed to another agent and fixes made. We followed it with the DOM Inspector MCP to fine tune elements that weren&amp;#39;t fixed after the Playwright test fixes. We found the DOM Inspector to be more accurate with smaller, element-based inspections. Even then it wasn&amp;#39;t 100%, but it did save us time on fixing tedious styling issues.&lt;/p&gt;
&lt;h3&gt;Updating content (also with bots)&lt;/h3&gt;
&lt;p&gt;Since updating content hurts in the CMS, we wanted to make it easy to update content without needing deep technical knowledge of Frontmatter or code in general, so we made some Claude skills for it.&lt;/p&gt;
&lt;h4&gt;Skills for the command line&lt;/h4&gt;
&lt;p&gt;For non-developers, understanding git operations (or even being in the terminal) can be intimidating. But, we saw the value of using a PR-based workflow for quality and consistency. So, we made some utility skills to help with this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/new-branch&lt;/code&gt; — this would pull down the main branch on origin to prevent any avoidable merge conflicts, add a prefix to the branch name to know it came from a skill, and avoid any cruft from past branch checkouts from being included in the new PR.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/deploy-local-preview&lt;/code&gt; — since starting up a local dev server to preview your work takes a few lines in the terminal, we created a skill that does this for users. The skill will navigate to the site selected, spin up a dev server, and deploy a local preview.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Skills to update content&lt;/h4&gt;
&lt;p&gt;For each of our page types, we built skills that will create a Frontmatter file, ask the user for each field, upload images (with image size checks), call &lt;code&gt;/deploy-local-preview&lt;/code&gt; to check the work, and use the Github CLI to create a pull request. This provides guardrails to make sure all the required information is given, reduces navigating a cumbersome CMS UI, prevents massive image files from being used (we added a polite reminder to compress to under 250kb and won&amp;#39;t accept the larger file) and keeps page updates strictly to focus on content, not code.&lt;/p&gt;
&lt;h4&gt;Things to consider&lt;/h4&gt;
&lt;p&gt;Since we&amp;#39;ve built out skills with AI to update the content, there are a few things to keep in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compute is expensive.&lt;/strong&gt; You don&amp;#39;t need Opus to deploy a local preview when Haiku will do the job. We set default models on certain skills to make sure the model is right for the task. We also set the default model in the repo to Opus 4.6 to save on usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use skills to catch large image files and other common things that will slow down performance.&lt;/strong&gt; We added filesize limits to our page skills to prevent massive images from getting uploaded to our codebase and slowing down the build and site performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect sensitive parts of the site with Hooks.&lt;/strong&gt; Since the models have access to the whole codebase and there are sensitive items you don&amp;#39;t want changed, don&amp;#39;t allow AI to change them. For example, we protected our Content Security Policy with a &lt;code&gt;PreToolUse&lt;/code&gt; hook that prevents any changes to our CSP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fixing the &amp;quot;rate limit problem&amp;quot;&lt;/h2&gt;
&lt;p&gt;While we were under the hood, we tackled another recurring nightmare: API rate limits for our forms.&lt;/p&gt;
&lt;p&gt;Our forms relied on fetching fields from our marketing automation system during the build. If we hit a rate limit, the build broke. To fix this, we built a service using Vercel Blob. We now fetch and store form fields in a fast, reliable blob store at the start of the build.&lt;/p&gt;
&lt;p&gt;This reduced our marketing automation system API calls to nearly zero during the critical build phase, removing yet another point of failure.&lt;/p&gt;
&lt;h2&gt;The results: reliability as a feature&lt;/h2&gt;
&lt;p&gt;The shift from a heavy, API-dependent CMS to a lean, file-based Astro site has been a game-changer for our productivity.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Metric&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Before (Gatsby + CMS)&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;After (Astro + Claude)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Average Build Time&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;14 Minutes&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&amp;lt; 4 Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Web Vitals Score&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;89&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;97&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Broken Staging Builds&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Frequent (API/Plugin issues)&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;95% Reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Content Schema Limits&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Restricted by Plan&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Vibe Check&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Frustrating&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;High-Five Worthy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;By moving our content into the codebase and using Claude to bridge the gap for non-technical users, we didn&amp;#39;t just speed up our site — we made our entire deployment pipeline more resilient.&lt;/p&gt;
&lt;p&gt;Because at the end of the day, the best way to fix a broken build is to remove the things that break it in the first place.&lt;/p&gt;
</content:encoded></item><item><title>You don’t need to pick one: how Sentry and OpenTelemetry work together</title><link>https://blog.sentry.io/sentry-opentelemetry-work-together/</link><guid isPermaLink="true">https://blog.sentry.io/sentry-opentelemetry-work-together/</guid><description>Use Sentry on the frontend, keep OpenTelemetry on the backend, and choose direct OTLP or Collector forwarding for OTLP events.</description><pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You already instrumented the backend with OpenTelemetry. Your services emit spans. Your teams know the OTel APIs. Maybe you already run a Collector. So when you start evaluating Sentry, the obvious question is:&lt;/p&gt;
&lt;p&gt;Do you need to replace your OpenTelemetry setup with the Sentry SDK?&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;The practical answer is usually: keep OpenTelemetry where it already works, add the Sentry SDK where it gives you more application context, and send OpenTelemetry Protocol (OTLP) events to Sentry. For a web app, that often means using the Sentry SDK on the frontend for browser tracing, errors, &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;logs&lt;/a&gt;, &lt;a href=&quot;https://sentry.io/product/session-replay/&quot;&gt;Session Replay&lt;/a&gt;, and source maps, while keeping OpenTelemetry on the backend for existing service instrumentation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One scope note: OTLP can carry traces, logs, and metrics. At this moment, Sentry&amp;#39;s OTLP ingest supports logs and traces, not metrics. We&amp;#39;re considering adding support for them in the future.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The important part is separating two decisions that often get lumped together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How traces stay connected across frontend and backend.&lt;/li&gt;
&lt;li&gt;How backend OTLP events are exported to Sentry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you separate those, the architecture gets a lot easier to reason about.&lt;/p&gt;
&lt;h2&gt;Sentry vs OpenTelemetry is the wrong question&lt;/h2&gt;
&lt;p&gt;The first decision is trace linking. If a user clicks a button in your React app and that click triggers a backend request, the frontend and backend need to agree on the same &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;distributed trace&lt;/a&gt; context. In this example, the Sentry frontend SDK sends W3C &lt;code&gt;traceparent&lt;/code&gt; headers (configurable through the &lt;code&gt;propagateTraceparent&lt;/code&gt; option), and the OpenTelemetry backend continues the trace.&lt;/p&gt;
&lt;p&gt;That linking is handled by the frontend SDK configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;Sentry.init({
  integrations: [
    Sentry.browserTracingIntegration(),
  ],
  tracesSampleRate: 1.0,
  // ensure traceparent headers get sent
  propagateTraceparent: true,
  tracePropagationTargets: [
    &amp;#39;localhost&amp;#39;,
    &amp;#39;127.0.0.1&amp;#39;,
    /^http:\/\/localhost:8000\/api\//,
    /^http:\/\/127\.0\.0\.1:8000\/api\//,
    // your backend endpoint here
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second decision is export. After your backend creates telemetry, where do those OTLP events go?&lt;/p&gt;
&lt;p&gt;There are two common options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send OTLP events directly from the backend to Sentry&amp;#39;s OTLP endpoint.&lt;/li&gt;
&lt;li&gt;Send OTLP events to an OpenTelemetry Collector, then have the Collector forward them to Sentry&amp;#39;s OTLP endpoint.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That trace-continuation step is what lets a Sentry-instrumented browser action become the parent of backend OpenTelemetry work, regardless of which OTLP export option you choose.&lt;/p&gt;
&lt;p&gt;If you want the reference docs for these pieces, start with &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/sentry-with-otel/&quot;&gt;linking Sentry SDKs with OpenTelemetry SDKs&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/direct/traces/&quot;&gt;sending OpenTelemetry traces directly to Sentry&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/direct/logs/&quot;&gt;sending OpenTelemetry logs directly to Sentry&lt;/a&gt;, and &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/forwarding/&quot;&gt;forwarding OpenTelemetry data to Sentry&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Direct OTLP vs Collector forwarding&lt;/h2&gt;
&lt;p&gt;Direct OTLP and Collector forwarding both end at Sentry&amp;#39;s OTLP endpoint. The difference is whether your service talks to Sentry itself or talks to a Collector first.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Use it when&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Tradeoff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Direct OTLP to Sentry&lt;/td&gt;
&lt;td&gt;You have one backend service or project and want the smallest setup&lt;/td&gt;
&lt;td&gt;Fewer moving parts and a short path from service to Sentry&lt;/td&gt;
&lt;td&gt;Less central control over processing, sampling, and routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collector forwarding&lt;/td&gt;
&lt;td&gt;You have multiple services, already run a Collector, need processing, or want multi-vendor routing&lt;/td&gt;
&lt;td&gt;Centralized routing, batching, processing, sampling, and easier vendor evaluation&lt;/td&gt;
&lt;td&gt;Another component to deploy and operate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Direct OTLP is the simplest path for a single backend project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Collector forwarding is the better fit when your observability setup is already more than one app talking to one destination. It gives you one place to receive telemetry from many services, batch it, process it, and send it to one or more backends.&lt;/p&gt;
&lt;p&gt;That last part matters when you are evaluating Sentry. You can keep routing telemetry to an existing vendor while also forwarding a copy to Sentry, then compare the debugging experience without rewriting backend instrumentation.&lt;/p&gt;
&lt;p&gt;There is one important Sentry-specific detail for larger setups: a generic OTLP HTTP exporter points at one Sentry project endpoint with one project key. If you send every service through that one exporter, every service lands in the same Sentry project. For multi-project routing, use the &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/forwarding/pipelines/sentry-exporter/&quot;&gt;Sentry exporter&lt;/a&gt;. It can route OTLP events to projects based on a resource attribute like &lt;code&gt;service.name&lt;/code&gt;, and it can auto-create missing projects when configured with the right Sentry API permissions.&lt;/p&gt;
&lt;h2&gt;A demo architecture&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s check out a &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/tree/bf5e026c4923483e3d148d9fd30ead4d1540c774&quot;&gt;demo project&lt;/a&gt; that uses the Collector forwarding path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;React + @sentry/react
  -&amp;gt; fetch with traceparent
  -&amp;gt; FastAPI + OpenTelemetry
  -&amp;gt; OpenTelemetry Collector
  -&amp;gt; Sentry OTLP endpoint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend lives in &lt;code&gt;frontend/&lt;/code&gt;. It is a React + Vite app using &lt;code&gt;@sentry/react&lt;/code&gt;. The backend lives in &lt;code&gt;backend/&lt;/code&gt;. It is a FastAPI service using the OpenTelemetry SDK, FastAPI instrumentation, SQLAlchemy instrumentation, manual spans, and standard logging in checkout logic. The Collector lives in &lt;code&gt;collector/&lt;/code&gt; and forwards those OTLP events to Sentry.&lt;/p&gt;
&lt;p&gt;The point of the demo is not that every layer uses the same SDK. The point is that every layer participates in the same trace, with backend logs attached to that debugging context.&lt;/p&gt;
&lt;h3&gt;The frontend uses the Sentry SDK&lt;/h3&gt;
&lt;p&gt;The Sentry setup is in &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/frontend/src/instrument.ts&quot;&gt;&lt;code&gt;frontend/src/instrument.ts&lt;/code&gt;&lt;/a&gt;. It enables browser tracing, Session Replay, logs, and trace propagation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN || undefined,
  environment: import.meta.env.MODE,
  sendDefaultPii: true,
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration({
      maskAllText: false,
      blockAllMedia: false,
    }),
  ],
  enableLogs: true,
  tracesSampleRate: 1.0,
  propagateTraceparent: true,
  tracePropagationTargets: [
    &amp;#39;localhost&amp;#39;,
    &amp;#39;127.0.0.1&amp;#39;,
    /^http:\/\/localhost:8000\/api\//,
    /^http:\/\/127\.0\.0\.1:8000\/api\//,
  ],
  replaysSessionSampleRate: 1.0,
  replaysOnErrorSampleRate: 1.0,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend checkout flow is in &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/frontend/src/hooks/useCheckoutLab.ts#L98-L107&quot;&gt;&lt;code&gt;frontend/src/hooks/useCheckoutLab.ts&lt;/code&gt;&lt;/a&gt;. It creates Sentry spans for user-facing work, logs useful state changes, and captures unexpected errors:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;await Sentry.startSpan(
  {
    name: &amp;#39;Run checkout scenario&amp;#39;,
    op: &amp;#39;ui.checkout&amp;#39;,
    attributes: {
      scenario,
      itemCount,
      totalCents,
    },
  },
  async () =&amp;gt; {
    const order = await createCheckout(cart, scenario)
    setLastOrder(order)
  },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual request is a normal &lt;code&gt;fetch&lt;/code&gt; call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const response = await fetch(`${API_BASE_URL}/api/checkout`, {
  method: &amp;#39;POST&amp;#39;,
  headers: { &amp;#39;content-type&amp;#39;: &amp;#39;application/json&amp;#39; },
  body: JSON.stringify({ items, scenario }),
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no manual trace header code in that request. The Sentry browser tracing integration handles the propagation as long as the destination matches &lt;code&gt;tracePropagationTargets&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;The backend keeps OpenTelemetry&lt;/h3&gt;
&lt;p&gt;The backend does not install or initialize the Sentry SDK. Its OpenTelemetry setup is in &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/backend/app/core/observability.py&quot;&gt;&lt;code&gt;backend/app/core/observability.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It creates an OpenTelemetry &lt;code&gt;TracerProvider&lt;/code&gt;, attaches service resource attributes, exports spans over OTLP HTTP, and registers W3C trace-context propagation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;resource = Resource.create(
    {
        **parse_resource_attributes(settings.otel_resource_attributes),
        &amp;quot;service.name&amp;quot;: settings.otel_service_name,
        &amp;quot;deployment.environment&amp;quot;: settings.app_environment,
    }
)
provider = TracerProvider(resource=resource)

if settings.otel_exporter_otlp_traces_endpoint:
    exporter = OTLPSpanExporter(endpoint=settings.otel_exporter_otlp_traces_endpoint)
    provider.add_span_processor(BatchSpanProcessor(exporter))

trace.set_tracer_provider(provider)
propagate.set_global_textmap(
    CompositePropagator(
        [
            TraceContextTextMapPropagator(),
            W3CBaggagePropagator(),
        ]
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also configures OTLP log export. The backend creates a &lt;code&gt;LoggerProvider&lt;/code&gt;, attaches an &lt;code&gt;OTLPLogExporter&lt;/code&gt;, and adds an OpenTelemetry &lt;code&gt;LoggingHandler&lt;/code&gt; to the app logger:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;provider = LoggerProvider(resource=build_resource(settings))
exporter = OTLPLogExporter(endpoint=settings.otel_exporter_otlp_logs_endpoint)
provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
set_logger_provider(provider)

otel_handler = LoggingHandler(level=logging.INFO, logger_provider=provider)
app_logger = logging.getLogger(&amp;quot;checkout_trace_lab&amp;quot;)
app_logger.addHandler(otel_handler)
app_logger.setLevel(logging.INFO)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default backend endpoints are local:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://localhost:4318/v1/logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means the backend exports OTLP traces and logs to the Collector, not directly to Sentry.&lt;/p&gt;
&lt;p&gt;The FastAPI app also allows the browser trace headers through CORS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;allow_headers=[
    &amp;quot;content-type&amp;quot;,
    &amp;quot;sentry-trace&amp;quot;,
    &amp;quot;baggage&amp;quot;,
    &amp;quot;traceparent&amp;quot;,
    &amp;quot;tracestate&amp;quot;,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is easy to miss. If your browser is making cross-origin requests and CORS blocks the propagation headers, your frontend and backend traces can split apart even if both sides are instrumented correctly.&lt;/p&gt;
&lt;h3&gt;The checkout flow adds manual OTel spans and logs&lt;/h3&gt;
&lt;p&gt;The backend service code in &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/backend/app/services/checkout.py&quot;&gt;&lt;code&gt;backend/app/services/checkout.py&lt;/code&gt;&lt;/a&gt; models a checkout workflow. It creates manual OpenTelemetry spans for business operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def validate_cart(engine: Engine, payload: CheckoutRequest) -&amp;gt; dict[str, ProductRow]:
    with tracer.start_as_current_span(&amp;quot;checkout.validate_cart&amp;quot;) as span:
        requested_ids = [item.product_id for item in payload.items]
        span.set_attribute(&amp;quot;cart.item_count&amp;quot;, len(payload.items))
        ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The checkout path includes spans for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;checkout.validate_cart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout.reserve_inventory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout.calculate_tax&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout.payment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout.write_order&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout.send_confirmation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also emits backend logs for the same business steps, such as &lt;code&gt;checkout.started&lt;/code&gt;, &lt;code&gt;checkout.cart_validated&lt;/code&gt;, &lt;code&gt;checkout.inventory_reserved&lt;/code&gt;, &lt;code&gt;checkout.payment_approved&lt;/code&gt;, &lt;code&gt;checkout.order_written&lt;/code&gt;, and &lt;code&gt;checkout.completed&lt;/code&gt;. Those logs go through Python&amp;#39;s standard &lt;code&gt;logging&lt;/code&gt; API, then the OpenTelemetry &lt;code&gt;LoggingHandler&lt;/code&gt; exports them through OTLP.&lt;/p&gt;
&lt;p&gt;This is the part OTel-first teams care about most. Those spans and logs stay in the OpenTelemetry pipeline. You do not need to rewrite them with &lt;code&gt;Sentry.startSpan()&lt;/code&gt; or Sentry logging APIs just to view the trace and related logs in Sentry.&lt;/p&gt;
&lt;h3&gt;The Collector forwards OTLP to Sentry&lt;/h3&gt;
&lt;p&gt;The Collector config is in &lt;a href=&quot;https://github.com/nikolovlazar/sentry-otel/blob/bf5e026c4923483e3d148d9fd30ead4d1540c774/collector/otel-collector.yaml&quot;&gt;&lt;code&gt;collector/otel-collector.yaml&lt;/code&gt;&lt;/a&gt;. It receives OTLP over gRPC and HTTP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It batches the data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;processors:
  batch:
    send_batch_size: 1024
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then it forwards to Sentry with the OTLP HTTP exporter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;exporters:
  debug:
    verbosity: basic
  otlphttp/sentry:
    endpoint: ${env:SENTRY_OTLP_ENDPOINT}
    headers:
      x-sentry-auth: &amp;quot;sentry sentry_key=${env:SENTRY_OTLP_PUBLIC_KEY}&amp;quot;
    compression: gzip
    encoding: proto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reminder: this demo uses generic &lt;code&gt;otlphttp&lt;/code&gt; for a single Sentry project. For multi-project routing or automatic project creation, swap it for the &lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/sentryexporter&quot;&gt;&lt;code&gt;sentry&lt;/code&gt; exporter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The configured pipelines send to both the debug exporter and Sentry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, otlphttp/sentry]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug, otlphttp/sentry]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the Collector forwarding pattern: the backend sends OTLP events to one local endpoint, and the Collector decides where that telemetry goes next.&lt;/p&gt;
&lt;h2&gt;What each layer is responsible for&lt;/h2&gt;
&lt;p&gt;The cleanest way to understand this architecture is by ownership.&lt;/p&gt;
&lt;p&gt;The frontend Sentry SDK owns browser-specific debugging context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Browser spans and frontend transactions&lt;/li&gt;
&lt;li&gt;Frontend errors and React error boundaries&lt;/li&gt;
&lt;li&gt;Session Replay&lt;/li&gt;
&lt;li&gt;Frontend logs&lt;/li&gt;
&lt;li&gt;Source maps through the Sentry Vite plugin&lt;/li&gt;
&lt;li&gt;Trace propagation to the backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The backend OpenTelemetry SDK owns backend instrumentation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FastAPI request spans&lt;/li&gt;
&lt;li&gt;SQLAlchemy spans&lt;/li&gt;
&lt;li&gt;HTTPX spans if the backend calls other services&lt;/li&gt;
&lt;li&gt;Manual checkout spans&lt;/li&gt;
&lt;li&gt;Backend logs exported with OpenTelemetry&lt;/li&gt;
&lt;li&gt;Resource attributes such as &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;deployment.environment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;OTLP export to the Collector&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Collector owns routing and processing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Receiving OTLP on &lt;code&gt;4317&lt;/code&gt; and &lt;code&gt;4318&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Batching telemetry&lt;/li&gt;
&lt;li&gt;Printing debug output locally&lt;/li&gt;
&lt;li&gt;Forwarding to Sentry&amp;#39;s OTLP endpoint&lt;/li&gt;
&lt;li&gt;Providing the place to add sampling, transforms, filters, or multi-vendor routing later&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is why Sentry and OpenTelemetry are not competing choices here. They are doing different jobs in the same observability pipeline.&lt;/p&gt;
&lt;h2&gt;What you can see in Sentry&lt;/h2&gt;
&lt;p&gt;In the demo, I ran the checkout scenarios from the same frontend session. In Sentry, they show up as one connected trace that starts in React and continues through the FastAPI backend. You can see the normal checkout, slow payment, inventory miss, payment declined, and backend crash work in the same distributed trace instead of jumping between separate tools.&lt;/p&gt;
&lt;p&gt;The backend logs are associated with that trace too. Logs like &lt;code&gt;checkout.started&lt;/code&gt;, &lt;code&gt;checkout.inventory_reserved&lt;/code&gt;, &lt;code&gt;checkout.payment_slow_path&lt;/code&gt;, and &lt;code&gt;checkout.completed&lt;/code&gt; come from Python&amp;#39;s standard logging API, get exported through OTLP, and land in Sentry attached to the same debugging context as the spans.&lt;/p&gt;
&lt;p&gt;That is the useful part of the setup: the frontend SDK gives you browser context, the backend keeps its OpenTelemetry spans and logs, and Sentry gives you one place to inspect the full request path.&lt;/p&gt;
&lt;h2&gt;A decision tree for your own app&lt;/h2&gt;
&lt;p&gt;Use direct OTLP to Sentry when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have one backend service or one project.&lt;/li&gt;
&lt;li&gt;You want the fewest moving parts.&lt;/li&gt;
&lt;li&gt;You do not need central processing or multi-destination routing yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use Collector forwarding when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You already run an OpenTelemetry Collector.&lt;/li&gt;
&lt;li&gt;You have multiple backend services.&lt;/li&gt;
&lt;li&gt;You need services to land in separate Sentry projects.&lt;/li&gt;
&lt;li&gt;You need sampling, filtering, transforms, or batching outside the app process.&lt;/li&gt;
&lt;li&gt;You want to send telemetry to Sentry and another vendor while evaluating Sentry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add the Sentry backend SDK later when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need error monitoring. OpenTelemetry does not capture and send errors to Sentry—only the Sentry SDK can capture backend exceptions and link them to the trace, so you can jump from an error to the full request path and inspect the related logs, spans, and context.&lt;/li&gt;
&lt;li&gt;You want profiling.&lt;/li&gt;
&lt;li&gt;You want &lt;a href=&quot;https://docs.sentry.io/product/metrics/&quot;&gt;Sentry&amp;#39;s Application Metrics&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;You want other Sentry features that are not represented by your current OpenTelemetry data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last step is optional, not a prerequisite. You can start with Sentry on the frontend and OpenTelemetry on the backend, then decide later whether adding the backend Sentry SDK is worth it for your services.&lt;/p&gt;
&lt;h2&gt;Start with the smallest change that preserves your trace&lt;/h2&gt;
&lt;p&gt;If your backend already uses OpenTelemetry, do not start by rewriting instrumentation. Start by deciding where OTLP events should go.&lt;/p&gt;
&lt;p&gt;For a single backend, direct OTLP to Sentry is usually enough.&lt;/p&gt;
&lt;p&gt;For multiple services, vendor evaluation, or anything that needs routing and processing, put a Collector in the middle.&lt;/p&gt;
&lt;p&gt;Then link your frontend Sentry SDK to your backend OTel SDK with W3C &lt;code&gt;traceparent&lt;/code&gt; propagation. That gives you the useful part first: one trace that starts where the user action starts and continues through the backend code you already instrumented.&lt;/p&gt;
&lt;p&gt;You do not need to pick Sentry or OpenTelemetry. Use both where they fit.&lt;/p&gt;
</content:encoded></item><item><title>Your agent can&apos;t fix what it can&apos;t see</title><link>https://blog.sentry.io/agents-need-production-context/</link><guid isPermaLink="true">https://blog.sentry.io/agents-need-production-context/</guid><description>Agents can&apos;t fix bugs they can&apos;t see. Learn how Sentry MCP and CLI give coding agents the production context to diagnose and fix issues automatically.</description><pubDate>Tue, 26 May 2026 16:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Agents are getting better and better at fixing bugs. They&amp;#39;re even getting better at testing their work, thanks to headless browsers, sandboxes, simulators, etc.&lt;/p&gt;
&lt;p&gt;But what about the bugs that only show up once you bring in different browsers, languages, extensions, internet speeds, and all the other variables that get mixed in the second you ship to prod? Or all the bugs that only show up when you account for… well, humans being humans and doing weird stuff you didn&amp;#39;t expect them to do?&lt;/p&gt;
&lt;p&gt;The bottleneck for self-healing software isn&amp;#39;t agent intelligence. It&amp;#39;s that agents have no idea what actually broke. They&amp;#39;re debugging from source code alone, which is roughly as effective as diagnosing a server outage by skimming the README. What they&amp;#39;re missing is production context: the stack trace, the request payload, the environment, the breadcrumbs leading up to the failure.&lt;/p&gt;
&lt;p&gt;Your agents need someone/something telling them what&amp;#39;s breaking in the wild &lt;em&gt;and&lt;/em&gt; giving them the context they need to understand why.&lt;/p&gt;
&lt;p&gt;We built &lt;a href=&quot;https://mcp.sentry.dev/&quot;&gt;Sentry MCP&lt;/a&gt; and the &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt; to make that context available to both humans, and increasingly as important, their agents. You can wire up a system today where a Sentry alert triggers an agent, the agent investigates the issue using the same evidence you would, and a draft PR with a fix lands in your repo before you open a browser.&lt;/p&gt;
&lt;h2&gt;Why draft PRs, not auto-merge&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s be honest about what&amp;#39;s realistic. A system that detects, fixes, tests, deploys, and monitors its own patches without human involvement is not something you should build today. That&amp;#39;s how you get a very exciting incident review.&lt;/p&gt;
&lt;p&gt;The useful version is more modest: a production error fires, an agent investigates it with real Sentry context, writes a small fix with a regression test, and opens a draft PR. A human is very much in the loop.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s not fully autonomous, but it&amp;#39;s not trivial either. Most bugs sit in a queue, triaged, prioritized, assigned, waiting, and often lose out to new features. Seer diagnoses the root cause in under two minutes. A complete Autofix run, from root cause analysis to an opened PR, takes about six minutes.&lt;/p&gt;
&lt;p&gt;An agent that opens a reviewable, mergeable fix six minutes after the error fires is a meaningful change to your mean time to resolution, even if a human still clicks merge.&lt;/p&gt;
&lt;h2&gt;Two ways to give your agent production context&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Sentry MCP&lt;/strong&gt; is the right choice for agents that support the Model Context Protocol (Claude Code, Cursor, Codex, Windsurf, VS Code with Copilot). Your agent connects to the hosted server, authenticates via OAuth, and gets structured access to issues, events, traces, and Seer analysis. No local install required.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# One-liner for any MCP-compatible client
npx add-mcp https://mcp.sentry.dev/mcp

# Or for Claude Code specifically
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your client doesn&amp;#39;t support the one-liner, add the config manually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;sentry&amp;quot;: {
      &amp;quot;url&amp;quot;: &amp;quot;https://mcp.sentry.dev/mcp&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Sentry CLI&lt;/strong&gt; is the right choice for scripted workflows, CI pipelines, or any automation where you need structured output you can pipe to &lt;code&gt;jq&lt;/code&gt; or feed into another process.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl https://cli.sentry.dev/install -fsS | bash
sentry auth login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sentry issue list

Issues in acme/checkout:
╭──────────────┬──────────────────────────────────────────────────────┬──────┬─────┬────────┬───────┬──────────────╮
│ SHORT ID     │ ISSUE                                                │ SEEN │ AGE │ EVENTS │ USERS │ TRIAGE       │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ CHECKOUT-P1  │ TimeoutError: Payment charge exceeded 30s            │   3h │  3h │  1.8k  │   340 │ High  86%    │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ CHECKOUT-N7  │ TypeError: Cannot read property &amp;#39;total&amp;#39;              │   1d │  5d │    215 │    82 │ High  71%    │
├──────────────┼──────────────────────────────────────────────────────┼──────┼─────┼────────┼───────┼──────────────┤
│ API-34       │ RateLimitError: Too many requests to /v1/charges     │   3d │ 21d │     67 │    24 │ Med   42%    │
╰──────────────┴──────────────────────────────────────────────────────┴──────┴─────┴────────┴───────┴──────────────╯
Tip: Use &amp;#39;sentry issue view &amp;#39; to view details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CHECKOUT-P1&lt;/code&gt; is at the top, a timeout in the checkout service with 1.8k events and an 86% fixability score. Drill in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sentry issue view CHECKOUT-P1

CHECKOUT-P1: TimeoutError: Payment charge exceeded 30s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
╭────────────┬─────────────────────────────────────────────╮
│ Status     │ ● Unresolved (Ongoing)                      │
│ Fixability │ High (86%)                                  │
│ Level      │ error                                       │
│ Platform   │ node                                        │
│ Project    │ checkout-service                            │
│ Events     │ 1832                                        │
│ Users      │ 340                                         │
│ First seen │ 3 hours ago                                 │
│ Last seen  │ 12 minutes ago                              │
│ Culprit    │ chargeCustomer (src/payment.ts)             │
│ Link       │ https://acme.sentry.io/issues/CHECKOUT-P1/  │
╰────────────┴─────────────────────────────────────────────╯

Tip: Use &amp;#39;sentry issue explain CHECKOUT-P1&amp;#39; for AI root cause analysis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks like a straightforward timeout. An agent with just this would add retry logic or bump the timeout. But run &lt;code&gt;sentry issue explain&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ sentry issue explain CHECKOUT-P1

ℹ Starting root cause analysis, it can take several minutes...

Root Cause Analysis Complete
━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Cause #0: The checkout service&amp;#39;s /charge endpoint times out
waiting for the payment service, which blocks on an inventory
availability check. The inventory service&amp;#39;s check_stock query
regressed from ~200ms to ~28s after migration
0047_drop_unused_indexes removed the compound index on
(product_id, warehouse_id).

Repository: acme/inventory-service
Affected: src/queries/check_stock.ts:18
First seen: release-3.1.0 (deployed 3h ago)

Reproduction steps:
1. User submits checkout → POST /charge
2. Payment service calls inventory.check_stock(items)
3. check_stock runs full table scan (missing index) → 28s
4. Payment call exceeds 30s timeout → TimeoutError bubbles up to checkout

To create a plan, run: sentry issue plan CHECKOUT-P1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The root cause isn&amp;#39;t in the checkout service at all. It&amp;#39;s a dropped database index in the inventory service, two hops away in the trace. No amount of retry logic in &lt;code&gt;payment.ts&lt;/code&gt; fixes that.&lt;/p&gt;
&lt;h2&gt;From alert to draft PR&lt;/h2&gt;
&lt;p&gt;When a Sentry alert fires on a new or regressed issue, a webhook triggers a worker that checks out your repo and runs a coding agent with a prompt grounded in the specific issue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A production error was captured by Sentry. The issue ID is CHECKOUT-P1.

Use Sentry MCP to retrieve the full issue details: stack trace,
breadcrumbs, tags, release, environment, distributed traces,
suspect commits, and Seer analysis.

Based on the evidence:

1. Identify the root cause. Follow traces across services.
2. Make the smallest safe fix in the right repository.
3. Add or update a regression test that covers this failure.
4. Run the test suite.
5. Open a draft PR with the Sentry issue link, root-cause
   summary, files changed, and test results.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent pulls the issue via MCP. The distributed trace shows the checkout call chaining through the payment service into an inventory check that&amp;#39;s taking 28 seconds. Metrics confirm the inventory service&amp;#39;s p99 spiked from 200ms to 28s three hours ago. Suspect commits point at a migration in &lt;code&gt;acme/inventory-service&lt;/code&gt; that dropped a compound index. Session replay shows users rage-clicking &amp;quot;Pay&amp;quot; while nothing happens, generating duplicate charge attempts.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sentry issue plan CHECKOUT-P1&lt;/code&gt; lays out the fix: restore the compound index on &lt;code&gt;(product_id, warehouse_id)&lt;/code&gt;. A draft PR lands in &lt;code&gt;acme/inventory-service&lt;/code&gt; with the migration, a root-cause summary linking back to the Sentry trace, and a regression test.&lt;/p&gt;
&lt;h2&gt;Try it with Cursor Automations&lt;/h2&gt;
&lt;p&gt;We publish a &lt;a href=&quot;https://sentry.io/cookbook/regressed-issue-to-pr-cursor/&quot;&gt;cookbook recipe&lt;/a&gt; for this exact workflow using Cursor&amp;#39;s Automations feature. It walks through connecting your repo to Sentry, adding the MCP server to an automation, and configuring a webhook alert to trigger on regressed issues.&lt;/p&gt;
&lt;p&gt;Because Sentry knows the release history and suspect commits, the agent doesn&amp;#39;t search the entire repo for the problem. It starts where the evidence points. For regressed issues specifically, it can identify which commit reintroduced the bug, read the original fix, and understand what went wrong the second time around.&lt;/p&gt;
&lt;h2&gt;What&amp;#39;s next&lt;/h2&gt;
&lt;p&gt;The more telemetry your app sends to Sentry (traces, metrics, logs, session replays), the harder the bugs an agent can tackle. Today it&amp;#39;s dropped indexes across service boundaries. Six months ago it was null checks. The merge rate on Autofix PRs has climbed from 41% to 46% in that time, and the diagnosis complexity is growing with it.&lt;/p&gt;
&lt;p&gt;There are real limits. Bugs that need product judgment, issues in code the agent can&amp;#39;t reach, and problems where there isn&amp;#39;t enough telemetry to connect the dots: those still need you. But the surface area of what agents can fix is expanding every month.&lt;/p&gt;
&lt;p&gt;Connect &lt;a href=&quot;https://mcp.sentry.dev/&quot;&gt;Sentry MCP&lt;/a&gt; to your editor or install the &lt;a href=&quot;https://cli.sentry.dev&quot;&gt;CLI&lt;/a&gt;. Hook up your repos for code mappings and tracing. Run &lt;code&gt;sentry issue explain&lt;/code&gt; on something that&amp;#39;s been sitting in your backlog and see what it finds.&lt;/p&gt;
&lt;p&gt;Check out the &lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/seer/autofix/&quot;&gt;Seer Autofix docs&lt;/a&gt; for more on coding agent handoff to Claude Code and Cursor.&lt;/p&gt;
</content:encoded></item><item><title>The product analytics you already have</title><link>https://blog.sentry.io/product-analytics-you-already-have/</link><guid isPermaLink="true">https://blog.sentry.io/product-analytics-you-already-have/</guid><description>Your Sentry traces, logs, and metrics already answer most product analytics questions. Learn how to query existing telemetry for product insights.</description><pubDate>Thu, 21 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You already have everything you need.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re using Sentry, you have &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;traces&lt;/a&gt;, &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;structured logs&lt;/a&gt;, and now &lt;a href=&quot;https://sentry.io/product/metrics/&quot;&gt;application metrics&lt;/a&gt;. Most teams use that stuff for debugging and stop there. But get this: that same data can answer most of the product questions you&amp;#39;ve been sending to a separate analytics tool, maintained by a separate team, with a separate data model and a separate bill. (Not all of them. We&amp;#39;ll get honest about the gaps later.)&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t a post about whether product analytics tools should exist. It&amp;#39;s about the fact that developers have been sitting on top of a goldmine of product insight and outsourcing the questions to someone else. You don&amp;#39;t have to.&lt;/p&gt;
&lt;p&gt;There&amp;#39;s a reason this matters more now than it did five years ago. The line between &amp;quot;product manager&amp;quot; and &amp;quot;software engineer&amp;quot; is blurring. Engineers are increasingly expected to think about adoption, retention, and user behavior, not just uptime and latency. If you&amp;#39;re a product engineer (and increasingly, that&amp;#39;s just &amp;quot;engineer&amp;quot;), the tools you already use for debugging are the same tools you should be using to answer product questions. You just haven&amp;#39;t been querying them that way.&lt;/p&gt;
&lt;h2&gt;Every product question maps to telemetry you already have&lt;/h2&gt;
&lt;p&gt;&amp;quot;How many users completed onboarding this week?&amp;quot; That&amp;#39;s a counter metric: &lt;code&gt;metrics.count(&amp;quot;onboarding.completed&amp;quot;, 1)&lt;/code&gt;. You can slice it by plan tier, country, or referral source with attributes you&amp;#39;re already setting.&lt;/p&gt;
&lt;p&gt;&amp;quot;What&amp;#39;s our p95 checkout latency by region?&amp;quot; That&amp;#39;s a distribution on a span. The trace already has the timing. You just need to query it.&lt;/p&gt;
&lt;p&gt;&amp;quot;Why did signups drop on Tuesday?&amp;quot; That&amp;#39;s a structured log. The signup service logged &lt;code&gt;signup.failed&lt;/code&gt; with &lt;code&gt;reason: email_validation_error&lt;/code&gt; and &lt;code&gt;deploy_sha: a3f9b2c&lt;/code&gt;. The log is already correlated to the trace that shows the full request lifecycle, the span that errored, and the release that introduced it. From the trace, you&amp;#39;re one click away from the issue, which links you to the commit and the line of code that caused it.&lt;/p&gt;
&lt;p&gt;These are the same questions your PM asks their analytics tool. The difference is that when &lt;em&gt;you&lt;/em&gt; answer them from your telemetry, the answer comes with context. The analytics tool gives you the &amp;quot;what.&amp;quot; Your telemetry gives you the &amp;quot;what,&amp;quot; the &amp;quot;why,&amp;quot; and a direct path to the code that&amp;#39;s responsible.&lt;/p&gt;
&lt;p&gt;A customer told us recently that they were tired of analytics tools giving them a heads-up &lt;em&gt;after&lt;/em&gt; the damage was done. They wanted to be proactive. The way they got there was setting up alerts and monitors on the telemetry they were already collecting. When a business-critical metric starts trending down, the alert fires before the weekly dashboard review catches it. And because the alert is on a span or a metric that&amp;#39;s connected to the full trace, the investigation starts with context, not a context-switch to a different tool. They didn&amp;#39;t change the data they were collecting. They changed how they were watching it.&lt;/p&gt;
&lt;h2&gt;What it looks like in practice&lt;/h2&gt;
&lt;p&gt;Say you want to know whether users are adopting your new export feature, whether it&amp;#39;s performant, and whether paid users behave differently than free users.&lt;/p&gt;
&lt;p&gt;You don&amp;#39;t need to file a ticket asking someone to instrument this in an analytics tool. You already have the three primitives: spans, logs, and application metrics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spans for request-level context.&lt;/strong&gt; Spans tell you how requests move through your system and how long each operation takes. The export API handler already has a span. Add business-level attributes (like &lt;code&gt;export.user_tier&lt;/code&gt;, which lets you slice by plan):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;with sentry_sdk.start_span(op=&amp;quot;export.generate&amp;quot;, name=&amp;quot;Generate export file&amp;quot;) as span:
    span.set_attribute(&amp;quot;export.file_size_mb&amp;quot;, file_size_mb)
    span.set_attribute(&amp;quot;export.format&amp;quot;, export_format)
    span.set_attribute(&amp;quot;export.user_tier&amp;quot;, user.plan)
    span.set_attribute(&amp;quot;export.row_count&amp;quot;, row_count)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Query adoption, performance, and errors in one place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;span.op:export.generate | count(), avg(span.duration), count_if(span.status:internal_error)
  group by export.user_tier, export.format
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;export.user_tier  | export.format | count() | avg(span.duration) | error_count
──────────────────|───────────────|─────────|────────────────────|────────────
pro               | xlsx          | 1,204   | 3.8s               | 89
pro               | csv           | 3,847   | 1.2s               | 24
free              | csv           | 12,493  | 0.9s               | 3
free              | xlsx          | 891     | 4.1s               | 142
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can already see the story: xlsx exports are 3-4x slower than csv, and free-tier xlsx is erroring on 16% of requests. You didn&amp;#39;t need an analytics tool to surface that. You needed to query the spans you already had.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Structured logs for discrete business events.&lt;/strong&gt; Logs capture discrete events with enough detail to help you debug what happened and why. When a user upgrades their plan, you want to record the events that took place before and after the plan change, where it was triggered, and any revenue impact. That&amp;#39;s a business event worth recording, but it&amp;#39;s not a performance-sensitive operation you need to trace end-to-end. Log it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;

sentry_sdk.logger.info(
    &amp;quot;plan.upgraded&amp;quot;,
    previous_plan=previous_plan,
    new_plan=new_plan,
    user_id=user.id,
    upgrade_source=source,  # &amp;quot;paywall&amp;quot;, &amp;quot;settings&amp;quot;, &amp;quot;checkout&amp;quot;
    mrr_delta_usd=mrr_change,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can query plan upgrades by source, see which upgrade path drives the most revenue, and if something breaks in the upgrade flow, the log entry is already linked to the trace context that shows you where it failed.&lt;/p&gt;
&lt;p&gt;Query your logs for upgrade events from the past 30 days:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plan.upgraded | count(), sum(mrr_delta_usd)
  group by upgrade_source
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;upgrade_source | count() | sum(mrr_delta_usd)
───────────────|─────────|───────────────────
paywall        | 842     | $27,340
settings       | 214     | $8,120
checkout       | 1,307   | $51,890
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checkout drives the most upgrades, but it also converts at a higher average MRR per upgrade ($39.69 vs. $32.47 for paywall). That&amp;#39;s the kind of insight your PM is running a separate tool to get, and it&amp;#39;s sitting in your logs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Application Metrics for KPIs and health signals.&lt;/strong&gt; Metrics are how you track the measures that tell you whether things are healthy – both in your service and your business. Things like checkout conversion rate, signup and traffic over time, and error budget burn. These are the signals you track continuously, get alerted on, and study trends:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;from sentry_sdk import metrics

metrics.count(&amp;quot;export.completed&amp;quot;, 1, attributes={&amp;quot;tier&amp;quot;: user.plan, &amp;quot;format&amp;quot;: export_format})
metrics.distribution(&amp;quot;export.file_size&amp;quot;, file_size_mb, attributes={&amp;quot;tier&amp;quot;: user.plan})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives you a counter and distribution you can alert on, visualize, and use to observe trends over time without building or maintaining an analytics pipeline.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s three primitives in one SDK covering every angle you need: request-level detail from spans, discrete business events from logs, and aggregated trends from metrics, all queryable in one tool and all connected to each other.&lt;/p&gt;
&lt;p&gt;Compare this to the usual workflow: someone instruments a &lt;code&gt;feature_export_used&lt;/code&gt; event in an analytics tool, builds a dashboard, checks it weekly. Three weeks later they notice adoption is flat. They ask engineering if there are issues. Engineering checks Sentry, finds the export is timing out for files over 50MB, which covers most real-world usage. Three weeks lost because the analytics tool could see the symptom but not the cause.&lt;/p&gt;
&lt;p&gt;With your telemetry, you can set up a monitor on that span&amp;#39;s error rate and duration. When the timeout starts happening, the alert fires on the span itself, not on a downstream analytics metric that takes weeks to reflect the problem. The metric shows the count dropping. The span shows the duration spiking. The log shows the error. And all three point back to the same trace, the same release, the same line of code.&lt;/p&gt;
&lt;p&gt;To go one step further, you can ask &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;Seer&lt;/a&gt; if anything is broken that may be causing adoption to stay flat.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/posts/product-analytics-you-already-have/ask-seer-analytics.webp&quot; alt=&quot;Asking Seer to analyze product analytics data&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The skills already transfer&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re already using Sentry for debugging, the only mental shift is realizing that &amp;quot;business telemetry&amp;quot; and &amp;quot;system telemetry&amp;quot; aren&amp;#39;t different categories.&lt;/p&gt;
&lt;p&gt;The business question (&amp;quot;did the user convert?&amp;quot;) and the engineering question (&amp;quot;did the request succeed?&amp;quot;) are the same question asked at different altitudes. You don&amp;#39;t need a separate tool to ask the business question. You need to add &lt;code&gt;purchase.value_usd&lt;/code&gt; to the span you already have, log the &lt;code&gt;purchase.completed&lt;/code&gt; event with the attributes that matter, and increment the counter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-py&quot;&gt;# On the span
span.set_data(&amp;quot;purchase.value_usd&amp;quot;, order.total)
span.set_data(&amp;quot;purchase.item_count&amp;quot;, len(order.items))

# As a structured log
sentry_sdk.logger.info(
    &amp;quot;purchase.completed&amp;quot;,
    value_usd=order.total,
    item_count=len(order.items),
    coupon_applied=bool(order.coupon),
    user_id=user.id,
)

# As a metric
metrics.count(&amp;quot;purchase.completed&amp;quot;, 1, attributes={&amp;quot;coupon&amp;quot;: str(bool(order.coupon))})
metrics.distribution(&amp;quot;purchase.value&amp;quot;, order.total, attributes={&amp;quot;plan&amp;quot;: user.plan})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One customer framed it well: when there&amp;#39;s a discrepancy in a business metric, it shows up in other parts of the system too. That&amp;#39;s what &lt;a href=&quot;https://sentry.io/solutions/application-observability/&quot;&gt;observability&lt;/a&gt; means. Everything is connected. The moment you start treating your telemetry as the source of truth for product questions, you stop needing a second system to answer them.&lt;/p&gt;
&lt;h2&gt;Where this gets hard (and why it&amp;#39;s getting easier)&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s be honest about the gaps.&lt;/p&gt;
&lt;p&gt;Multi-session retention analysis, behavioral cohorts (&amp;quot;users who did X but not Y within 14 days&amp;quot;), and cross-session funnel conversion are genuinely difficult to reconstruct from raw telemetry. Spans are request-scoped. Logs are event-scoped. Metrics are time-series. Stitching together a user&amp;#39;s journey across sessions and days requires aggregation infrastructure that observability tools haven&amp;#39;t traditionally built.&lt;/p&gt;
&lt;p&gt;If your PM needs a 30-day retention curve segmented by acquisition channel, you can&amp;#39;t just &lt;code&gt;GROUP BY&lt;/code&gt; your way there today.&lt;/p&gt;
&lt;p&gt;But most of the data is already there. Your spans, logs, and metrics all carry user IDs, timestamps, and business attributes. The missing piece is the aggregation and visualization layer. That&amp;#39;s a query engine problem, not a data model problem. OpenTelemetry is making this easier to solve every quarter, because once instrumentation is standardized, the aggregation layer becomes commoditized. The gap is real, and it&amp;#39;s shrinking.&lt;/p&gt;
&lt;p&gt;We think there&amp;#39;s interesting work to be done here, and we plan to dig into some of these harder use cases in future posts, showing how far you can get with Sentry&amp;#39;s existing query tools even for cross-session analysis.&lt;/p&gt;
&lt;p&gt;For the questions that matter day-to-day as a developer building and shipping features, the gap doesn&amp;#39;t exist. Is my feature being adopted? Is it performant? Is it erroring? Are paid users behaving differently than free users? Which upgrade path drives the most revenue? You can answer all of these right now, from the telemetry you already have, without waiting for anyone.&lt;/p&gt;
&lt;h2&gt;Try it&lt;/h2&gt;
&lt;p&gt;Pick one feature you shipped recently that you&amp;#39;re curious about. There most certainly is an opportunity to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add business-level attributes to the spans on the critical path of that feature&amp;#39;s functionality&lt;/li&gt;
&lt;li&gt;Add Sentry Logs that are high cardinality wide events including details you would want to query this data by (user plan, surface, activity data)&lt;/li&gt;
&lt;li&gt;Sprinkle application metrics across with attributes that will be useful in creating dashboards.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have found that agents are quite good at following these types of requests, and we have some skills to deploy in your local IDE and once you&amp;#39;re done, &lt;a href=&quot;https://sentry.io/product/seer/agent/&quot;&gt;ask Seer Agent&lt;/a&gt; about the best ways to create dashboards, monitors and alerts for them.&lt;/p&gt;
&lt;p&gt;See how long it takes before the analytics dashboard for that feature stops being the thing anyone opens first.&lt;/p&gt;
&lt;p&gt;You already have the data, you already have the tools, and you&amp;#39;ve just been letting someone else ask your questions for you.&lt;/p&gt;
</content:encoded></item><item><title>New ways to agentically build and edit dashboards</title><link>https://blog.sentry.io/dashboard-updates/</link><guid isPermaLink="true">https://blog.sentry.io/dashboard-updates/</guid><description>Create and edit Sentry dashboards with AI agents, the Sentry CLI, or pre-built templates you can clone and customize for your monitoring needs.</description><pubDate>Thu, 14 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The traditional dashboard workflow, teams slowly handcrafting visualizations to track critical KPIs, is dying in a world of AI agents.&lt;/p&gt;
&lt;p&gt;A few years ago, in a pre-agentic-everything world, we tried to make it easier for developers to monitor critical experiences. We introduced Insights pages, which were pre-configured dashboards any Sentry user could adopt instantly that surfaced common health signals, like Web and Mobile Vitals.&lt;/p&gt;
&lt;p&gt;The idea was right, but there was a problem: while many companies share common signals, every organization is unique. Without meaningful customization, most teams still ended up having to slog through manually building dashboards themselves. So we kept iterating.&lt;/p&gt;
&lt;p&gt;Large language models are what finally made a reality of on-demand, customizable dashboards possible. Visualizations remain one of the most information-dense ways for humans (and agents) to communicate. What changed is the cost of creating those views.&lt;/p&gt;
&lt;p&gt;Instead of assembling dashboards widget by widget, you can now prompt an agent to create or edit dashboards directly in Sentry, or use the &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt; to connect Sentry to other models, and generate a dashboard tailored to the task at hand.&lt;/p&gt;
&lt;p&gt;Insights pages are now &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/&quot;&gt;Sentry-built Dashboards&lt;/a&gt;. Clone them, then ask an agent to customize them for your project. Dashboards can now be created in seconds, used for the lifetime of a project or investigation, and discarded once they stop providing value.&lt;/p&gt;
&lt;h2&gt;What&amp;#39;s new&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agentic dashboard creation &amp;amp; editing (beta):&lt;/strong&gt; All organizations with &lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/&quot;&gt;AI-powered features enabled&lt;/a&gt; can now create and edit dashboards in Sentry using an agent-powered chat experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insights are now Sentry dashboards:&lt;/strong&gt; We replaced Insights pages with clonable, editable Sentry dashboards that you can customize to fit your specific use case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dashboard creation &amp;amp; editing via the Sentry CLI:&lt;/strong&gt; You can create and manage Sentry dashboards from your terminal via the all-new Sentry CLI.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Sentry dashboards use issue, &lt;a href=&quot;https://docs.sentry.io/concepts/key-terms/tracing/&quot;&gt;tracing&lt;/a&gt; and &lt;a href=&quot;https://docs.sentry.io/product/explore/metrics/getting-started/&quot;&gt;application metrics&lt;/a&gt; data. You can also &lt;a href=&quot;https://docs.sentry.io/product/explore/&quot;&gt;query against multiple event types&lt;/a&gt; in Sentry and save queries to dashboards.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Agentic dashboard generation and editing&lt;/h2&gt;
&lt;p&gt;You can now create and edit dashboards in Sentry agentically via the same capabilities that power &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;Seer, Sentry&amp;#39;s AI debugger&lt;/a&gt; (&lt;strong&gt;Note&lt;/strong&gt;: while Seer is an add-on option, agentic dashboard creation is a separate feature that is available for free). When you create or edit a dashboard, you still have the option to add or edit manually, but you can now just tell Sentry what you want and we&amp;#39;ll put it together for you automatically. Creating and editing dashboards with AI makes the experience of going from concept to final dashboard much faster than manual creation:&lt;/p&gt;
&lt;p&gt;It&amp;#39;s a best practice to use agentic dashboard creation as a starting point for a new dash or edits to an existing dash, and then verify and tweak the updates to make sure it&amp;#39;s exactly what you want. This experience is still in open beta, so we&amp;#39;re still ironing out some of the kinks.&lt;/p&gt;
&lt;p&gt;Many more dashboards can now easily be created. With this in mind, we&amp;#39;ve created a markdown widget to encourage you to leave notes and document what you&amp;#39;ve built:&lt;/p&gt;
&lt;h3&gt;Dashboard revision history&lt;/h3&gt;
&lt;p&gt;Dashboards now maintain a revision history. Any edits made through the UI, Seer agent, or API are automatically tracked. To view prior revisions, click the clock icon in the upper-right corner.&lt;/p&gt;
&lt;p&gt;If you, or a 🤖, make a mistake, you can restore a known-good version by selecting a previous revision and clicking &lt;strong&gt;Revert to Selection&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Sentry use case: fixing and monitoring jest tests&lt;/h3&gt;
&lt;p&gt;Last week, we increased the number of CI runners we use for jest tests, moving from 4 to 8 on our main branch. We&amp;#39;ve had 4 runners for years to parallelize the work. Total CI time depends on how long the slowest runner takes to do its work; to make sure that everything finishes at the same time we measure the duration to run each test file and parallelize based on that. More runners should mean faster CI time.&lt;/p&gt;
&lt;p&gt;Instead, we noticed that overall CI time had been regressing for a few days, going from 6 minutes up to 10. The culprit was that the balancer script wasn&amp;#39;t running successfully because tests were failing out. Once we fixed some flaky tests, that time went down from ~10 minutes to less than 4. Our new runner configuration did improve overall CI time after all!&lt;/p&gt;
&lt;p&gt;To prevent this from happening again we used agentic dashboard creation (plus some manual metric creation) to whip up some new dashboards (and monitors) to keep tabs on flakes, balance failures, and overall slowness:&lt;/p&gt;
&lt;h2&gt;Insights are now Sentry dashboards&lt;/h2&gt;
&lt;p&gt;In the Dashboard nav item, you&amp;#39;ll see the option to explore Sentry Built dashboards:&lt;/p&gt;
&lt;p&gt;This is a &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/&quot;&gt;collection of dashboards pre-built by Sentry&lt;/a&gt; to address common monitoring use cases. The Sentry-built dashboards cannot be edited, but they can be duplicated to create custom dashboards. You can think of them as templates that can be adapted to address your specific use case.&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;frontend&lt;/strong&gt; side, the &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/frontend/web-vitals/&quot;&gt;Web Vitals&lt;/a&gt; dashboard surfaces LCP, INP, CLS, and TTFB across your real users, broken down by page, with a Performance Score that flags which pages have the most room to improve. Frontend Session Health connects deployments to crash and error rates, so you can see when a release tanked your stability. Frontend Assets shows you which JS and CSS assets are slow or render-blocking, useful when LCP regressed and you don&amp;#39;t yet know why.&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;backend&lt;/strong&gt; side, you get dashboards for slow queries (with drill-downs into individual query summaries and sample events), cache hit/miss rates, queue throughput and processing latency, and outbound API requests grouped by domain. The &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/backend/&quot;&gt;Backend Overview&lt;/a&gt; ties these together with p50/p75 duration and the most time-consuming queries and domains.&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;mobile&lt;/strong&gt;, there&amp;#39;s &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/mobile/mobile-vitals/&quot;&gt;Mobile Vitals&lt;/a&gt; (cold/warm app starts, slow and frozen frame rates, TTID/TTFD), &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/mobile/session-health/&quot;&gt;Mobile Session Health&lt;/a&gt; (crash-free sessions and users, with release annotations), and dedicated drill-downs for app starts and screen rendering.&lt;/p&gt;
&lt;p&gt;There are also &lt;strong&gt;framework-specific dashboards&lt;/strong&gt;: a &lt;a href=&quot;https://docs.sentry.io/product/dashboards/sentry-dashboards/nextjs/&quot;&gt;Next.js Overview&lt;/a&gt; with a tree-based SSR view for finding performance bottlenecks, and a Laravel Overview tuned for Laravel-specific metrics.&lt;/p&gt;
&lt;h2&gt;Creating dashboards via the Sentry CLI&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt; includes a full &lt;code&gt;dashboard&lt;/code&gt; command, bringing dashboard creation and management out of the browser and into your shell. You can list, view, create, and modify dashboards, including adding, editing, and deleting individual widgets, without ever leaving your terminal. With the new &lt;code&gt;dashboard&lt;/code&gt; command, dashboards become code-adjacent artifacts: scriptable, repeatable, and reviewable. Define dashboards in shell scripts or CI pipelines, commit them alongside your application code, and roll them out the same way you ship features.&lt;/p&gt;
&lt;p&gt;Every command supports &lt;code&gt;--json&lt;/code&gt; output, making it straightforward for scripts, internal tools, or AI coding agents to provision and update dashboards programmatically (that means you can easily create dashboards from Claude Code, GitHub Copilot, Cursor, or your agent of choice).&lt;/p&gt;
&lt;h3&gt;Sentry use case: investigating integrations&lt;/h3&gt;
&lt;p&gt;In order to test out a potential &lt;a href=&quot;https://github.com/getsentry/sentry-ruby/pull/2925&quot;&gt;yabeda integration&lt;/a&gt; for &lt;a href=&quot;https://docs.sentry.io/product/explore/metrics/&quot;&gt;application metrics&lt;/a&gt;, one of our engineers created a custom dashboard with the CLI and &lt;a href=&quot;https://github.com/dingsdax/fizzy/blob/e6c2c4dc3f30fa016c72c28d7371608065cae2cd/docs/sentry-dashboard.md&quot;&gt;documented the process&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Get started&lt;/h3&gt;
&lt;p&gt;Whether you&amp;#39;re cloning a Sentry-built dashboard as a starting point, prompting an agent to spin one up from scratch, or scripting dashboard creation directly from your terminal, the goal is the same: spend less time building the view and more time acting on what it shows you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sentry Dashboards&lt;/strong&gt; are available now on all plans. Open &lt;a href=&quot;https://sentry.io/orgredirect/organizations/:orgslug/dashboards/&quot;&gt;Dashboards&lt;/a&gt; to see them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Dashboards&lt;/strong&gt; are available on all plans. Click &lt;strong&gt;Create Dashboard&lt;/strong&gt; to start from scratch, or duplicate a pre-built dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic Dashboard creation&lt;/strong&gt; is live now for organizations with AI features enabled. If you don&amp;#39;t see the &lt;strong&gt;Generate Dashboard&lt;/strong&gt; option, check your org&amp;#39;s AI settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have questions or feedback?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Join the conversation &lt;a href=&quot;https://discord.com/invite/sentry&quot;&gt;in our Discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New to Sentry?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Try &lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Sentry for free&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>From vibe code to production-ready: observability for Next.js and Supabase apps</title><link>https://blog.sentry.io/nextjs-supabase-observability/</link><guid isPermaLink="true">https://blog.sentry.io/nextjs-supabase-observability/</guid><description>Instrument Next.js and Supabase with Sentry to get unified errors, distributed traces, logs, performance insights, and AI-assisted fixes.</description><pubDate>Mon, 11 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The way we build software has drastically changed over the past few years. What hasn&amp;#39;t changed is that this software ends up in front of real people: you, me, my mom.&lt;/p&gt;
&lt;p&gt;And when those users inevitably run into something broken, you as the application&amp;#39;s developer need to be equipped with the right tools, context and understanding of what broke, where it broke, and how to fix it as quickly as possible.&lt;/p&gt;
&lt;p&gt;Every day we&amp;#39;re inching closer to self-healing software. If you are building a Next.js application and are using Supabase as the backend service, the tooling described below can help you get one step closer to a self-closing loop of producing quality software and fixing what slipped through the cracks with minimal disruption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Supabase gives you query performance insights, row-level security (RLS) advisories, and edge function logs out of the box, but it can&amp;#39;t trace across your full stack&lt;/li&gt;
&lt;li&gt;Sentry fills that gap: &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/distributed-tracing/&quot;&gt;distributed traces&lt;/a&gt; from your Next.js frontend through Supabase Edge Functions to Postgres, all in one place&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/product/drains/supabase/&quot;&gt;Log draining from Supabase&lt;/a&gt; into Sentry gives you a single source of truth for errors, traces, and infrastructure logs&lt;/li&gt;
&lt;li&gt;Sentry auto-detects &lt;a href=&quot;https://docs.sentry.io/product/issues/issue-details/performance-issues/n-one-queries/&quot;&gt;N+1 queries&lt;/a&gt;, slow spans, and performance regressions without manual configuration&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/seer/&quot;&gt;Seer&lt;/a&gt;, Sentry&amp;#39;s AI debugger, can suggest a likely root cause for new issues automatically and hand off fixes to your coding agent&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The stack problem agents create&lt;/h2&gt;
&lt;p&gt;AI-assisted development has a specific failure mode: agents write working code that has no observability built in. You could end up with a Next.js app that talks to Supabase via three different connection methods (direct Postgres, the Supabase JS SDK, and Drizzle, because the agent kept switching strategies), edge functions running in Deno, and no unified view of what&amp;#39;s actually happening at runtime.&lt;/p&gt;
&lt;p&gt;The other failure mode is subtler. Agents forget indexes. They could end up writing N+1 queries that are invisible locally because your dev database has 40 rows. You ship, your database grows to 400 rows, and suddenly a search query takes ten seconds. Sentry catches this automatically, but only if it&amp;#39;s instrumented correctly from the start.&lt;/p&gt;
&lt;p&gt;Getting that instrumentation right requires understanding a few things about how Supabase and Sentry fit together.&lt;/p&gt;
&lt;h2&gt;Supabase&amp;#39;s built-in observability and its limits&lt;/h2&gt;
&lt;p&gt;Supabase has solid built-in observability. The &lt;em&gt;Query Performance&lt;/em&gt; panel in the dashboard shows which queries run most often and which consume the most time. That&amp;#39;s where you start when performance is the problem. The &lt;em&gt;Advisors&lt;/em&gt; surface security issues like missing RLS policies and rank them by severity. The &lt;em&gt;Index Advisor&lt;/em&gt; flags missing indexes before they become production incidents.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;Logs&lt;/em&gt; section gives you structured logs from every Supabase subsystem: edge functions, the Postgres REST API (PostgREST), the connection pooler, storage, and cron jobs. You can query them with SQL directly in the dashboard.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s genuinely useful. But it&amp;#39;s bounded by what Supabase can see, which is everything that happens inside Supabase. It can&amp;#39;t tell you that a slow Postgres query was triggered by a specific user action in your Next.js frontend, or that an edge function timeout caused a cascade of errors in your API layer. For that, you need &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;distributed tracing&lt;/a&gt; across the full stack.&lt;/p&gt;
&lt;h2&gt;Connecting Supabase logs to Sentry&lt;/h2&gt;
&lt;p&gt;The fastest way to get Supabase data into Sentry is the &lt;a href=&quot;https://docs.sentry.io/product/drains/supabase/&quot;&gt;log drain&lt;/a&gt;. In the Supabase dashboard, under &lt;em&gt;Logs &amp;gt; Drain&lt;/em&gt;, you add a destination and paste your Sentry data source name (DSN). All logs from that Supabase project start flowing into a corresponding Sentry project.&lt;/p&gt;
&lt;p&gt;A few things worth knowing about this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;#39;s currently all-or-nothing. You can&amp;#39;t filter by log level on the Supabase side before the drain&lt;/li&gt;
&lt;li&gt;Once logs are in Sentry, you can filter by severity (&lt;code&gt;severity:warn&lt;/code&gt;, &lt;code&gt;severity:error&lt;/code&gt;) in the Log Explorer&lt;/li&gt;
&lt;li&gt;Keep the log drain in its own Sentry project, separate from your Next.js app and your edge functions. This keeps the signal clean and makes it easier to set project-specific alerts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason to bother with this, beyond convenience, is that Sentry can correlate these infrastructure logs with traces from your application layer. When an edge function throws an error, you can see the full request path: Next.js page load → API route → edge function → Postgres query, with timing for each span.&lt;/p&gt;
&lt;p&gt;For a step-by-step walkthrough of this setup, see the &lt;a href=&quot;https://sentry.io/cookbook/setup-supabase-log-drain-monitoring/&quot;&gt;Supabase log drain monitoring recipe&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Instrumenting Next.js and Supabase Edge Functions&lt;/h2&gt;
&lt;p&gt;This is where most agent-generated setups go wrong. Next.js is a full-stack framework that runs in multiple runtimes: Node.js on the server, V8 in the browser, and potentially edge runtimes. Supabase Edge Functions run in Deno. These are not the same environment, and they need separate Sentry projects and separate SDK configurations.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cli.sentry.dev&quot;&gt;The Sentry CLI&lt;/a&gt; handles this detection automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;npx sentry@latest init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the Next.js app, your &lt;code&gt;sentry.server.config.ts&lt;/code&gt; should include the Supabase integration to get automatic instrumentation of database queries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;


const supabaseClient = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
  integrations: [
    // Instruments Supabase queries as spans in your traces
    // so you can see exactly which DB calls are slow
    Sentry.supabaseIntegration(supabaseClient, Sentry, {
      tracing: true,
      breadcrumbs: true,
    }),
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the Supabase integration, your traces will show that an API route was slow, but not which query caused it. With it, every Supabase SDK call becomes a named span with timing data. See the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/&quot;&gt;Next.js integrations docs&lt;/a&gt; for the full list of what&amp;#39;s available.&lt;/p&gt;
&lt;p&gt;For edge functions running in Deno, initialize Sentry at the top of each function before any other imports:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;

Sentry.init({
  dsn: Deno.env.get(&amp;quot;SENTRY_DSN&amp;quot;),
  tracesSampleRate: 1.0, // sample everything in edge functions; volume is usually low
});

Deno.serve(async (req) =&amp;gt; {
  return await Sentry.withIsolationScope(async () =&amp;gt; {
    // your handler code
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reason for separate projects: when Sentry&amp;#39;s AI features (more on this below) analyze an issue, they work within a project&amp;#39;s context. Mixing Next.js errors with Deno errors and Postgres logs in a single project makes that analysis noisier and less useful.&lt;/p&gt;
&lt;h2&gt;Automatic detection: N+1 queries, slow spans, and Web Vitals&lt;/h2&gt;
&lt;p&gt;Once instrumented, Sentry starts surfacing issues you didn&amp;#39;t know to look for.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N+1 queries&lt;/strong&gt; get detected automatically. If your code fetches a list of posts and then queries the database once per post to get comments, Sentry identifies the pattern and creates a performance issue. This is the kind of &amp;quot;logic&amp;quot; agents like to write constantly. It&amp;#39;s the natural way to express the functionality, and it&amp;#39;s invisible until you have real traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slow spans&lt;/strong&gt; appear in the Trace Explorer. You can see exactly which database query, API call, or server-side render is consuming time, with the full request context attached.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://webvitals.com/&quot;&gt;&lt;strong&gt;Core Web Vitals&lt;/strong&gt;&lt;/a&gt; for the frontend (&lt;a href=&quot;https://webvitals.com/lcp&quot;&gt;LCP&lt;/a&gt;, &lt;a href=&quot;https://webvitals.com/inp&quot;&gt;INP&lt;/a&gt;, &lt;a href=&quot;https://webvitals.com/cls&quot;&gt;CLS&lt;/a&gt;) show up in the Next.js performance dashboard alongside your API latency and server transaction data. Having frontend and backend performance in one place makes it easier to figure out whether a slow page is a rendering problem or a slow API response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;https://sentry.io/orgredirect/organizations/:orgslug/dashboards/?filter=onlyPrebuilt&amp;query=next.js&amp;sort=mostPopular&quot;&gt;prebuilt Next.js dashboard&lt;/a&gt; in Sentry covers most of what you need out of the box and doesn&amp;#39;t count against your dashboard quota.&lt;/p&gt;
&lt;h2&gt;Setting up agents to instrument correctly&lt;/h2&gt;
&lt;p&gt;Two things make the difference between an agent that instruments your app correctly and one that produces outdated, incomplete configuration.&lt;/p&gt;
&lt;h3&gt;MCPs over training data&lt;/h3&gt;
&lt;p&gt;Both Sentry and Supabase have Model Context Protocol (MCP) servers. When your coding agent has access to the Sentry MCP, it can query your actual issues, traces, and project configuration in real time instead of guessing based on training data that might be two years old. Sentry&amp;#39;s SDK has changed significantly, and agents without current context will often configure it as if it&amp;#39;s only for error monitoring, missing performance tracing entirely.&lt;/p&gt;
&lt;h3&gt;Skills files&lt;/h3&gt;
&lt;p&gt;For Claude Code, this is &lt;code&gt;.claude/&lt;/code&gt;. For Cursor and others, &lt;code&gt;.agents/&lt;/code&gt;. These files give your agent project-specific context that persists across sessions. Take a look at &lt;a href=&quot;https://docs.sentry.io/ai/agent-skills/&quot;&gt;our Agent Skills documentation&lt;/a&gt; for a detailed breakdown of all the skills Sentry offers.&lt;/p&gt;
&lt;p&gt;A practical workflow: when you need to add Sentry to a project, go to the Sentry docs, find the SDK for your framework, copy the setup prompt they provide, and give that to your agent. The docs include current best practices and the right SDK version. Don&amp;#39;t just tell the agent to &amp;quot;add Sentry.&amp;quot; It will find a way to do it, and the result will probably work, but it won&amp;#39;t be right.&lt;/p&gt;
&lt;h2&gt;Monitoring beyond errors&lt;/h2&gt;
&lt;p&gt;Errors are the obvious case. But some of the most useful monitoring is for things that aren&amp;#39;t errors.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;&lt;strong&gt;Log-based monitors&lt;/strong&gt;&lt;/a&gt; let you alert on patterns in your log stream. If you&amp;#39;re draining Supabase logs into Sentry, you can create a monitor that fires when the count of &lt;code&gt;connection received&lt;/code&gt; logs drops below a threshold in a given hour. Not an error, just a signal that something might be wrong with your database connectivity. In the Sentry UI: &lt;em&gt;Alerts &amp;gt; Create Alert &amp;gt; Logs&lt;/em&gt;, filter by message content, set a count threshold, and assign it to yourself or a team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic alerting&lt;/strong&gt; is useful when you don&amp;#39;t know your normal thresholds yet. Set an alert to use anomaly detection instead of a fixed value. Sentry&amp;#39;s ML figures out what &amp;quot;normal&amp;quot; looks like for your transaction response times and fires when something falls outside that pattern. Start with dynamic, tune to specific values once you understand your baseline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentry CLI for dashboards:&lt;/strong&gt; The new &lt;a href=&quot;https://sentry.io/cookbook/create-dashboards-with-ai-agent/&quot;&gt;&lt;strong&gt;Sentry CLI&lt;/strong&gt; has a &lt;code&gt;dashboards&lt;/code&gt; command&lt;/a&gt; that an agent can use to build a custom dashboard from your actual trace data. Point it at your project, ask it to build a performance dashboard for your application, and it will inspect your active transactions and spans to figure out what&amp;#39;s worth visualizing. The output isn&amp;#39;t perfect (you&amp;#39;ll want to review widget configurations), but it&amp;#39;s a reasonable starting point that takes about thirty seconds instead of thirty minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Seer: from alert to fix&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;Seer&lt;/a&gt; is Sentry&amp;#39;s AI debugger. It has access to your full issue history, traces, logs, and &lt;a href=&quot;https://sentry.io/product/session-replay/&quot;&gt;session replays&lt;/a&gt;. You can ask it plain questions: &amp;quot;which of my open issues are getting worse?&amp;quot; or &amp;quot;what are my slowest database queries?&amp;quot; and it will pull from your actual data to answer.&lt;/p&gt;
&lt;p&gt;The more interesting capability is &lt;a href=&quot;https://sentry.io/product/seer/autofix/&quot;&gt;Autofix&lt;/a&gt;. Configure it in your Sentry project settings by connecting your repository. When a new issue comes in, Seer automatically suggests a likely root cause and, if you want, generates a draft PR with a suggested fix. You can configure how far it goes: root cause only, or full fix with updated tests.&lt;/p&gt;
&lt;p&gt;For the Supabase security advisory workflow: the Supabase MCP exposes RLS policy issues and other advisories. An agent with both the Supabase MCP and the Sentry MCP can fetch those advisories and create Sentry issues from them, putting security problems into the same workflow as application errors. From there, Seer can pick them up and attempt fixes automatically.&lt;/p&gt;
&lt;p&gt;This is what &amp;quot;self-healing software&amp;quot; actually looks like in practice: not magic, but a pipeline where new issues get triaged, analyzed, and handed to a coding agent without you having to be the one who notices them first.&lt;/p&gt;
&lt;h2&gt;Where to start&lt;/h2&gt;
&lt;p&gt;The fastest path is three steps: run &lt;code&gt;npx sentry@latest init&lt;/code&gt; to instrument your Next.js app, add the Supabase integration to your server config for query-level spans. Then set up a log drain from Supabase into its own Sentry project. That gets you unified tracing across the full stack. From there, connect your repo to Seer and let it start suggesting fixes for new issues as they come in.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://sentry.io/integrations/supabase/&quot;&gt;Sentry Supabase integration docs&lt;/a&gt; cover setup end to end. Supabase has their own &lt;a href=&quot;https://supabase.com/docs/guides/telemetry/sentry-monitoring&quot;&gt;Sentry monitoring guide&lt;/a&gt; and a separate guide for &lt;a href=&quot;https://supabase.com/docs/guides/functions/examples/sentry-monitoring&quot;&gt;edge function monitoring&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Monitor Unreal Engine Game Performance with Application Metrics</title><link>https://blog.sentry.io/unreal-engine-performance-metrics/</link><guid isPermaLink="true">https://blog.sentry.io/unreal-engine-performance-metrics/</guid><description>The Unreal SDK now auto-instruments FPS, frame time, network health, and game stats, giving your team real player performance data in production.</description><pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Your Unreal game can ship with zero errors and still not feel great. Stutters during combat, a frame-rate cliff on the big boss, rubber-banding in multiplayer, none of it shows up as a crash and none of it shows up in Sentry, leaving you without any visibility into what your players are actually experiencing in the wild. Well, until now.&lt;/p&gt;
&lt;p&gt;Unreal Engine already gives you plenty of tools to measure game performance and collect runtime stats, but all that data stays on the dev&amp;#39;s machine.&lt;/p&gt;
&lt;p&gt;The Unreal SDK&amp;#39;s new automatic performance metrics feature closes this gap by piping FPS, frame time, network health, and other common game telemetry straight to Sentry, so your team gets actionable insight into where performance breaks down, on which hardware, for which players. Pair it with &lt;a href=&quot;https://docs.sentry.io/platforms/unreal/configuration/releases/&quot;&gt;Release &amp;amp; Health&lt;/a&gt; and you can watch the performance impact of each release land over time.&lt;/p&gt;
&lt;p&gt;A quick note before we dig in: every gamedev has used a profiler at some point. Automatic performance metrics are a different-but-related tool, both go after the same problem at different layers: metrics find &lt;strong&gt;where&lt;/strong&gt; the game is slowing down, profiling explains &lt;strong&gt;why&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;What Sentry now tracks&lt;/h2&gt;
&lt;p&gt;Currently, Unreal SDK auto-instruments metrics for several key areas that impact overall performance including frame time, network and game-specific stats.&lt;/p&gt;
&lt;h3&gt;Frame time&lt;/h3&gt;
&lt;p&gt;The most direct read on whether your game feels responsive. Frame times tell you &amp;quot;how long the engine spent on each frame&amp;quot;; breaking it down by thread tells you which subsystem is the bottleneck.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Average FPS&lt;/li&gt;
&lt;li&gt;Total frame time&lt;/li&gt;
&lt;li&gt;Game thread work time&lt;/li&gt;
&lt;li&gt;Render thread work time&lt;/li&gt;
&lt;li&gt;GPU frame time&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Comparing game thread vs render thread vs GPU time is the classic way to tell whether you&amp;#39;re CPU-bound or GPU-bound and which team (gameplay, rendering, content) owns the fix.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;FPS metric example (grouped by GPU)&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Network insights&lt;/h3&gt;
&lt;p&gt;Multiplayer performance lives or dies by connection quality, and crash reporting can&amp;#39;t see any of it. These metrics tell you whether packet loss, latency or bandwidth starvation is quietly degrading the experience.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incoming/outgoing bandwidth&lt;/li&gt;
&lt;li&gt;Packet throughput and loss&lt;/li&gt;
&lt;li&gt;Client ping and jitter&lt;/li&gt;
&lt;li&gt;Active connection count&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Server builds additionally get per-client ping averages, per-client bandwidth and saturated-connection counts for load-shedding analysis (see the &lt;a href=&quot;https://docs.sentry.io/platforms/unreal/metrics/#network&quot;&gt;full list of network metrics&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;These metrics only exist during active multiplayer sessions. Singleplayer games without networking emit nothing here and some values are client-only (ping, jitter) or server-only (active clients, saturation).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ping metric example&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Game stats&lt;/h3&gt;
&lt;p&gt;A small grab-bag of engine-level signals that often explain hitches the frame-time breakdown alone can&amp;#39;t.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Number of active UObjects&lt;/li&gt;
&lt;li&gt;Physical memory used by the process&lt;/li&gt;
&lt;li&gt;Duration of the blocking GC pause&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A UObject count that climbs steadily between GCs is a classic leak signature and correlating it with GC pause duration often reveals exactly when a leak starts hurting player experience.&lt;/p&gt;
&lt;p&gt;Unlike frame time, these are sampled on a slower cadence: memory and object count every 60 seconds, GC pause emitted after each collection cycle. Values change slowly enough that per-frame resolution would be wasted throughput.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Used Memory metric example (grouped by platform, console-only)&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Sampling performance metrics&lt;/h2&gt;
&lt;p&gt;Emitting a metric every frame would be an overhead on its own. To avoid that, the SDK samples at a fixed interval, emitting one data point every N frames for per-frame metrics like frame time and FPS, and every N seconds for slower-changing ones like memory use or network health. The defaults are conservative and tunable per project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;~2 samples per second for frame time at 60 FPS&lt;/li&gt;
&lt;li&gt;Every 10 seconds for network&lt;/li&gt;
&lt;li&gt;Every 60 seconds for game stats&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On any single client this is sparse, a hitch on a non-sampled frame won&amp;#39;t be captured. But across many players the aggregate distribution converges on the real picture. You want to know &amp;quot;what&amp;#39;s the p95 frame time on RTX 3050 hardware?&amp;quot;, not &amp;quot;what did frame #47312 look like on dev&amp;#39;s laptop.&amp;quot; If you need tighter resolution simply dial the interval down.&lt;/p&gt;
&lt;h2&gt;Metrics attributes&lt;/h2&gt;
&lt;p&gt;An aggregate FPS number on its own doesn&amp;#39;t tell you much. What makes it useful is breaking it down: per GPU, per platform, per level. Every automatic metric is tagged with context attributes so you can do exactly that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPU model name&lt;/li&gt;
&lt;li&gt;Number of CPU cores&lt;/li&gt;
&lt;li&gt;Total physical RAM&lt;/li&gt;
&lt;li&gt;Screen resolution&lt;/li&gt;
&lt;li&gt;Current game map/level name&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Metrics also carry the release version, operating system, and crucially the trace ID of whatever was happening when they were emitted. That last one is what separates metrics-in-Sentry from a standalone monitoring tool: spot a frame-time spike in the dashboard, click into the sample and you land in the full trace for that moment alongside any errors and spans captured with it.&lt;/p&gt;
&lt;p&gt;For example, group FPS (&lt;code&gt;game.perf.fps&lt;/code&gt;) by GPU (&lt;code&gt;gpu.name&lt;/code&gt;) and the answer to &amp;quot;what FPS do RTX 3080 players actually see versus RTX 3050?&amp;quot; is one query away. Swap the grouping to OS (&lt;code&gt;os.name&lt;/code&gt;) and you can compare memory footprint across Xbox, PlayStation and Switch.&lt;/p&gt;
&lt;h2&gt;Try it out and tell us what&amp;#39;s next&lt;/h2&gt;
&lt;p&gt;Automatic performance metrics are enabled by default in Unreal SDK &lt;a href=&quot;https://github.com/getsentry/sentry-unreal/releases&quot;&gt;1.11.0&lt;/a&gt;. See the &lt;a href=&quot;https://docs.sentry.io/platforms/unreal/metrics/#automatic-metrics&quot;&gt;Unreal SDK metrics docs&lt;/a&gt; for more on engine-version requirements and advanced configuration. Automatic metrics work on desktop, consoles and Android (with iOS support coming soon).&lt;/p&gt;
&lt;p&gt;Ship a build with automatic performance metrics enabled and let it run for a few sessions, that&amp;#39;s often enough to see whether hardware segmentation, frame-time percentiles or network health are already surfacing something worth fixing.&lt;/p&gt;
&lt;p&gt;And since the feature is still experimental, what gets measured next is up for grabs. If there&amp;#39;s a signal you wish we were capturing, open an issue on the &lt;a href=&quot;https://github.com/getsentry/sentry-unreal/issues&quot;&gt;Unreal SDK repo&lt;/a&gt;, as that&amp;#39;s the best way to shape where this goes.&lt;/p&gt;
&lt;p&gt;Have questions or feedback?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Join the conversation &lt;a href=&quot;https://discord.com/invite/sentry&quot;&gt;in our Discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Email us at &lt;a href=&quot;mailto:gaming-updates@sentry.io&quot;&gt;&lt;strong&gt;gaming-updates@sentry.io&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New to Sentry?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Try &lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Sentry for free&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Fixing JavaScript observability, one library at a time</title><link>https://blog.sentry.io/fixing-javascript-observability/</link><guid isPermaLink="true">https://blog.sentry.io/fixing-javascript-observability/</guid><description>Sentry is adding TracingChannel support to 44 JavaScript libraries upstream, replacing fragile monkey-patching with native observability that works across all runtimes.</description><pubDate>Thu, 07 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the past few weeks, we have been driving a cross-ecosystem effort to replace the &amp;quot;monkey-patching&amp;quot; that powers all JavaScript APM tools today with something built into the runtime. Here is why, how, and where it stands.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This applies to server-side JavaScript only (Node.js, Bun, Deno, Cloudflare Workers). Browsers do not have &lt;code&gt;diagnostics_channel&lt;/code&gt; and lack the async context propagation primitives needed to polyfill it.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Monkey-patching does not scale&lt;/h2&gt;
&lt;p&gt;My teammate &lt;a href=&quot;https://github.com/s1gr1d&quot;&gt;Sigrid&lt;/a&gt; wrote a detailed breakdown of &lt;a href=&quot;https://blog.sentry.io/observability-with-tracing-channels/&quot;&gt;why monkey-patching is failing and how &lt;code&gt;TracingChannel&lt;/code&gt; solves it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The short version: every JavaScript APM tool, including Sentry&amp;#39;s, instruments libraries by intercepting &lt;code&gt;require()&lt;/code&gt; and &lt;code&gt;import&lt;/code&gt; calls at runtime using &lt;a href=&quot;https://github.com/nodejs/import-in-the-middle&quot;&gt;import-in-the-middle&lt;/a&gt; (IITM) and &lt;a href=&quot;https://github.com/nodejs/require-in-the-middle&quot;&gt;require-in-the-middle&lt;/a&gt; (RITM). This breaks with ECMAScript Modules (ESM), does not work in non-Node runtimes, conflicts with bundlers, and couples us to internal implementation details we do not control. The SDK also must load before the library it instruments, or instrumentation silently does nothing.&lt;/p&gt;
&lt;p&gt;This is not a Sentry-specific problem. Every APM vendor maintaining JavaScript instrumentation deals with the same fragility. The ecosystem is stuck.&lt;/p&gt;
&lt;p&gt;Most library maintainers do not think about observability. They do not know what they would need to expose, and adopting something like OpenTelemetry means taking on an implementation burden, not just adding a standard. APMs managed to patch their way around this for years, so nobody on the library side ever had to figure it out.&lt;/p&gt;
&lt;p&gt;But there&amp;#39;s a better way.&lt;/p&gt;
&lt;h2&gt;TracingChannels - observability without patching&lt;/h2&gt;
&lt;p&gt;In late 2025, we were working with &lt;a href=&quot;https://github.com/pi0&quot;&gt;Pooya Parsa&lt;/a&gt; (creator of Nitro, h3, and the unjs ecosystem) on the best way to build a Sentry SDK for the Nitro framework. During that conversation, my teammate Sigrid suggested we look into &lt;a href=&quot;https://nodejs.org/api/diagnostics_channel.html#tracingchannel&quot;&gt;TracingChannel&lt;/a&gt;, a built-in API from Node&amp;#39;s &lt;code&gt;diagnostics_channel&lt;/code&gt; module. Sigrid&amp;#39;s &lt;a href=&quot;https://blog.sentry.io/observability-with-tracing-channels/&quot;&gt;blog post&lt;/a&gt; covers that API in depth, but the core idea is simple: if a library publishes structured events on a &lt;code&gt;TracingChannel&lt;/code&gt;, any APM tool can subscribe to those events without patching anything. The library just says &amp;quot;a query started&amp;quot; and &amp;quot;a query ended,&amp;quot; and whoever is listening can create spans from that.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Library side (e.g. inside mysql2)


const queryChannel = tracingChannel(&amp;#39;mysql2:query&amp;#39;);

queryChannel.tracePromise(async () =&amp;gt; {
  return await connection.query(sql);
}, { query: sql, serverAddress: host, serverPort: port });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cost of this added code is minimal, so this is an easy sell for library maintainers. From APM&amp;#39;s side, we just need to subscribe to that tracing channel and we get the events. No IITM, no RITM, no loader hooks, no initialization ordering. Zero overhead when nobody is listening. Works across Node, Bun, and Deno. Bundler safe. The API has been available since Node 18, and &lt;a href=&quot;https://www.npmjs.com/package/dc-polyfill&quot;&gt;&lt;code&gt;dc-polyfill&lt;/code&gt;&lt;/a&gt; covers runtimes that lack it, which already matches our support range.&lt;/p&gt;
&lt;h2&gt;Everyone agrees, nobody is pushing&lt;/h2&gt;
&lt;p&gt;After getting enough learnings about the tracing channel API and how to make it work with OpenTelemetry, I opened &lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-js/issues/6088&quot;&gt;an issue on Otel JS&lt;/a&gt; in November 2025 to discuss &lt;code&gt;TracingChannel&lt;/code&gt; support.&lt;/p&gt;
&lt;p&gt;The response was positive. A while after, someone from the OTel team even created a draft API approach for integrating &lt;code&gt;TracingChannel&lt;/code&gt; into the OTel SDK.&lt;/p&gt;
&lt;p&gt;But there is no significant push to drive ecosystem adoption. The draft exists; the ecosystem work does not.&lt;/p&gt;
&lt;p&gt;Everyone agrees that &lt;code&gt;TracingChannel&lt;/code&gt; is the future of JavaScript observability, but nobody is doing the work of getting libraries to adopt it. We have many instrumentations across databases, web frameworks, message queues, and AI providers that need &lt;code&gt;TracingChannel&lt;/code&gt; support. That is a mountain of upstream PRs, each requiring understanding the library&amp;#39;s internals, writing a proposal that maintainers will accept, implementing the changes, and iterating on review feedback.&lt;/p&gt;
&lt;p&gt;So I thought &amp;quot;fine, why not just get the ball rolling?&amp;quot;&lt;/p&gt;
&lt;p&gt;The first step was proving the pattern works. I had already built &lt;code&gt;TracingChannel&lt;/code&gt; support by hand in &lt;a href=&quot;https://github.com/h3js/h3/pull/1251&quot;&gt;h3&lt;/a&gt;, &lt;a href=&quot;https://github.com/h3js/srvx/pull/141&quot;&gt;srvx&lt;/a&gt;, &lt;a href=&quot;https://github.com/unjs/unstorage/pull/707&quot;&gt;unstorage&lt;/a&gt;, &lt;a href=&quot;https://github.com/unjs/db0/pull/193&quot;&gt;db0&lt;/a&gt;, and Nitro as part of the earlier SDK work. The unjs ecosystem was receptive and moved fast, which gave us shipped examples to point to and an end-to-end mental model: how events should be shaped, how context propagation flows, how to make it work with OTel, and what &lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/&quot;&gt;semantic conventions&lt;/a&gt; to follow.&lt;/p&gt;
&lt;p&gt;We also learned early that you can&amp;#39;t just say &amp;quot;hey you should use &lt;code&gt;TracingChannel&lt;/code&gt;,&amp;quot; which is just begging to be shelved to collect dust. Instead, like we did with Nitro, we say &amp;quot;Hey, we will do it for you and help you own it.&amp;quot; Accepting code into a repository adds a burden of maintenance, so we offer to help own it and make it part of the library.&lt;/p&gt;
&lt;p&gt;With that in mind, I reached out to &lt;code&gt;pg&lt;/code&gt;, &lt;code&gt;mysql2&lt;/code&gt;, and &lt;code&gt;redis&lt;/code&gt; to gauge their interest, offering to fully own this &amp;#39;til it ships and provide support even after. These are the top database driver libraries in the ecosystem, accounting for over 60 million downloads per week combined. If we can get &lt;code&gt;TracingChannel&lt;/code&gt; in them, we can get other libraries. All three said yes and were open to receiving a PR.&lt;/p&gt;
&lt;p&gt;I also reached out to &lt;a href=&quot;https://github.com/Qard&quot;&gt;Stephen Belanger&lt;/a&gt;, the creator of the &lt;code&gt;diagnostics_channel&lt;/code&gt; API in Node.js core. He is now helping push this forward, providing feedback on proposals and acting as the voice of authority which is sometimes needed to convince maintainers.&lt;/p&gt;
&lt;p&gt;So one by one, we&amp;#39;re making this happen across the ecosystem.&lt;/p&gt;
&lt;p&gt;For context on how this fits into the bigger picture: My team is working on making our SDK runtime-agnostic, we are working multiple paths in parallel, most of which have an immediate effect. The &lt;code&gt;TracingChannel&lt;/code&gt; initiative work is the long-term play. We cannot expect users to upgrade to new library versions overnight, and we probably won&amp;#39;t convince everyone to implement them at the same time so the migration will be gradual.&lt;/p&gt;
&lt;h2&gt;Scaling it with AI&lt;/h2&gt;
&lt;p&gt;Here is the practical reality: Being one person trying to add &lt;code&gt;TracingChannel&lt;/code&gt; support to 44 libraries is just not going to happen. I do not know the internals of any of them. I have never looked at the Redis protocol implementation or &lt;code&gt;mysql2&lt;/code&gt;&amp;#39;s query pipeline before this project.&lt;/p&gt;
&lt;p&gt;So I built a feedback loop using Claude Code that handles the per-library heavy lifting via SKILLS:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Research and Propose. Given a library name, Claude researches its async model, existing OTel instrumentation, maintenance status, and internal architecture, then drafts a proposal following all the patterns we have established. I review and adjust before it goes anywhere.&lt;/li&gt;
&lt;li&gt;Implement. Given an approved proposal, Claude produces a working implementation with tests, handling &lt;code&gt;tracePromise&lt;/code&gt;/&lt;code&gt;traceCallback&lt;/code&gt; selection, &lt;code&gt;hasSubscribers&lt;/code&gt; guards, Node 18 compatibility, and integration tests against real services via Docker.&lt;/li&gt;
&lt;li&gt;Capture Review Feedback. When a PR gets reviewed upstream, Claude triages every comment, assesses validity, suggests responses, and flags patterns that should inform future proposals. I decide what to act on and handle all communication with maintainers myself.&lt;/li&gt;
&lt;li&gt;Update the Tracker. Claude fetches the latest status of every upstream PR and keeps the migration tracker current.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each cycle feeds the next one. Learnings from one library&amp;#39;s review process improve the next library&amp;#39;s proposal. The knowledge compounds and is dumped into a &lt;code&gt;LEARNING.md&lt;/code&gt; file to guide future work.&lt;/p&gt;
&lt;p&gt;To clarify the human/AI split: Claude handles research, boilerplate implementation, and pattern application. I handle architecture decisions, insertion point identification, all maintainer communication, and final review of every line before it ships. Critically, every commit is co-authored and AI involvement is made transparent. Library maintainers interact with a human, not with an AI. I kept certain parts human-led because that shows respect to the maintainer&amp;#39;s work, which is critical to convincing them to adopt code into their library.&lt;/p&gt;
&lt;p&gt;This approach turned what would be a multi-year solo effort into a production line where I can keep dishing out proposals every day, start implementations in parallel, learn from them all and integrate the learnings into pending and future work.&lt;/p&gt;
&lt;h2&gt;10 merged, 34 to go&lt;/h2&gt;
&lt;p&gt;We are tracking many instrumentations across four categories. Here is where things stand:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Category&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Total&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Merged&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;PR Open&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;In Discussion&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Not Started&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;OTel-provided&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;24&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;6&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Sentry-built&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;10&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;0&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;0&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Other ecosystem&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;5&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Logging&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;0&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;0&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Total&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;44&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;10&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Notable wins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sidorares/node-mysql2/pull/4178&quot;&gt;mysql2&lt;/a&gt; - Merged. One of the most popular database drivers in the npm ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/redis/node-redis/pull/3195&quot;&gt;node-redis&lt;/a&gt; and &lt;a href=&quot;https://github.com/redis/ioredis/pull/2089&quot;&gt;ioredis&lt;/a&gt; - Both merged. The two dominant Redis clients now ship &lt;code&gt;TracingChannel&lt;/code&gt; support.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/h3js/h3/pull/1251&quot;&gt;h3&lt;/a&gt;, &lt;a href=&quot;https://github.com/h3js/srvx/pull/141&quot;&gt;srvx&lt;/a&gt;, &lt;a href=&quot;https://github.com/unjs/unstorage/pull/707&quot;&gt;unstorage&lt;/a&gt; - All merged. The unjs ecosystem was early and enthusiastic. This touches Nitro, which in turn touches Nuxt and other downstream frameworks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also helped establish ecosystem coordination through an &lt;a href=&quot;https://github.com/e18e/ecosystem-issues/issues/255&quot;&gt;e18e umbrella issue&lt;/a&gt; and the &lt;a href=&quot;https://github.com/unjs/untracing&quot;&gt;&lt;code&gt;untracing&lt;/code&gt;&lt;/a&gt; spec that standardizes &lt;code&gt;TracingChannel&lt;/code&gt; usage for library authors.&lt;/p&gt;
&lt;h2&gt;What this means for Sentry&lt;/h2&gt;
&lt;p&gt;This flips the instrumentation model. Libraries own the contract, and we subscribe to it. Every problem described above (ESM breakage, init ordering, runtime lock-in, bundler conflicts) goes away. Our instrumentation code gets simpler, and we stop maintaining runtime-specific hacks.&lt;/p&gt;
&lt;p&gt;This also benefits every APM tool, not just Sentry. Driving it builds trust with library maintainers and the broader community, sure, but several maintainers have specifically called out that they appreciate the approach because it helps everyone and is not biased towards any one APM provider.&lt;/p&gt;
&lt;h2&gt;The flywheel is starting&lt;/h2&gt;
&lt;p&gt;Take &lt;code&gt;node-redis&lt;/code&gt; as a case study. During our collaboration with the Redis team, they were already working on their own first-party OpenTelemetry instrumentation. They wanted our &lt;code&gt;TracingChannel&lt;/code&gt; proposal to align with and power that instrumentation. We re-implemented their already shipped metrics plugin using tracing channels and it worked without changing a single test. Now, we are &lt;a href=&quot;https://github.com/redis/node-redis/pull/3218&quot;&gt;helping them with traces&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Shortly after &lt;code&gt;mysql2&lt;/code&gt; shipped &lt;code&gt;TracingChannel&lt;/code&gt; support, someone independently built &lt;a href=&quot;https://github.com/Vunovati/mysql2-otel-instrumentation&quot;&gt;&lt;code&gt;mysql2-otel-instrumentation&lt;/code&gt;&lt;/a&gt;, a pure &lt;code&gt;diagnostics_channel&lt;/code&gt; subscriber that replaces OTel&amp;#39;s monkey-patched &lt;code&gt;@opentelemetry/instrumentation-mysql2&lt;/code&gt;. The motivation was exactly the problem we are solving: RITM was not working. A library adds &lt;code&gt;TracingChannel&lt;/code&gt; support, and the subscribers manifest on their own.&lt;/p&gt;
&lt;h2&gt;What&amp;#39;s next&lt;/h2&gt;
&lt;p&gt;We have open PRs against Express, PostgreSQL (pg), Knex, and GraphQL, the kind of libraries where &lt;code&gt;TracingChannel&lt;/code&gt; support means millions of applications get better observability without changing a line of their own code. MongoDB, Mongoose, Prisma, and Hono are in active discussion, and we have drafted proposals for Koa and Consola. There are still 20+ libraries on the list we have not reached out to yet, including Node&amp;#39;s built-in HTTP module, Kafka clients, and AI provider SDKs.&lt;/p&gt;
&lt;p&gt;Beyond individual library adoption, the next layer is reducing duplication on the consumer side. Right now, every APM tool that subscribes to a &lt;code&gt;TracingChannel&lt;/code&gt; has to independently map library payloads to OpenTelemetry semantic conventions. We are designing a shared mapper registry, a set of co-maintained modules that translate &lt;code&gt;TracingChannel&lt;/code&gt; events into standardized spans and attributes. The goal is to build and prove this internally at Sentry first, then open-source it so any APM vendor can plug in. If a library ships &lt;code&gt;TracingChannel&lt;/code&gt; support and a mapper exists, instrumentation becomes automatic.&lt;/p&gt;
&lt;p&gt;The long-term picture is an ecosystem where libraries emit events as a first-class concern, mappers are community-maintained, and APM tools compete on what they do with the data rather than on how creatively they can patch your dependencies. We are not there yet, but the flywheel is turning.&lt;/p&gt;
&lt;p&gt;You can help by talking about tracing channels and advocating for their adoption in the libraries you use. If you maintain a library and want to add &lt;code&gt;TracingChannel&lt;/code&gt; support, the &lt;a href=&quot;https://github.com/unjs/untracing&quot;&gt;untracing&lt;/a&gt; conventions and our &lt;a href=&quot;https://github.com/getsentry/js-tracing-channels-proposals/&quot;&gt;published proposals&lt;/a&gt; are a good starting point.&lt;/p&gt;
</content:encoded></item><item><title>Improved debugging for Expo apps with the React Native SDK</title><link>https://blog.sentry.io/debugging-expo-react-native-sdk/</link><guid isPermaLink="true">https://blog.sentry.io/debugging-expo-react-native-sdk/</guid><description>Sentry&apos;s React Native SDK now gives Expo apps OTA update context, emergency launch detection, EAS build hooks, and navigation performance spans.</description><pubDate>Wed, 06 May 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Events from Expo apps account for about 75% of the total event volume we receive from React Native apps. That number made it an easy decision to invest in updates to the Sentry React Native SDK to improve the debugging and performance workflow for your Expo apps.&lt;/p&gt;
&lt;p&gt;With these updates, you can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter issues by OTA update channel or version to instantly narrow down whether a problem is tied to a specific update&lt;/li&gt;
&lt;li&gt;Get alerted on emergency launches so you know when your OTA pipeline is failing before users report it&lt;/li&gt;
&lt;li&gt;Track EAS Build health in Sentry so you don&amp;#39;t have to dig through build logs to find out what broke&lt;/li&gt;
&lt;li&gt;See the full picture of navigation performance including prefetch timing and asset loading&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Automatic OTA update context on every event&lt;/h2&gt;
&lt;p&gt;When you ship over-the-air updates with Expo Updates, things can go wrong in ways that are invisible without the right context. Which update channel was the user on? Which runtime version? Was this the embedded bundle or a downloaded update?&lt;/p&gt;
&lt;p&gt;Now, every Sentry event is automatically enriched with an &lt;code&gt;ota_updates&lt;/code&gt; context, with no other setup required. You get the update ID, channel, runtime version, launch duration, and whether the app is using embedded assets. All of this is captured out of the box in Expo projects.&lt;/p&gt;
&lt;p&gt;We also set searchable tags (&lt;code&gt;expo.updates.channel&lt;/code&gt;, &lt;code&gt;expo.updates.runtime_version&lt;/code&gt;, &lt;code&gt;expo.updates.update_id&lt;/code&gt;) on every event, so you can filter your issue stream down to a specific channel or update with a single search query.&lt;/p&gt;
&lt;h2&gt;Emergency launch detection&lt;/h2&gt;
&lt;p&gt;Expo Updates performs an emergency launch when it fails to load the latest OTA update and falls back to the embedded bundle. When this happens, your users are silently running an older version of your app, and you might not even know.&lt;/p&gt;
&lt;p&gt;The SDK now detects emergency launches at startup and automatically sends a warning-level event to Sentry with the reason. You can set up an alert on the &lt;code&gt;expo.updates.emergency_launch&lt;/code&gt; tag and know immediately when your update pipeline is broken in production.&lt;/p&gt;
&lt;h2&gt;EAS build hooks: Track build failures in Sentry&lt;/h2&gt;
&lt;p&gt;Build failures in EAS Build happen on remote infrastructure, outside your local environment and outside your app. Until now, debugging them meant digging through EAS build logs manually.&lt;/p&gt;
&lt;p&gt;With the new EAS Build Hooks, you can send build lifecycle events directly to Sentry. Add three script entries to your &lt;code&gt;package.json&lt;/code&gt; (or just one if you prefer the combined &lt;code&gt;on-complete&lt;/code&gt; hook) and every failed build will send an &lt;code&gt;EASBuildError&lt;/code&gt; event with the build platform, profile, build ID, git commit hash, and CI status.&lt;/p&gt;
&lt;p&gt;You can also capture successful builds to give you a complete picture of your build pipeline health right inside Sentry. All events are tagged with &lt;code&gt;eas.*&lt;/code&gt; tags for easy filtering and alerting.&lt;/p&gt;
&lt;p&gt;Setup is minimal: point the hook scripts at the ones the SDK provides, set your DSN as an EAS secret, and you&amp;#39;re done. Check out the &lt;a href=&quot;https://docs.sentry.io/platforms/react-native/manual-setup/expo/eas-build-hooks/&quot;&gt;EAS Build Hooks documentation&lt;/a&gt; for setup instructions.&lt;/p&gt;
&lt;h2&gt;Performance spans for Expo Router prefetching&lt;/h2&gt;
&lt;p&gt;Expo Router v5 introduced &lt;code&gt;router.prefetch()&lt;/code&gt; to preload routes before the user navigates to them. It&amp;#39;s a great tool for perceived performance, but until now, prefetch timing was invisible in your traces.&lt;/p&gt;
&lt;p&gt;Wrapping your router with &lt;code&gt;Sentry.wrapExpoRouter(useRouter())&lt;/code&gt; now creates a &lt;code&gt;navigation.prefetch&lt;/code&gt; span for each prefetch call. You can see exactly how long route preloading takes alongside your other navigation spans, and identify routes where prefetching is slow or unnecessary.&lt;/p&gt;
&lt;h2&gt;Expo constants and environment context&lt;/h2&gt;
&lt;p&gt;Every event from an Expo app is automatically enriched with an &lt;code&gt;expo_constants&lt;/code&gt; context containing metadata about the execution environment: where the app is running (Expo Go, standalone, bare), the app name and version from &lt;code&gt;app.json&lt;/code&gt;, Expo SDK version, EAS project ID, and debug mode status.&lt;/p&gt;
&lt;p&gt;Combined with the OTA updates context, this gives you a complete picture of the environment for every event without writing a single line of configuration code.&lt;/p&gt;
&lt;h2&gt;Image and asset loading instrumentation&lt;/h2&gt;
&lt;p&gt;Image and asset loading is one of the biggest contributors to how fast your app feels. We&amp;#39;ve added automatic performance spans for two of the most common Expo packages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;expo-image&lt;/code&gt;: Wrap Image with &lt;code&gt;Sentry.wrapExpoImage(Image)&lt;/code&gt; once at startup, and every &lt;code&gt;Image.prefetch()&lt;/code&gt; and &lt;code&gt;Image.loadAsync()&lt;/code&gt; call gets a performance span.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;expo-asset&lt;/code&gt;: Wrap Asset with &lt;code&gt;Sentry.wrapExpoAsset(Asset)&lt;/code&gt; for spans on &lt;code&gt;Asset.loadAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both wrappers are safe to call multiple times, create spans only when a trace is active (zero overhead otherwise), and don&amp;#39;t require expo-image or expo-asset to be installed. They&amp;#39;re peer dependencies.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;All of the OTA update and constants context is enabled by default. No configuration, no extra dependencies. For build hooks and performance instrumentation, setup is a few lines of code. Make sure you&amp;#39;re on &lt;a href=&quot;https://github.com/getsentry/sentry-react-native&quot;&gt;version 8.10&lt;/a&gt; to get the latest improvements and fixes.&lt;/p&gt;
&lt;p&gt;Expo also allows you to &lt;a href=&quot;https://expo.dev/blog/diagnose-and-debug-errors-faster-with-issues-and-replays-from-sentry-in-expo&quot;&gt;pull in issue details and replays from Sentry&lt;/a&gt; for errors occurring in your EAS deployments.&lt;/p&gt;
&lt;p&gt;Don&amp;#39;t have a Sentry account yet? &lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Sign up for free&lt;/a&gt;, the developer plan includes everything you need to instrument an Expo app end to end. Have feedback on these integrations or ideas for what should come next? Open an issue on &lt;a href=&quot;https://github.com/getsentry/sentry-react-native&quot;&gt;&lt;code&gt;getsentry/sentry-react-native&lt;/code&gt;&lt;/a&gt; or drop into our &lt;a href=&quot;https://discord.gg/sentry&quot;&gt;Discord&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Introducing Application Metrics: Track the signal, see the spike, jump to the trace</title><link>https://blog.sentry.io/introducing-application-metrics/</link><guid isPermaLink="true">https://blog.sentry.io/introducing-application-metrics/</guid><description>We just launched Application Metrics, a new way to track critical signals in your application. It lets you understand your users with context and catch problems before they become errors.</description><pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few weeks ago we had a bug with &lt;a href=&quot;https://docs.sentry.io/product/explore/session-replay/&quot;&gt;Session Replay&lt;/a&gt;. Replays were failing in some browsers once more than 1,000 video segments loaded. We had no idea how often it happened or who was hitting it, and because the failure didn&amp;#39;t always produce an error, we had no way to find affected users to reproduce it.&lt;/p&gt;
&lt;p&gt;Before, we could&amp;#39;ve answered this with spans or logs, but it&amp;#39;s clunky — spans are often sampled, so you can miss outliers; logs are less structured and tend to change over time. Both are better suited for investigation. Metrics are ideal for tracking known behaviors over time. So we set up a metric in the Sentry SDK with a user and provider attribute, filtered for sessions over 1,000 segments, and had a repro case in minutes.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the job &lt;a href=&quot;https://sentry.io/product/metrics/&quot;&gt;Application Metrics&lt;/a&gt; is for: track the signals you care about, and attach the context you might need later. When something breaks, the data is already there waiting.&lt;/p&gt;
&lt;h2&gt;Full events, not pre-aggregated counters&lt;/h2&gt;
&lt;p&gt;Metrics tools designed for tracking infrastructure telemetry tend to aggregate, stripping out information like user, IP address, and region. They&amp;#39;re just a counter.&lt;/p&gt;
&lt;p&gt;Sentry&amp;#39;s Application Metrics stores full events, including high-cardinality fields like user. So you&amp;#39;re able to ask not just &amp;quot;was the checkout experience slow in my application?&amp;quot;, but &amp;quot;was the checkout experience slow for users on the east coast?&amp;quot;, or &amp;quot;was a specific user&amp;#39;s scheduled job causing a queue backlog?&amp;quot;&lt;/p&gt;
&lt;h2&gt;Same SDK, one line of code&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re on a recent Sentry SDK, metrics are already enabled. No new dependencies or sidecar — just one more line.&lt;/p&gt;
&lt;p&gt;There are three types you&amp;#39;ll reach for most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Counter&lt;/strong&gt; — increment a number each time something happens. Think &lt;code&gt;payment.declined&lt;/code&gt;, &lt;code&gt;search.zero_results&lt;/code&gt;, or &lt;code&gt;email.failed&lt;/code&gt;. Good for tracking rates and totals you want to alert on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribution&lt;/strong&gt; — record a value each time something happens, then ask questions about the spread. How long did that job take? How many items were in the queue? Use this when the average isn&amp;#39;t the whole story.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gauge&lt;/strong&gt; — track a current value over time. &lt;code&gt;queue.depth&lt;/code&gt;, &lt;code&gt;cache.size&lt;/code&gt;, &lt;code&gt;active.connections&lt;/code&gt;. The number you&amp;#39;d want on a dashboard.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can attach attributes to all three. That&amp;#39;s where Application Metrics differs from many infrastructure monitoring tools, which pre-aggregate and strip context. When you attach &lt;code&gt;user.id&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, or &lt;code&gt;projectId&lt;/code&gt;, the event is stored with that context intact — so when a distribution spikes, you&amp;#39;re not just looking at a number, you&amp;#39;re looking at a number tied to a specific user, in a specific region, on a specific project.&lt;/p&gt;
&lt;h2&gt;Click a spike, see the trace&lt;/h2&gt;
&lt;p&gt;By storing full metrics events — including the trace ID — metrics become part of a broader trace-connected debugging workflow.&lt;/p&gt;
&lt;p&gt;When a metric reaches an unexpected threshold (a background job backing up with unsent emails; a UI component taking painfully long to load) you can jump from that metric to traces, logs, and errors, and get a full picture of what actually went wrong around the time your pager went off:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is a 429 error happening in a loop at the same time that a distribution measuring React component load times spikes?&lt;/li&gt;
&lt;li&gt;Is an upstream email service running slow at the same time that a gauge measuring queue depth increases?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How we actually used this to find a Session Replay bug&lt;/h2&gt;
&lt;p&gt;To investigate the Session Replay problem, we began by adding a distribution that tracked the number of video segments loaded. We included the high-cardinality &lt;code&gt;projectId&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the code we added to start tracking video segments in replays:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const replayId = replay?.getReplay().id;
const projectId = replay?.getReplay().project_id;

const onLoadAllEvents = useEffectEvent(() =&amp;gt; {
  const attributes = {
    projectId: String(projectId),
    replayId,
  };

  Sentry.metrics.distribution(&amp;#39;replay.eventCount&amp;#39;, events?.length ?? 0, {
    attributes,
  });

  Sentry.metrics.distribution(&amp;#39;replay.videoEventCount&amp;#39;, videoEvents?.length ?? 0, {
    attributes,
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href=&quot;https://github.com/getsentry/sentry/pull/114001&quot;&gt;getsentry/sentry#114001&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We attached &lt;code&gt;replayId&lt;/code&gt; and &lt;code&gt;projectId&lt;/code&gt; as attributes on the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/enriching-events/scopes/&quot;&gt;scope&lt;/a&gt; so we could isolate high event counts to specific projects and replays. Given that we were having trouble reproducing the problem, this would help us catch the issue red-handed, tracing it back to a specific organization.&lt;/p&gt;
&lt;p&gt;With that in place, we quickly learned two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The issue was rare — just 7 occurrences in the past week.&lt;/li&gt;
&lt;li&gt;We had the exact users affected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From there, we could trace those sessions, reproduce the issue, and fix it.&lt;/p&gt;
&lt;p&gt;Because each metric event carries a trace ID, we could go further. We added targeted logs to see exactly what the user was doing when &amp;gt;1,000 frames loaded — were they scrubbing the video, loading many videos in succession, etc. Next time we saw a &lt;code&gt;replay.videoEventCount&lt;/code&gt; over 1,000, we jumped to the connected trace, saw the log lines, and had the context to fix the bug.&lt;/p&gt;
&lt;h2&gt;Metrics vs. everything else&lt;/h2&gt;
&lt;p&gt;Metrics aren&amp;#39;t a replacement for &lt;a href=&quot;https://sentry.io/product/error-monitoring/&quot;&gt;errors&lt;/a&gt;, traces, or logs. They fill a specific gap: tracking interesting, well-understood events in your application with high fidelity.&lt;/p&gt;
&lt;p&gt;Not every event needs to be a metric. Logs are great during investigation. But when you find a signal you care about long-term — something that tracks application health — turn it into a metric.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good candidates:&lt;/strong&gt; business KPIs tied to code execution (&lt;code&gt;payment.declined&lt;/code&gt;, &lt;code&gt;search.zero_results&lt;/code&gt;), application health indicators (&lt;code&gt;job.retried&lt;/code&gt;, &lt;code&gt;email.failed&lt;/code&gt;), resource utilization (&lt;code&gt;queue.depth&lt;/code&gt;, &lt;code&gt;cache.hit_rate&lt;/code&gt;), and success/failure rates you want to alert on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not the right fit:&lt;/strong&gt; infrastructure metrics like CPU and memory (use your infra tool), forensic debugging (use &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;Sentry Logs&lt;/a&gt;), or request-level performance and connectivity (use &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;Sentry Tracing&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Start with the metric your team checks first&lt;/h2&gt;
&lt;p&gt;Every Sentry plan comes with 5GB of Application Metrics. If you&amp;#39;re on a recent SDK version, you already have access.&lt;/p&gt;
&lt;p&gt;Pick the one signal your team reaches for first when something goes wrong. Maybe it&amp;#39;s &lt;code&gt;checkout.failed&lt;/code&gt;, maybe it&amp;#39;s &lt;code&gt;queue.depth&lt;/code&gt;, maybe it&amp;#39;s &lt;code&gt;deployment.duration&lt;/code&gt;. Instrument it, attach the attributes you&amp;#39;d want to filter on — user, project, region, whatever matters for that metric — and set an alert threshold.&lt;/p&gt;
&lt;p&gt;When it fires, click through to the trace, find the context around the spike, and fix it.&lt;/p&gt;
&lt;p&gt;Start a free Application Metrics trial in &lt;strong&gt;&lt;a href=&quot;https://sentry.io/orgredirect/organizations/:orgslug/explore/metrics/&quot;&gt;Explore &amp;gt; Metrics&lt;/a&gt;&lt;/strong&gt;, or check out the &lt;a href=&quot;https://docs.sentry.io/product/explore/metrics/&quot;&gt;Application Metrics docs →&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Two commands to Sentry: now on Stripe Projects</title><link>https://blog.sentry.io/sentry-stripe-projects/</link><guid isPermaLink="true">https://blog.sentry.io/sentry-stripe-projects/</guid><description>Sentry is now a provider in Stripe Projects. Provision error monitoring, upgrade plans, and open your dashboard from the CLI or from your coding agent.</description><pubDate>Wed, 29 Apr 2026 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Two commands. That&amp;#39;s how little it takes to go from nothing to a fully configured Sentry project with error monitoring, performance tracing, and session replay:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects init my-app
stripe projects add sentry/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No signup form. No email verification dance. No dashboard tab-switching to copy-paste a DSN into your &lt;code&gt;.env&lt;/code&gt;. Your account is created, your project is provisioned, and five environment variables land in your working directory, ready for your SDK to pick up.&lt;/p&gt;
&lt;p&gt;And if you&amp;#39;re using a coding agent? It does the same thing, except you didn&amp;#39;t type the commands. You just said &amp;quot;add error monitoring.&amp;quot;&lt;/p&gt;
&lt;h2&gt;What this actually is&lt;/h2&gt;
&lt;p&gt;Sentry is now a provider in &lt;a href=&quot;https://projects.dev&quot;&gt;Stripe Projects&lt;/a&gt;. Stripe Projects is a CLI workflow that lets developers (and their AI agents) discover, provision, and manage infrastructure services directly from the terminal. Think of it as a package manager, but for the services your app depends on at runtime.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the full catalog:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ stripe projects catalog sentry

SERVICES
    project   ● Free tier
    seer      ● Paid

PLANS
    developer ● Free
    team      ● $29/month
    business  ● $89/month
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two deployable services (a Sentry project and Seer AI), three plan tiers. All manageable from the CLI. Billing goes through your existing Stripe payment method, with no separate Sentry billing setup.&lt;/p&gt;
&lt;h2&gt;The &amp;quot;just tell your agent&amp;quot; part&lt;/h2&gt;
&lt;p&gt;When you run &lt;code&gt;stripe projects init&lt;/code&gt;, it scaffolds agent skill files into your project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.agents/skills/stripe-projects-cli/SKILL.md
.claude/skills/stripe-projects-cli/SKILL.md
.cursor/rules/stripe-projects-cli.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These teach Claude Code, Cursor, or any coding agent how to use the Stripe Projects CLI. The agent reads the skill, discovers available services via &lt;code&gt;stripe projects catalog sentry --json&lt;/code&gt;, and provisions what you need.&lt;/p&gt;
&lt;p&gt;A real interaction looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You: &amp;quot;Add error monitoring to this project&amp;quot;

Agent: [runs stripe projects catalog sentry --json]
Agent: [runs stripe projects add sentry/project --no-interactive --accept-tos]
Agent: &amp;quot;Done. Sentry is provisioned. Your DSN and auth token are in .env.
        I can integrate the Sentry SDK into your app next.&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent doesn&amp;#39;t need special Sentry knowledge. It just needs the Stripe Projects CLI and the skill file that &lt;code&gt;init&lt;/code&gt; already created. Provisioning becomes a step it handles between writing your code and running your tests.&lt;/p&gt;
&lt;p&gt;Once the account is provisioned, you can ask the agent to instrument your app with &lt;code&gt;sentry init&lt;/code&gt;. Since the auth token and DSN are already in the environment, the &lt;a href=&quot;https://cli.sentry.dev&quot;&gt;Sentry CLI&lt;/a&gt; knows exactly which project to target, with no configuration prompts, no guesswork.&lt;/p&gt;
&lt;h2&gt;Upgrades, downgrades, and the billing dance&lt;/h2&gt;
&lt;p&gt;Plans are first-class resources:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects add sentry/team                          # start on Team ($29/mo)
stripe projects upgrade sentry-plan sentry/business      # upgrade to Business
stripe projects downgrade sentry-plan sentry/team        # change your mind
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All non-interactive, all scriptable. Billing happens through Stripe&amp;#39;s &lt;a href=&quot;https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens&quot;&gt;Shared Payment Token&lt;/a&gt;, so your existing Stripe payment method pays for Sentry with zero billing configuration on the Sentry side.&lt;/p&gt;
&lt;p&gt;You can also add Seer (our AI debugging assistant) as a separate service:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects add sentry/seer      # $40/active contributor/month
stripe projects remove sentry-seer   # if you change your mind
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The magic login&lt;/h2&gt;
&lt;p&gt;This one&amp;#39;s my favorite. When you need your Sentry dashboard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects open sentry
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This mints a single-use magic login URL. Click it (or let the CLI open it), and you&amp;#39;re logged into your Sentry dashboard. You skip the password prompt, the single sign-on (SSO) redirect, and the &amp;quot;which account was this again?&amp;quot; moment. Straight to your issues page.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s the kind of feature that sounds trivial but has a surprisingly dense security model once you start thinking about two-factor authentication (2FA) users, expired passwords, and SSO bypass prevention.&lt;/p&gt;
&lt;h2&gt;Multi-team collaboration&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s where it gets interesting. Stripe&amp;#39;s protocol distinguishes between the &lt;strong&gt;account owner&lt;/strong&gt; (the Stripe account&amp;#39;s email) and the &lt;strong&gt;actor&lt;/strong&gt; (the person running the CLI command). We use this to build a proper collaboration model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First team member runs &lt;code&gt;stripe projects add sentry/project&lt;/code&gt; → creates the Sentry org&lt;/li&gt;
&lt;li&gt;Second team member runs the same command → joins the &lt;em&gt;same&lt;/em&gt; Sentry org&lt;/li&gt;
&lt;li&gt;Everyone shares one org, one billing setup, one set of projects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We tie the Sentry organization to the Stripe organization, so everyone on the same Stripe account ends up in the same Sentry org. No per-developer silos, no &amp;quot;who created this and why can&amp;#39;t I see it&amp;quot; conversations.&lt;/p&gt;
&lt;h2&gt;Credential rotation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects rotate sentry-project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;New DSN, new auth token, old ones revoked. The fresh credentials land in your &lt;code&gt;.env&lt;/code&gt; automatically. The dashboard stays closed.&lt;/p&gt;
&lt;h2&gt;How to try it&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install the Stripe CLI and the Projects plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe plugin install projects
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Initialize and add Sentry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;stripe projects init my-app
stripe projects add sentry/project
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;That&amp;#39;s it. Your &lt;code&gt;SENTRY_DSN&lt;/code&gt; and &lt;code&gt;SENTRY_AUTH_TOKEN&lt;/code&gt; are in &lt;code&gt;.env&lt;/code&gt;, ready for any Sentry SDK to pick up.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the full lifecycle (catalog browsing, plan management, Seer, deep links, credential rotation), check the &lt;a href=&quot;https://projects.dev&quot;&gt;Stripe Projects documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re building with AI coding agents and want error monitoring that provisions itself, &lt;a href=&quot;https://projects.dev&quot;&gt;give it a try&lt;/a&gt;. And if your agent breaks something in the process, well, you&amp;#39;ll have Sentry to tell you about it.&lt;/p&gt;
</content:encoded></item><item><title>Sentry&apos;s integration with Perforce is now generally available</title><link>https://blog.sentry.io/perforce-integration-ga/</link><guid isPermaLink="true">https://blog.sentry.io/perforce-integration-ga/</guid><description>Sentry&apos;s Perforce P4 integration is now GA, bringing stack trace linking, suspect commits, and on-demand source context to game dev and VFX teams.</description><pubDate>Wed, 29 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Perforce meets Sentry&lt;/h2&gt;
&lt;p&gt;If you work in game development, VFX, or any industry dealing with large binary assets, chances are your codebase lives in Perforce P4. It&amp;#39;s the version control system behind some of the biggest games and creative projects in the world — and until now, it&amp;#39;s been one of the last major SCMs without first-class Sentry support.&lt;/p&gt;
&lt;p&gt;Today, we&amp;#39;re changing that. The Sentry + Perforce P4 integration is now generally available for all Sentry organizations.&lt;/p&gt;
&lt;h2&gt;What you get&lt;/h2&gt;
&lt;p&gt;The integration connects your P4 server directly to Sentry, unlocking the same source-code-aware debugging workflows that Git-based teams have relied on for years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stack trace linking&lt;/strong&gt; — Click from an error&amp;#39;s stack trace directly to the corresponding file in your Perforce P4 depot or P4 Code Review (formerly Helix Swarm) instance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit tracking&lt;/strong&gt; — Associate Perforce P4 changelists with Sentry releases so you always know exactly what code shipped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspect commits&lt;/strong&gt; — Sentry automatically identifies which changelists likely introduced an error, cutting your triage time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suggested assignees&lt;/strong&gt; — Get assignment recommendations based on changelist authorship — the person who last touched the code gets surfaced first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P4 Code Review (formerly Swarm) linking&lt;/strong&gt; — If your team uses the P4 Code Review (formerly Helix Swarm) application for code reviews and browser-based depot browsing, Sentry links directly to your P4 Code Review instance. Stack trace links open the exact file in P4 Code Review&amp;#39;s web UI, so reviewers and investigators can jump from an error straight into the code without needing a local workspace.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;On-demand source context&lt;/h2&gt;
&lt;p&gt;The most requested capability during our beta: show me the code. Previously, showing the source code for native crashes required users to upload source maps to Sentry. Without source maps, showing the stacktraces was possible, but they didn&amp;#39;t have source context.&lt;/p&gt;
&lt;p&gt;With on-demand SCM source context, Sentry fetches source code directly from your Perforce P4 depot and displays it inline in the stack trace — even when your crash dumps or error reports don&amp;#39;t include embedded source. Expand any in-app frame and Sentry pulls the relevant lines on the fly, with the error line highlighted.&lt;/p&gt;
&lt;p&gt;This is especially valuable for native game development workflows where minidumps and crash reports rarely carry source context. Instead of switching to your IDE or running &lt;code&gt;p4 print&lt;/code&gt; manually, the code is right there in the issue.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;Setup takes just a few minutes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Connect your server&lt;/strong&gt; — Go to Settings &amp;gt; Integrations &amp;gt; Perforce and enter your credentials.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configure code mappings&lt;/strong&gt; — Map your Sentry projects to Perforce P4 depots and set up path translations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable source context (optional)&lt;/strong&gt; — Turn on SCM source context in your project&amp;#39;s General Settings to get inline code in stack traces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sentry communicates with your Perforce P4 server using the P4Python library, executing lightweight read-only commands (&lt;code&gt;p4 depots&lt;/code&gt;, &lt;code&gt;p4 changes&lt;/code&gt;, &lt;code&gt;p4 print&lt;/code&gt;). Each organization maintains isolated credentials, and we support both password-based auth and pre-generated P4 tickets for LDAP environments.&lt;/p&gt;
&lt;h2&gt;How to get started&lt;/h2&gt;
&lt;p&gt;During beta, we worked closely with multiple game studios to battle-test the integration against real-world Perforce P4 deployments. We resolved concurrency challenges around P4 trust and ticket file isolation, ensuring connections stay clean and independent — whether you have one project or hundreds hitting the same or multiple servers.&lt;/p&gt;
&lt;p&gt;The Perforce integration is available now for all Sentry organizations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/organization/integrations/source-code-mgmt/perforce/&quot;&gt;Read the Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have questions or feedback?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Join the conversation &lt;a href=&quot;https://discord.com/invite/sentry&quot;&gt;in our Discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Email us at &lt;a href=&quot;mailto:gaming-updates@sentry.io&quot;&gt;gaming-updates@sentry.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New to Sentry?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Try &lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Sentry for free&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Introducing Seer Agent: The answer is already in Sentry. Now you can ask for it.</title><link>https://blog.sentry.io/introducing-seer-agent/</link><guid isPermaLink="true">https://blog.sentry.io/introducing-seer-agent/</guid><description>Most AI debugging tools start from whatever you paste in. Seer Agent starts from everything Sentry already knows about your app. Now in open beta.</description><pubDate>Tue, 28 Apr 2026 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a story about an engineer&amp;#39;s night that could have been bad, but ended up… not so bad.&lt;/p&gt;
&lt;p&gt;A few weeks ago, on a Saturday, our &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;AI debugger, Seer&lt;/a&gt;, started failing.&lt;/p&gt;
&lt;p&gt;Note the big scary spike on the right.&lt;/p&gt;
&lt;p&gt;The errors were generic failures from the LLM calls, nothing that pointed at a root cause. Most of the team wasn&amp;#39;t scheduled to be on this weekend, and it just so happened Indragie, our Head of AI, was online. He started paging engineers.&lt;/p&gt;
&lt;p&gt;While he waited for people to come online, he opened up a tool we&amp;#39;ve been testing internally for a few months now: Seer Agent. Indragie told Seer Agent a bit about what he was seeing, and asked it to figure out what was going on.&lt;/p&gt;
&lt;p&gt;It came back in seconds. The model calls were being rate-limited in specific regions for a specific model, even though we had enough provisioned throughput to handle the traffic. The rate limiting turned out to be a symptom of an upstream infrastructure outage on the provider&amp;#39;s side, which we confirmed after the incident, but Seer Agent had already pointed us at the exact region-and-model pattern that made the provider&amp;#39;s role obvious. Everything else was fine.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the kind of finding that would normally start with someone pulling up a dashboard, filtering by region, cross-referencing traffic against error rate, noticing the shape, and then working backwards to why one specific region was stumbling. Indragie knows his stuff, but he&amp;#39;s not contributing to the codebase day to day, he&amp;#39;s management ;), so it would have taken him at least half an hour to get there. If we&amp;#39;re being honest, probably longer.&lt;/p&gt;
&lt;p&gt;He had the root cause ready before the on-call engineer joined the channel.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s the job Seer Agent is for: to investigate any issue in your application from &amp;#39;big super visible outage that has people shouting at you on Twitter&amp;#39; to &amp;#39;things are running slow and you don&amp;#39;t know why&amp;#39;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Today, we&amp;#39;re rolling out &lt;a href=&quot;https://sentry.io/product/seer/agent/&quot;&gt;Seer Agent&lt;/a&gt; to everyone in open beta.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The problem isn&amp;#39;t always an issue&lt;/h2&gt;
&lt;p&gt;Seer&amp;#39;s original premise was simple: when Sentry catches an issue, Seer reads the stack trace, the trace data, the logs, replays, commit history, and the code, and tells you what&amp;#39;s wrong. It works well because the investigation has a concrete starting point (the issue), and the data you need is already linked to it.&lt;/p&gt;
&lt;p&gt;But a lot of debugging doesn&amp;#39;t start with an error.&lt;/p&gt;
&lt;p&gt;Sometimes it starts the way Indragie&amp;#39;s example started: you do have an issue, but the error message isn&amp;#39;t the most helpful and the real failure is somewhere upstream that the stack trace doesn&amp;#39;t reach.&lt;/p&gt;
&lt;p&gt;In all of those cases, you know something about the symptom. You just don&amp;#39;t know where to look.&lt;/p&gt;
&lt;p&gt;So you start manually: open the trace explorer, write a query, filter by environment, group by region, switch to &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;logs&lt;/a&gt;, pivot on a tag, go look at the service that&amp;#39;s upstream of this one, check its error rates, go back to traces, try a different span attribute. You&amp;#39;re not debugging yet. You&amp;#39;re navigating to where the debugging will happen.&lt;/p&gt;
&lt;p&gt;Seer Agent is the tool that does that navigation for you. You describe what you&amp;#39;re seeing, and it does the traversal across all of the context Sentry has on your system and tells you what it found.&lt;/p&gt;
&lt;h2&gt;Your telemetry is already a graph&lt;/h2&gt;
&lt;p&gt;You can already search across your telemetry in Sentry&amp;#39;s Explore product. You can write queries against traces, filter logs, pivot on attributes. Explore is powerful, and for people who already know the ins and outs of their Sentry data it&amp;#39;s the fastest way to answer a specific question.&lt;/p&gt;
&lt;p&gt;The problem with starting a debugging session in Explore is that you have to know the shape of your data before you can ask anything. If you don&amp;#39;t know which service is upstream of the failing one, you can&amp;#39;t filter for it. If you don&amp;#39;t know what span attribute to group by, the group-by is a shrug. Explore rewards operators who already have the map.&lt;/p&gt;
&lt;p&gt;Seer Agent doesn&amp;#39;t search your telemetry the way a generic LLM with a search tool would. Sentry&amp;#39;s telemetry is already &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;trace-connected&lt;/a&gt;. When an error happens, we know the trace it happened in. It knows the spans inside that trace, the logs emitted during those spans, the deploy that was live at the time, and the commits in that deploy. The agent walks those connections directly. It isn&amp;#39;t guessing at time ranges and hoping the right rows show up in a text search; it&amp;#39;s traversing a graph that was built at ingest.&lt;/p&gt;
&lt;p&gt;Concretely: if you ask about an error, Seer Agent can pull the exact trace that produced it, the exact spans in that trace, the exact logs emitted by those spans, and the exact source lines the spans came from, without a single &lt;code&gt;WHERE timestamp BETWEEN&lt;/code&gt; clause. Then it can walk the same graph in the other direction: which other services participated in traces that touched this endpoint, which of them were unhealthy at the same moment, and what their error rates looked like.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s what made Indragie&amp;#39;s investigation fast. He didn&amp;#39;t tell Seer Agent &amp;quot;look at region-level error rates for the Vertex AI provider.&amp;quot; He gave it the Sentry issue. It pulled the trace, saw which regions the failing calls were routed to, cross-referenced against recent calls to other models that went through the same provider, noticed that one specific model family was failing in specific regions while others were fine, and surfaced the pattern. Four steps of manual pivoting, done in one pass.&lt;/p&gt;
&lt;h2&gt;Fixing the hard issues&lt;/h2&gt;
&lt;p&gt;Some bugs are fun to investigate and tackle yourself. &amp;quot;Lmao look at this silly line of code, who wrote this — oh no, it was me.&amp;quot;&lt;/p&gt;
&lt;p&gt;Others are not. They&amp;#39;re big and ugly and complex and require you to have (or quickly obtain) an absurd amount of context in your brain just to know where to start. Not coincidentally, these are things Seer Agent is very good at.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failures whose root cause is upstream of your service.&lt;/strong&gt; Your stack trace ends at your own call site; the real cause is a 429 from someone else&amp;#39;s data center. Without Seer Agent you go find the provider&amp;#39;s status page, check whether the region you use is affected, and correlate against your own traffic. Seer Agent correlates the traffic against the request shape (provider, model, region, time) and tells you whether the failure is distributed in a pattern that indicates an upstream cause before you open another tab.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failures that don&amp;#39;t trigger a clean alert.&lt;/strong&gt; A slow degradation on a single endpoint, a 1% error rate that started two hours ago, a tail-latency increase that&amp;#39;s only visible in p99. These are the investigations that start with &amp;quot;I noticed this and I want to know if it&amp;#39;s real.&amp;quot; Seer Agent can pull the baseline for you, compare the current window against it, and tell you whether the thing you noticed is statistically interesting or noise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failures that span services.&lt;/strong&gt; An issue fires in service A, but the real cause is that service B started returning malformed responses ten minutes ago. A trace-connected graph is the only way to see this cleanly, and a human walking the graph manually will lose context two hops in. The agent doesn&amp;#39;t.&lt;/p&gt;
&lt;p&gt;The bottleneck moves from &amp;quot;where do I look&amp;quot; to &amp;quot;what do I do about what I found,&amp;quot; which is where you actually want your engineers spending their time.&lt;/p&gt;
&lt;h2&gt;Multiplayer Mode in Slack&lt;/h2&gt;
&lt;p&gt;The Slack Seer agent is in active development, but in beta and ready to use today. You&amp;#39;ll be able to start an investigation the same way you&amp;#39;d ask an on-call engineer, by DMing or mentioning it in an incident channel, without having to bounce to the Sentry UI while you&amp;#39;re trying to put out a fire. Here&amp;#39;s an example how we were using Seer Agent in Slack while building it: &lt;/p&gt;
&lt;p&gt;The more interesting thing is that the investigation becomes multiplayer. In the Sentry UI, Seer Agent is a solo tool. But in Slack, anyone in the channel can redirect it mid-step, add context the agent didn&amp;#39;t have, or just watch the traversal and learn the system a little better. The investigation also stays in the thread after the incident resolves, so when the same pattern shows up next month, someone can search for it instead of starting over.&lt;/p&gt;
&lt;p&gt;You can also trigger Autofix directly from Slack. Sentry alerts now include a &amp;quot;Fix with Seer&amp;quot; button and an initial read on the likely error. Clicking it kicks off the full Autofix workflow. This is currently in public beta. Read more about it in the &lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/seer/&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The setup is light. If you already have the Sentry &lt;a href=&quot;https://docs.sentry.io/organization/integrations/notification-incidents/slack/&quot;&gt;Slack integration&lt;/a&gt; installed, the Fix with Seer button on your error alerts is already live. If you don&amp;#39;t, install it from Settings → Integrations. To DM Seer Agent or @mention it in a channel, run &lt;code&gt;/sentry link&lt;/code&gt; in Slack to connect your account — if the Sentry app is already installed, just pick the same workspace when you link and the new features turn on.&lt;/p&gt;
&lt;h2&gt;What we&amp;#39;re building next&lt;/h2&gt;
&lt;p&gt;A few of the things on the short list, roughly ordered by when you&amp;#39;ll see them:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auto-triage on incident creation.&lt;/strong&gt; Right now, you have to go to Sentry or Slack and prompt Seer. The better version is the one where an incident getting created automatically fires off an investigation and posts the findings back to the incident channel before anyone has to ask. There&amp;#39;s a design for this on our side, and we&amp;#39;re starting with our own incident workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Proactive follow-ups.&lt;/strong&gt; When the agent finishes an analysis, it should suggest the next question, not wait for you to figure out what to ask next. &amp;quot;Do you want me to check whether this pattern exists in other services?&amp;quot; is a cheap prompt to generate and a large quality-of-life win for investigations that run long.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Message queueing and forceful interrupts.&lt;/strong&gt; Small items, but both high-frequency complaints: you can&amp;#39;t queue a follow-up while the agent is thinking, and sometimes you want to kill the current step and redirect without losing the session. Both are on the near-term list.&lt;/p&gt;
&lt;h2&gt;How to try it&lt;/h2&gt;
&lt;p&gt;Seer Agent is in open beta for all Sentry users. Open any page in Sentry, hit &lt;code&gt;Cmd + /&lt;/code&gt; or click the &amp;quot;Ask Seer&amp;quot; button, and ask it something.&lt;/p&gt;
&lt;p&gt;Peek a the docs &lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/seer/#seer-agent&quot;&gt;here&lt;/a&gt;, and we&amp;#39;ll run a workshop on it next month if you want to watch the team drive it live.&lt;/p&gt;
&lt;p&gt;If you find a case where it falls over, &lt;a href=&quot;https://github.com/getsentry/sentry/discussions/105737&quot;&gt;tell us&lt;/a&gt;. Half of what&amp;#39;s on the &amp;quot;what we&amp;#39;re building&amp;quot; list above came from people using it and telling us exactly where the agent went wrong.&lt;/p&gt;
</content:encoded></item><item><title>Two years without cookies on the site, here&apos;s where we ended up</title><link>https://blog.sentry.io/two-years-without-cookies-on-the-site/</link><guid isPermaLink="true">https://blog.sentry.io/two-years-without-cookies-on-the-site/</guid><description>Two years after removing all advertising cookies from sentry.io, here&apos;s what changed: roughly 70% of our growth budget now goes to awareness, and new activated users have been growing exponentially.</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In January 2024, I wrote about &lt;a href=&quot;https://blog.sentry.io/we-removed-advertising-cookies-heres-what-happened/&quot;&gt;removing all advertising cookies and user tracking from sentry.io&lt;/a&gt;. It was eight months into the decision at the time, and we were still figuring out what broke and what surprised us. That post struck a nerve: it became one of the most-read things we&amp;#39;ve ever published, probably because everyone building or running a product on the web was watching the same cookie deprecation timeline and wondering what would actually happen if someone just ripped the bandaid off.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s been over two years now. We never put the cookies back (also never planned to). And the way we spend our growth budget has changed pretty dramatically as a result. Not because we planned some grand strategy from the start, but because removing cookies forced us to rethink where we put our money, and what we actually expected it to do. Roughly 70% of our growth budget now goes to awareness. Here&amp;#39;s a sample of what that actually looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We signed a multi-year deal with the Golden State Warriors, Valkyries, and Chase Center.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://syntax.fm/&quot;&gt;Syntax.fm&lt;/a&gt; became part of Sentry in 2023 rather than starting our own corporate podcast.&lt;/li&gt;
&lt;li&gt;We spend a decent amount on &lt;a href=&quot;https://www.linkedin.com/posts/activity-7439422767827066880-Yf2A&quot;&gt;billboards and OOH&lt;/a&gt; every year.&lt;/li&gt;
&lt;li&gt;Podcasts, Reddit, YouTube, third-party newsletters, and influencers are huge channels for us.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.sentry.io/another-year-another-750-000-to-open-source-maintainers/&quot;&gt;We donated $750,000 to open source maintainers&lt;/a&gt; through our &lt;a href=&quot;https://opensourcepledge.com/&quot;&gt;Open Source Pledge&lt;/a&gt; these last two years (and more prior).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Along with the core business model and everything else we do, it&amp;#39;s safe to say these investments are working. Our new activated users have been growing exponentially.&lt;/p&gt;
&lt;p&gt;This type of investment is pretty uncommon for a company that sells to developers, but we started a lot smaller before getting to these bigger plays. Here&amp;#39;s what I&amp;#39;ve learned so far.&lt;/p&gt;
&lt;h2&gt;The ways people discover software are changing&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re building a product, you&amp;#39;ve probably noticed this already: the ways people find and evaluate tools are shifting under everyone&amp;#39;s feet.&lt;/p&gt;
&lt;p&gt;There used to be a fairly predictable path. Someone Googles a problem, clicks a few results, maybe reads a comparison post, signs up for a trial. You could grow a product by being good at showing up in that flow: writing content, running some ads, doing SEO.&lt;/p&gt;
&lt;p&gt;That path is getting less reliable. Referral traffic from Google is down and zero-click searches are up. &lt;a href=&quot;https://searchengineland.com/zero-click-searches-up-organic-clicks-down-456660&quot;&gt;Semrush&amp;#39;s 2025 data&lt;/a&gt; shows around 60% of Google searches now end without a click to any website. When AI Overviews appear, organic click-through rates &lt;a href=&quot;https://www.onely.com/blog/zero-click-search-is-evolving-into-zero-search-discovery/&quot;&gt;drop roughly 40%&lt;/a&gt;. People&amp;#39;s feeds and inboxes are flooded with increasingly competent AI-generated content and outreach, making it harder for anything genuine to cut through.&lt;/p&gt;
&lt;p&gt;Meanwhile, companies are scrambling to figure out AEO/GEO (answer engine optimization / generative engine optimization — trying to get LLMs to recommend their products), because it&amp;#39;s a million if not billion dollar play for the future. Unfortunately the emerging playbook for that is &lt;a href=&quot;https://x.com/indexsy/status/2027819009958449367&quot;&gt;flooding the zone with listicles&lt;/a&gt;, AI-generated templates, or taking the low road against competitors with comparison pages.&lt;/p&gt;
&lt;p&gt;In short: there is less organic traffic to go around, LLMs are increasingly making recommendations on behalf of the people who used to click through to your site, and the internet is getting noisier. If you&amp;#39;re trying to get your product in front of people, the old playbook is degrading fast.&lt;/p&gt;
&lt;p&gt;Because of all of this, we decided the way to grow Sentry isn&amp;#39;t by out-producing the noise. It&amp;#39;s by showing up in channels where authenticity still compounds, where real people actually spend time, where there&amp;#39;s emotional connection, and doing so in phases so we can actually see what&amp;#39;s working.&lt;/p&gt;
&lt;h2&gt;Awareness spend pays off differently now&lt;/h2&gt;
&lt;p&gt;One thing that changed how I think about where to spend: the channels that drive awareness are now the same channels that LLMs pull from when making recommendations.&lt;/p&gt;
&lt;p&gt;YouTube has overtaken Reddit as the most frequently cited social platform in AI-generated answers. &lt;a href=&quot;https://www.adweek.com/media/youtube-reddit-ai-search-engine-citations/&quot;&gt;Data from Bluefish&lt;/a&gt; (reported in Adweek) shows YouTube appeared as a cited source in about 16% of LLM answers over the past six months, compared to 10% for Reddit. &lt;a href=&quot;https://higoodie.com/blog/most-cited-domains-in-llms&quot;&gt;Goodie AI&amp;#39;s analysis&lt;/a&gt; of 6.1 million citations shows YouTube&amp;#39;s share of social citations roughly doubled between August and December 2025.&lt;/p&gt;
&lt;p&gt;A developer watching a technical YouTube video where someone uses Sentry to debug a hydration error is one thing. An LLM citing that video when someone asks &amp;quot;what&amp;#39;s the best tool for catching hydration errors?&amp;quot; is another thing entirely. Brand awareness, discoverability, and LLM recommendations aren&amp;#39;t separate problems anymore — they&amp;#39;re the same problem.&lt;/p&gt;
&lt;p&gt;A dollar spent on a developer influencer video feeds all three at the same time. &lt;a href=&quot;https://www.linkedin.com/posts/madhavbhandari_were-spending-33m-on-marketing-in-2026-activity-7430219623037407232-AwZY&quot;&gt;Here&amp;#39;s a great LinkedIn thread on that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This was one of the unexpected upsides I couldn&amp;#39;t have predicted when we removed cookies. In the original post I talked about how going cookieless forced us into self-reported attribution (asking signups how they heard about us) and holistic measurement (looking at blended data rather than pixel-level tracking). That mindset shift is exactly what made it possible to invest confidently in awareness, because these channels don&amp;#39;t show up cleanly in any tracking dashboard. If we&amp;#39;d kept our old attribution stack, I&amp;#39;m honestly not sure we would have had the conviction to make some of these bigger bets.&lt;/p&gt;
&lt;h2&gt;The trap of only doing what&amp;#39;s measurable&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s where I&amp;#39;ll get a little inside-baseball on how companies think about spending money to grow, because I think it&amp;#39;s useful context for anyone building a product, even a side project.&lt;/p&gt;
&lt;p&gt;Research from the &lt;a href=&quot;https://business.linkedin.com/marketing-solutions/content-marketing/b2b-trends/the-war-on-brand&quot;&gt;IPA&lt;/a&gt; (popularized by LinkedIn&amp;#39;s B2B Institute) suggests the optimal split between brand building and direct-response is around 60/40 in favor of brand. But most companies do the opposite; &lt;a href=&quot;https://dreamdata.io/blog/proven-b2b-growth-strategies-from-the-brand-demand-expand-framework&quot;&gt;Refine Labs found&lt;/a&gt; that the typical company puts roughly 80% of budget into things with immediate measurable results (paid search, retargeting) and only 20% into things that build long-term awareness. The reason is simple: it&amp;#39;s way easier to justify spending money when you can point to a dashboard that says &amp;quot;we spent X and got Y signups.&amp;quot;&lt;/p&gt;
&lt;p&gt;This leads to a cycle I&amp;#39;ve seen at multiple companies: you invest in SEO, paid search, and retargeting ads. Your dashboards show good results. You max those channels out. Growth flatlines. You restructure, expand topics, double down. It doesn&amp;#39;t move fast enough. Someone panics and calls for a rebrand or website overhaul.&lt;/p&gt;
&lt;p&gt;The harder but more interesting question is: how do you measure someone seeing your product in a YouTube video, or hearing about you on a podcast, or noticing your logo on a billboard and then Googling you three weeks later? A &lt;a href=&quot;https://wynter.com/post/the-state-of-b2b-saas-brand-marketing&quot;&gt;Wynter survey&lt;/a&gt; found that 50% of B2B SaaS companies don&amp;#39;t even try to track brand awareness. Half the industry isn&amp;#39;t measuring it, while the channels that are measurable are getting worse.&lt;/p&gt;
&lt;p&gt;At Sentry we&amp;#39;ve been fortunate to have leadership that sees the value in awareness spend, because internally it&amp;#39;s a hard sell at a lot of companies (you&amp;#39;re essentially asking for budget without a clean ROI story). When we removed third-party advertising cookies from our website (&lt;a href=&quot;https://blog.sentry.io/we-removed-advertising-cookies-heres-what-happened/&quot;&gt;I wrote about that whole journey here&lt;/a&gt;), it stripped away our reliance on tracking data, which was equal parts frustrating as it was eye-opening. It forced us into a few habits that have ended up benefiting growth:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusting instincts.&lt;/strong&gt; We all know intuitively that we&amp;#39;re influenced by what we see on YouTube, what we read on Reddit, what our peers recommend, even a well-placed billboard. But it&amp;#39;s hard to act on that when you can&amp;#39;t prove it in a spreadsheet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Looking at holistic data.&lt;/strong&gt; When we made a big investment in a top 5 tech podcast, we saw our unattributed, direct, and organic signups rise that same quarter. We didn&amp;#39;t need granular tracking to connect those dots; the overall numbers backed it up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not being afraid to test and fail.&lt;/strong&gt; We have failed a handful of times on podcasts, influencers, and channels. But roughly every fail comes with one channel that opens up a lot of growth, a worthy tradeoff if you&amp;#39;re thinking a few years down the road.&lt;/p&gt;
&lt;h2&gt;A framework for how we invested&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s the general approach that&amp;#39;s worked for us at Sentry. It&amp;#39;s helped us stay close and authentic with our audience as we scaled. Think of it as concentric circles:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start with known quantities first.&lt;/strong&gt; Focus on influencers, podcasts, and newsletters that solely speak to the people you&amp;#39;re building for and nobody else. For us it&amp;#39;s developers, so we focus on highly technical content that no marketing team or salesperson would be interested in. Newsletters like TLDR Web Dev or Bytes, podcasts like Syntax.fm.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Then as you saturate those, go to the broader category.&lt;/strong&gt; For us it&amp;#39;s tech: anyone interested in building applications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Finally, figure out what your audience cares about beyond your category.&lt;/strong&gt; We asked Reddit to map which other subreddits our target audience frequents. They came back with finance, tech, comedy, and sports (specifically basketball and F1). That insight led us to partner with the Warriors, Valkyries, and to sponsor podcasts like WTF1 and The Race. Seeing your brand sponsor the things your audience loves creates a deeper connection. Do more people than developers watch basketball? Sure. But the diehard devs in the Bay Area and beyond will hopefully see this and associate with Sentry on a deeper level because of it.&lt;/p&gt;
&lt;p&gt;The key is to do this in waves and stages. If you do it all at once, you won&amp;#39;t know what worked.&lt;/p&gt;
&lt;h2&gt;Knowing if it&amp;#39;s working&lt;/h2&gt;
&lt;p&gt;None of this works if you&amp;#39;re not paying attention to timing: when a campaign goes live, how long it should take to see a response, and which of your signup channels should be moving.&lt;/p&gt;
&lt;p&gt;I like to take a weekly time series of our signups by channel and just annotate which weeks we do major things. It&amp;#39;s not perfect attribution. But it&amp;#39;s directional, and directional is enough to keep stacking good decisions while ditching experiments that don&amp;#39;t land. I don&amp;#39;t want to come off as flippant, but sometimes I think we overcomplicate this. Tracking awareness can be simpler than people expect.&lt;/p&gt;
&lt;p&gt;Back in the &lt;a href=&quot;https://blog.sentry.io/we-removed-advertising-cookies-heres-what-happened/&quot;&gt;cookies post&lt;/a&gt;, I mentioned that we salvaged about 50% of our attribution data after going cookieless and instituted a self-reported &amp;quot;how did you hear about us&amp;quot; survey. Two years in, that survey has become one of our most valuable data sources. It&amp;#39;s how we learned that YouTube was driving more awareness than we thought, that certain podcast sponsorships were landing, and that developer word-of-mouth was far more influential than any display ad campaign we ever ran. The irony of losing our tracking pixels and gaining better insight into what actually works hasn&amp;#39;t worn off.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;These channels unlocked new levels of growth I didn&amp;#39;t fully see coming. I&amp;#39;m not saying this is the right approach for every company or that you should ditch measurement entirely. But if the predictable channels are plateauing, it&amp;#39;s worth experimenting with a phased approach: one investment at a time.&lt;/p&gt;
&lt;p&gt;Two years ago we removed all advertising cookies and told ourselves we&amp;#39;d see what happens. What happened is that we stopped optimizing for what was easy to measure and started investing in what we actually believed would work. That led to us understanding our customers better, and that ultimately has led to so many learnings and growth booms along the way.&lt;/p&gt;
</content:encoded></item><item><title>When agents orchestrate agents, who&apos;s watching?</title><link>https://blog.sentry.io/scaling-observability-for-multi-agent-ai-systems/</link><guid isPermaLink="true">https://blog.sentry.io/scaling-observability-for-multi-agent-ai-systems/</guid><description>Multi-agent AI systems fail silently. Learn what proper observability looks like when agents orchestrate agents, and how Sentry keeps you in control.</description><pubDate>Thu, 23 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You used to monitor services.&lt;/p&gt;
&lt;p&gt;Then you started monitoring AI calls inside services.&lt;/p&gt;
&lt;p&gt;Now your AI agent is spinning up &lt;em&gt;other&lt;/em&gt; AI agents to complete tasks. Your old monitoring instincts need to evolve.&lt;/p&gt;
&lt;p&gt;This isn&amp;#39;t hypothetical. Agentic architectures are already in production. Coding agents are calling search agents; orchestrators are spawning specialized sub-agents for retrieval, planning, and execution. Teams are shipping these systems faster than they&amp;#39;re figuring out how to watch them.&lt;/p&gt;
&lt;p&gt;The problem isn&amp;#39;t that agents fail. It&amp;#39;s that when they do, you often can&amp;#39;t tell which agent introduced the failure, or whether anything technically failed at all.&lt;/p&gt;
&lt;h2&gt;Traditional tracing wasn&amp;#39;t built for this&lt;/h2&gt;
&lt;p&gt;In a traditional stack, debugging a request means following one thread from entry point to database. One service, one owner, one place to look.&lt;/p&gt;
&lt;p&gt;In a multi-agent system, a single user action might trigger a planner agent, three tool-call agents, a validation agent, and a write agent. That&amp;#39;s five actors, potentially across different models, different prompts, and very different latency budgets. Errors don&amp;#39;t always surface as exceptions. A bad output from a sub-agent might not throw an error at all. It might just start the spiral, propagating as &lt;em&gt;context corruption&lt;/em&gt; further down the chain. The orchestrator thinks it succeeded. The user sees something wrong. You open your logs and find nothing obviously broken.&lt;/p&gt;
&lt;p&gt;If you want to see what this looks like in practice, &lt;a href=&quot;https://blog.sentry.io/debugging-multi-agent-ai-when-the-failure-is-in-the-space-between-agents/&quot;&gt;this breakdown of a real multi-agent debugging session&lt;/a&gt; shows exactly how a silent tool failure two hops upstream can corrupt final output without triggering a single error. It&amp;#39;s a good illustration of why the instinct to &amp;quot;read the logs&amp;quot; stops working at this level of complexity. In this world, little missteps compound and avalanche.&lt;/p&gt;
&lt;p&gt;This post focuses on what that complexity looks like when you&amp;#39;re operating at scale, across teams, with enterprise reliability expectations.&lt;/p&gt;
&lt;h2&gt;The visibility problem compounds with scale&lt;/h2&gt;
&lt;p&gt;One agent is readable. Two agents are manageable. Five agents calling each other conditionally, with branching logic and shared context? It&amp;#39;s a different category of problem entirely.&lt;/p&gt;
&lt;p&gt;You&amp;#39;re no longer debugging code execution. You&amp;#39;re debugging emergent behavior across a distributed decision graph. The same way microservices made &amp;quot;it&amp;#39;s slow somewhere in the stack&amp;quot; a meaningless statement without traces, multi-agent systems make &amp;quot;the AI did something wrong&amp;quot; nearly impossible to act on without the right instrumentation.&lt;/p&gt;
&lt;p&gt;Most teams discover this the hard way. Maybe that&amp;#39;s a sudden uptick in user churn with no clear cause, or an LLM silently returning bad data three hops down the chain. A token cost bill that tripled overnight. No alert fired because no single component technically crossed a threshold.&lt;/p&gt;
&lt;p&gt;Distributed tracing solved this exact problem for microservices. The question is whether your AI pipeline is instrumented to handle the next version of that problem.&lt;/p&gt;
&lt;h2&gt;What actual, useful multi-agent monitoring looks like&lt;/h2&gt;
&lt;p&gt;Getting visibility into multi-agent systems isn&amp;#39;t a new product category. It&amp;#39;s about applying the right primitives with the right granularity. &lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;Sentry&amp;#39;s AI observability&lt;/a&gt; tooling is built on the same foundation as its distributed tracing, which means the mental model transfers even as the complexity scales. Here&amp;#39;s what that actually requires:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trace continuity across agent handoffs.&lt;/strong&gt; The trace ID needs to follow the task through every agent invocation, not restart at each boundary. You need to see the full tree: who called what, in what order, with what inputs and outputs. A flat list of spans with the same parent doesn&amp;#39;t offer the same value when you need to understand which agent in the middle of the chain introduced a bad state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-agent span attribution.&lt;/strong&gt; Latency, token usage, model version, prompt hash, and output signal should be attributable to each agent individually, not rolled up to the top-level call. Knowing your orchestrator took 4.2 seconds tells you almost nothing. Knowing it was waiting 3.8 seconds on a retrieval sub-agent that returned low-confidence results tells you exactly where to go. This level of attribution is possible by attaching metadata such as model version, token counts, and prompt identifiers to each span during instrumentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failure mode differentiation.&lt;/strong&gt; Agent timeout, bad tool call output, context window overflow, model refusal, and hallucination downstream of a technically valid response are completely different problems with completely different fixes. Grouping them all as &amp;quot;AI errors&amp;quot; is the equivalent of logging every 500 as &amp;quot;server error.&amp;quot; Technically accurate, operationally useless.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost and token attribution at the task level.&lt;/strong&gt; A task that spawned six agents and consumed 40K tokens is a different animal than one that consumed 4K. You need this at query time, broken down per transaction, per user, and per feature. Not buried in an end-of-month billing aggregate. Just tag spans with cost and usage metadata at each agent boundary during instrumentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nested span trees showing agent relationships.&lt;/strong&gt; Sentry&amp;#39;s trace view shows agent invocations as nested spans, so you can see which agent called which, in what order, and what each one consumed. When multiple agents are calling a shared tool or a downstream agent is being invoked by more than one parent, that structure is visible in the trace.&lt;/p&gt;
&lt;h2&gt;Where Sentry fits&lt;/h2&gt;
&lt;p&gt;Sentry already has the primitives: distributed tracing, spans, breadcrumbs, performance metrics. If you&amp;#39;re using the Sentry SDK in your AI pipeline, you&amp;#39;re closer than you think.&lt;/p&gt;
&lt;p&gt;For supported frameworks, setup is minimal. Sentry auto-instruments agent invocations, tool calls, and LLM requests across the major AI frameworks in both Python and Node.js, including OpenAI, Anthropic, Google GenAI, LangChain, LangGraph, Pydantic AI, OpenAI Agents SDK, and Vercel AI SDK. Install the SDK and enable tracing to capture baseline visibility. Sentry can group similar failures across runs based on error patterns and metadata. For multi-agent systems, you&amp;#39;ll typically extend this with custom spans and tags to reflect your agent architecture.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what that looks like for the OpenAI Agents SDK in Python:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;
from sentry_sdk.integrations.openai_agents import OpenAIAgentsIntegration

sentry_sdk.init(
    dsn=&amp;quot;YOUR_DSN&amp;quot;,
    traces_sample_rate=1.0,
    integrations=[OpenAIAgentsIntegration()],
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your framework isn&amp;#39;t on the supported list, manual instrumentation takes about 10 lines of code per span type using Sentry&amp;#39;s &lt;code&gt;gen_ai.*&lt;/code&gt; span conventions such as &lt;code&gt;gen_ai.invoke_agent&lt;/code&gt;, &lt;code&gt;gen_ai.execute_tool&lt;/code&gt;, and &lt;code&gt;gen_ai.request&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One thing worth knowing before you set this up: all AI integrations capture prompt and response content by default, since &lt;code&gt;recordInputs&lt;/code&gt; and &lt;code&gt;recordOutputs&lt;/code&gt; both default to &lt;code&gt;true&lt;/code&gt;. If your prompts or responses contain sensitive data, set both to &lt;code&gt;false&lt;/code&gt;. Make sure your privacy policy permits capturing this content before going to production with the defaults enabled.&lt;/p&gt;
&lt;p&gt;Either way, you end up with a trace tree showing nested agent invocations, tool executions, and LLM calls as child spans. That gives you visibility into execution and performance. Understanding output correctness and decision quality still requires additional validation layers on top.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Seer can help reduce time-to-triage.&lt;/strong&gt; When a multi-agent task fails and you have a trace spanning five agents, &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;Seer&lt;/a&gt; can analyze the error context, surface the most likely source of degradation, and give you a starting point grounded in your actual production data rather than five equally plausible places to begin.&lt;/p&gt;
&lt;p&gt;For a full setup guide across supported frameworks, the &lt;a href=&quot;https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/&quot;&gt;AI agent observability guide&lt;/a&gt; covers instrumentation in detail. Start there if you&amp;#39;re setting this up for the first time.&lt;/p&gt;
&lt;p&gt;Practical starting point: instrument your orchestrator first. Get the top-level task as a transaction, with each agent call as a child span. Even partial visibility is better than none when you&amp;#39;re trying to triage a production degradation at 2am.&lt;/p&gt;
&lt;h2&gt;This is the readiness question&lt;/h2&gt;
&lt;p&gt;Teams adopting agentic systems are going to face the same question their SRE teams faced when they migrated from monolith to microservices: how do we know this is working?&lt;/p&gt;
&lt;p&gt;It&amp;#39;s not a question that stays abstract for long. The first time an agent-orchestrated workflow produces a wrong answer at scale, or quietly runs up a token bill nobody can attribute, or degrades in a way that no individual span flagged, that&amp;#39;s when the question becomes urgent. By then, the teams that already instrumented are triaging. The teams that didn&amp;#39;t are left guessing.&lt;/p&gt;
&lt;p&gt;The teams that answer that question first with real traces, real attribution, and real alerting are the ones that get to keep running agents in production. The others roll it back after the first incident they can&amp;#39;t explain.&lt;/p&gt;
&lt;p&gt;Multi-agent observability isn&amp;#39;t a nice-to-have at scale. It&amp;#39;s table stakes for anyone taking agents beyond the prototype phase. The complexity doesn&amp;#39;t ask permission before it shows up in production. It&amp;#39;s already there.&lt;/p&gt;
</content:encoded></item><item><title>No more monkey-patching: Better observability with tracing channels</title><link>https://blog.sentry.io/observability-with-tracing-channels/</link><guid isPermaLink="true">https://blog.sentry.io/observability-with-tracing-channels/</guid><description>Find out how Node.js Tracing Channels enable libraries to emit their own telemetry, replacing monkey-patching and fixing ESM observability.</description><pubDate>Tue, 21 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Almost every production application uses a number of different tools and libraries, whether that&amp;#39;s a library to communicate with a database, a cache, or frameworks like Nest.js or Nitro. To be able to observe what&amp;#39;s going on in production, application developers reach out for &lt;a href=&quot;https://sentry.io/solutions/application-performance-monitoring/&quot;&gt;Application Performance Monitoring&lt;/a&gt; (APM) tools like Sentry.&lt;/p&gt;
&lt;p&gt;But there&amp;#39;s an inherent problem: the performance data that APM tools need is most often not coming natively from the libraries themselves. The task of getting this data is delegated to APM tools like Sentry or &lt;a href=&quot;https://blog.sentry.io/send-your-existing-opentelemetry-traces/&quot;&gt;OpenTelemetry&lt;/a&gt;, which instrument crucial functionality of a library on their behalf.&lt;/p&gt;
&lt;h2&gt;What is instrumentation?&lt;/h2&gt;
&lt;p&gt;The most fundamental requirement to make an application observable is the ability to instrument each of its components and the libraries it uses. &lt;strong&gt;Instrumentation&lt;/strong&gt; is the process of adding code to a program to monitor and analyze its internal operations and generate diagnostic data. It&amp;#39;s exactly what the Sentry SDKs and OpenTelemetry instrumentation are doing under the hood.&lt;/p&gt;
&lt;p&gt;Consider a typical HTTP client library. Application developers want to know when a request starts and completes, along with some metadata like URL, status code and headers. Today, libraries handle this inconsistently: some provide custom hooks like &lt;code&gt;emitter.on(&amp;#39;request&amp;#39;, ...)&lt;/code&gt;, while others offer vendor-specific middleware to intercept requests. In these cases, Sentry and OpenTelemetry can write plug-ins that emit observability data.&lt;/p&gt;
&lt;p&gt;This works, but it puts the burden on the library or framework (e.g. Nuxt) to consciously design an instrumentation API and identify the right places to expose it. Hooks and interceptors allow injecting observability code at the correct spots, but APM maintainers are entirely dependent on library authors to keep those APIs stable over time. On top of that, there is no shared convention (each library exposes different hook shapes and different metadata) so APM maintainers must write and maintain very different plugins for each library.&lt;/p&gt;
&lt;h2&gt;How server-side JavaScript is instrumented&lt;/h2&gt;
&lt;p&gt;The traditional approach to JavaScript instrumentation is &amp;quot;monkey-patching&amp;quot;. That&amp;#39;s modifying library code at runtime so that library functions not only do their original job, but also emit observability data. This is only possible in CommonJS (CJS), where modules are mutable and synchronously loaded.&lt;/p&gt;
&lt;p&gt;However, the ecosystem is shifting. As server-side JavaScript moves further toward ES Modules (ESM), this approach breaks down. ES modules are immutable and loaded asynchronously, which means you simply can&amp;#39;t patch imports at runtime the same way anymore. For further information: the &lt;a href=&quot;https://github.com/getsentry/esm-observability-guide&quot;&gt;ESM Observability Instrumentation Guide&lt;/a&gt; covers this topic in greater detail.&lt;/p&gt;
&lt;p&gt;The current workaround (and a way to &amp;quot;patch&amp;quot; imports) is using Module Customization Hooks paired with the &lt;code&gt;--import&lt;/code&gt; flag. A popular hook is &lt;code&gt;import-in-the-middle/hook.mjs&lt;/code&gt;. It works, but it&amp;#39;s brittle, complex, and feels like what it is: a workaround.&lt;/p&gt;
&lt;p&gt;Both monkey-patching in CJS and Module Customization Hooks in ESM share the same fundamental flaw: they apply instrumentation &amp;quot;from the outside&amp;quot;. The library itself is passive. The question worth asking is: &lt;strong&gt;what if libraries were active participants in &lt;a href=&quot;https://sentry.io/solutions/application-observability/&quot;&gt;their own observability&lt;/a&gt; and emit telemetry data themselves?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This would be possible through diagnostics APIs like Tracing Channels.&lt;/p&gt;
&lt;h2&gt;Libraries should emit their own telemetry&lt;/h2&gt;
&lt;p&gt;Rather than waiting for APM tools to reach in and grab data, libraries can proactively expose their internal operations using tools built directly into the runtime. The right tool for this is &lt;strong&gt;Diagnostics Channels&lt;/strong&gt;, and more specifically, &lt;strong&gt;Tracing Channels&lt;/strong&gt;. Those features are being developed by the &lt;a href=&quot;https://github.com/nodejs/diagnostics&quot;&gt;Node.js Diagnostics Working Group&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A huge shoutout to &lt;a href=&quot;https://github.com/qard&quot;&gt;Stephen Belanger&lt;/a&gt;, the creator of the &lt;code&gt;diagnostics_channel&lt;/code&gt; API in Node.js, who founded the working group and has been instrumental in pushing this topic forward. He&amp;#39;s been providing feedback on proposals and acting as a voice of authority, which is sometimes exactly what&amp;#39;s needed to convince library maintainers to get on board.&lt;/p&gt;
&lt;h3&gt;Diagnostics Channels&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://nodejs.org/api/diagnostics_channel.html&quot;&gt;Diagnostics Channels&lt;/a&gt; are a high-performance, synchronous event system built directly into Node.js. They&amp;#39;re also supported in Bun, Deno, and Cloudflare Workers (via the Node.js compatibility flag), making them a cross-runtime primitive.&lt;/p&gt;
&lt;p&gt;Their primary use case is one-off events. For example, &amp;quot;a connection was opened&amp;quot; (like &lt;code&gt;node-redis&lt;/code&gt; &lt;a href=&quot;https://github.com/redis/node-redis/blob/41c908e6d65419fed6d985a9664427df1f48fb98/docs/diagnostics-channel.md?plain=1#L45-L48&quot;&gt;does this here&lt;/a&gt;). The limitation is that they don&amp;#39;t inherently represent a full lifecycle. You have to manually link &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;stop&lt;/code&gt; events to measure duration.&lt;/p&gt;
&lt;h3&gt;Tracing Channels&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel&quot;&gt;Tracing Channels&lt;/a&gt; solve exactly that limitation. A Tracing Channel is a bundle of related Diagnostics Channels that automatically creates sub-channels for a complete operation lifecycle: &lt;code&gt;start&lt;/code&gt;, &lt;code&gt;end&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;asyncStart&lt;/code&gt;. More importantly, a &lt;code&gt;TracingChannel&lt;/code&gt; automatically propagates context across async boundaries. This means APM tools can correlate a database query back to the incoming HTTP request that caused it, without any manual bookkeeping.&lt;/p&gt;
&lt;p&gt;Together, they give library and framework authors a standardized way to expose internal operations without coupling to any specific logging or tracing vendor. The library emits structured events and observability tools decide what to do with them.&lt;/p&gt;
&lt;h2&gt;How libraries can implement Tracing Channels&lt;/h2&gt;
&lt;p&gt;Tracing Channels have essentially zero cost when unused. If no subscriber is listening, emitting data costs almost nothing. It means library authors can add tracing channels without worrying about penalizing users who don&amp;#39;t need observability. The benefits are that there is no monkey-patching needed anymore and it eliminates the need for users to pass &lt;code&gt;--import&lt;/code&gt; flags for preloading in ESM.&lt;/p&gt;
&lt;h3&gt;Naming and consistency: The channel is the contract&lt;/h3&gt;
&lt;p&gt;Tracing Channels should always be scoped to the library that emits them, using the npm package name as the namespace. Since package names are globally unique, this keeps channel names collision-free. For example, &lt;code&gt;mysql2&lt;/code&gt; ships &lt;code&gt;mysql2:query&lt;/code&gt; which would emit &lt;code&gt;tracing:mysql2:query:start&lt;/code&gt; and all other channels. And the &lt;code&gt;unstorage&lt;/code&gt; library ships &lt;code&gt;unstorage.get&lt;/code&gt; which emits &lt;code&gt;tracing:unstorage.get:start&lt;/code&gt; and so on. The &lt;a href=&quot;https://github.com/unjs/untracing&quot;&gt;&lt;code&gt;untracing&lt;/code&gt;&lt;/a&gt; package is working to establish broader naming standards across the ecosystem.&lt;/p&gt;
&lt;p&gt;Equally important: Always emit a consistent data structure. Sentry and other APM tools can only provide automatic instrumentation if they know what shape your payload will have.&lt;/p&gt;
&lt;p&gt;The pattern itself is straightforward. The library wraps its operation in a &lt;code&gt;tracePromise&lt;/code&gt; call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Library side (e.g. inside ioredis)


const commandChannel = dc.tracingChannel(&amp;quot;ioredis:command&amp;quot;);

// In the command execution path:
commandChannel.tracePromise(
  async () =&amp;gt; {
    return await executeCommand(cmd);
  },
  { command: cmd.name, args: cmd.args },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And on the consumer side, an SDK like Sentry subscribes to those events:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Consumer side (e.g. Sentry SDK)


dc.tracingChannel(&amp;quot;ioredis:command&amp;quot;).subscribe({
  start(payload) {
    // create span
  },
  asyncEnd(payload) {
    // finish span
  },
  error({ error }) {
    // record error
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The library and the observability tool never need to know about each other. The channel is the contract.&lt;/p&gt;
&lt;h2&gt;The ecosystem is already moving&lt;/h2&gt;
&lt;p&gt;In early February 2026, we (&lt;a href=&quot;https://github.com/andreiborza&quot;&gt;Andrei&lt;/a&gt;, &lt;a href=&quot;https://github.com/JPeer264&quot;&gt;Jan&lt;/a&gt; and &lt;a href=&quot;https://github.com/s1gr1d&quot;&gt;Sigrid&lt;/a&gt;) from Sentry attended &lt;a href=&quot;https://opentelemetry.io/blog/2025/otel-unplugged-fosdem/&quot;&gt;OTel Unplugged EU&lt;/a&gt; and brought up the topic &amp;quot;Prepare for better JS ESM Support&amp;quot;, which was voted on the list of top priorities for the OpenTelemetry ecosystem.&lt;/p&gt;
&lt;p&gt;So this isn&amp;#39;t a theoretical proposal. A growing number of well-known libraries have already shipped or merged PRs for Diagnostics Channel and Tracing Channel support.&lt;/p&gt;
&lt;p&gt;On the framework and HTTP side, &lt;code&gt;undici&lt;/code&gt; (Node.js&amp;#39;s built-in HTTP client) has &lt;a href=&quot;https://undici-docs.vramana.dev/docs/api/DiagnosticsChannel&quot;&gt;shipped Diagnostics Channels&lt;/a&gt; since Node 20.12, and also &lt;code&gt;fastify&lt;/code&gt; (&lt;a href=&quot;https://fastify.dev/docs/latest/Reference/Hooks/#diagnostics-channel-hooks&quot;&gt;docs&lt;/a&gt;), &lt;code&gt;nitro&lt;/code&gt; (&lt;a href=&quot;https://github.com/nitrojs/nitro/pull/4001&quot;&gt;PR&lt;/a&gt;) and &lt;code&gt;h3&lt;/code&gt; (&lt;a href=&quot;https://github.com/h3js/h3/pull/1251&quot;&gt;PR&lt;/a&gt;) have native support. On the database side, &lt;code&gt;unstorage&lt;/code&gt; (&lt;a href=&quot;https://github.com/unjs/unstorage/pull/707&quot;&gt;PR&lt;/a&gt;) and &lt;code&gt;mysql2&lt;/code&gt; (&lt;a href=&quot;https://sidorares.github.io/node-mysql2/docs/documentation/tracing-channels&quot;&gt;Docs&lt;/a&gt;) already use Tracing Channels, and &lt;code&gt;pg&lt;/code&gt; / &lt;code&gt;pg-pool&lt;/code&gt; are actively working on it. Redis clients aren&amp;#39;t far behind either and already support Tracing Channels in &lt;code&gt;ioredis&lt;/code&gt; (&lt;a href=&quot;https://github.com/redis/ioredis/pull/2089&quot;&gt;PR&lt;/a&gt;) and &lt;code&gt;node-redis&lt;/code&gt; (&lt;a href=&quot;https://github.com/redis/node-redis/pull/3195&quot;&gt;PR&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;None of this happens without the people willing to do the work. A massive shoutout to Sentry engineer &lt;strong&gt;Abdelrahman Awad&lt;/strong&gt; (&lt;a href=&quot;https://github.com/logaretm&quot;&gt;@logaretm&lt;/a&gt;) for driving Tracing Channel implementations across multiple libraries. And a special thanks to &lt;strong&gt;Pooya Parsa&lt;/strong&gt; (&lt;a href=&quot;https://github.com/pi0&quot;&gt;@pi0&lt;/a&gt;), his openness to collaborate in &lt;code&gt;h3&lt;/code&gt; and &lt;code&gt;nitro&lt;/code&gt; was instrumental in formalizing this approach and showing the ecosystem what it could look like.&lt;/p&gt;
&lt;h2&gt;The vision ahead&lt;/h2&gt;
&lt;p&gt;We&amp;#39;re still in a &amp;quot;chicken and egg&amp;quot; phase. Libraries need to add channels before APM tools have strong reasons to listen to them, and APM tools need to start listening before authors feel the pressure to add them.&lt;/p&gt;
&lt;p&gt;The goal is &lt;strong&gt;universal JS observability&lt;/strong&gt;: a world where Node.js, Bun, and Deno share the same diagnostic patterns, and instrumentation just works without monkey-patching in CJS, without &lt;code&gt;--import&lt;/code&gt; flags in ESM, and without fragile workarounds. Libraries become active drivers of observability ensuring they are emitting data they think is the most relevant to their users.&lt;/p&gt;
</content:encoded></item><item><title>Debugging multi-agent AI: When the failure is in the space between agents</title><link>https://blog.sentry.io/debugging-multi-agent-ai-when-the-failure-is-in-the-space-between-agents/</link><guid isPermaLink="true">https://blog.sentry.io/debugging-multi-agent-ai-when-the-failure-is-in-the-space-between-agents/</guid><description>Multi-agent observability is exponentially harder than single-agent monitoring. Here&apos;s how to trace, debug, and monitor AI systems where agents orchestrate other agents.</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve been building a multi-agent research system. The idea is simple: give it a controversial technical topic like &amp;quot;Should we rewrite our Python backend in Rust?&amp;quot;, and three agents work on it. An Advocate argues for it, a Skeptic argues against, and a Synthesizer reads both briefs blind and produces a balanced analysis. Each agent has its own model, its own tools, its own system prompt.&lt;/p&gt;
&lt;p&gt;It worked great in testing. Then I noticed the Synthesizer kept producing analyses that leaned heavily toward one side. Not wrong, but noticeably lopsided. I mean, rewriting the Sentry monorepo in Rust is arguably a bad idea, but it was arguing against on things where I clearly knew it should be for it.&lt;/p&gt;
&lt;p&gt;I eventually traced it to the Skeptic&amp;#39;s &lt;code&gt;web_search&lt;/code&gt; tool. The Advocate was returning 3-4 solid data points per query. The Skeptic, however, was searching for different terms that didn&amp;#39;t match the data as well, and was getting back a single generic result. So the Advocate&amp;#39;s brief was well-sourced with citations, and the Skeptic&amp;#39;s brief was... vibes. The Synthesizer did what any reasonable reader would do: it weighted the better-sourced argument more heavily.&lt;/p&gt;
&lt;p&gt;The bug was in a tool call, inside one agent, that silently degraded the input to a completely different agent two steps later. I only found it by clicking through the trace and reading tool outputs at each step.&lt;/p&gt;
&lt;h2&gt;What is multi-agent observability?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;Multi-agent observability&lt;/a&gt; is visibility into how multiple AI agents coordinate, hand off work, and influence each other&amp;#39;s decisions.&lt;/p&gt;
&lt;p&gt;You probably already know single-agent observability: one reasoning chain, some tool calls, a response. The multi-agent version tracks a &lt;em&gt;graph&lt;/em&gt; of interconnected reasoning chains where the output of one agent becomes the input of another. A failure anywhere in the graph can silently corrupt everything downstream.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re running a single agent with a few tools, &lt;a href=&quot;https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/&quot;&gt;standard agent observability&lt;/a&gt; has you covered. But the moment you have agents calling other agents, delegating subtasks, or running in parallel with results merged later, you need a different level of visibility.&lt;/p&gt;
&lt;h2&gt;Why single-agent monitoring doesn&amp;#39;t cut it here&lt;/h2&gt;
&lt;p&gt;Your existing agent monitoring tells you that &lt;code&gt;Skeptic&lt;/code&gt; ran in 3.1 seconds and consumed 2,400 tokens. It does not tell you that &lt;code&gt;Skeptic&lt;/code&gt;&amp;#39;s &lt;code&gt;web_search&lt;/code&gt; returned weak results, that the brief it produced was thin compared to the &lt;code&gt;Advocate&lt;/code&gt;&amp;#39;s, and that the &lt;code&gt;Synthesizer&lt;/code&gt; produced a biased analysis because one of its inputs was poor.&lt;/p&gt;
&lt;p&gt;There are three specific reasons this falls apart.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blame is distributed.&lt;/strong&gt; When the final output is wrong, you can&amp;#39;t point at one agent. The Advocate built a reasonable argument from what its tools gave it. The Synthesizer did a reasonable synthesis of what it received. The bug is in the interaction between them, and no single agent&amp;#39;s logs will show it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The worst failures look fine.&lt;/strong&gt; In traditional software, things throw errors. In multi-agent AI, an agent returns a plausible-but-thin result, the next agent incorporates it without question, and by the time the final output arrives, weak data has been confidently summarized through multiple layers. You&amp;#39;d never know unless you compared the raw inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can&amp;#39;t test every path.&lt;/strong&gt; A single agent with 5 tools has 5 possible actions per step. Three agents with 5 tools each, running in parallel and merging results? The number of possible execution paths is absurd. You need to observe what actually happens in production because you can&amp;#39;t pre-test every combination.&lt;/p&gt;
&lt;h2&gt;Most &amp;quot;multi-agent&amp;quot; examples are actually single-agent&lt;/h2&gt;
&lt;p&gt;Before going further, I want to be honest. I built a multi-agent startup idea validator as my first attempt at this playground, and then realized... it was fake multi-agent. A &amp;quot;Market Analyst&amp;quot; handing off to a &amp;quot;Technical Advisor&amp;quot; handing off to a &amp;quot;Devil&amp;#39;s Advocate&amp;quot; is just one agent with different tools. A single agent with all the tools and a comprehensive system prompt produces the same output with less latency and less cost.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ai-agents/single-agent-multiple-agents&quot;&gt;Microsoft&amp;#39;s Cloud Adoption Framework&lt;/a&gt; puts it directly: &amp;quot;Don&amp;#39;t assume role separation requires multiple agents. Distinct roles might suggest multiple agents, but they don&amp;#39;t automatically justify a multi-agent architecture.&amp;quot;&lt;/p&gt;
&lt;p&gt;Multi-agent earns its pain when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Objectives genuinely conflict.&lt;/strong&gt; An agent told to &amp;quot;argue for&amp;quot; and &amp;quot;argue against&amp;quot; in the same prompt produces mediocre output at both. A generator and a critic need to be separate, or the critic pulls its punches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Information must be isolated.&lt;/strong&gt; If Agent A seeing Agent B&amp;#39;s work would bias the result, they can&amp;#39;t share a context window. Advocate/skeptic. Blind peer review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Different models serve different roles.&lt;/strong&gt; Cheap fast model for research, expensive capable model for synthesis. One agent means one model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasks should run in parallel.&lt;/strong&gt; Two independent research tasks running concurrently as separate agents is genuinely faster than one agent doing them sequentially.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security boundaries require separation.&lt;/strong&gt; The agent reading user PII shouldn&amp;#39;t have database write access.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your use case doesn&amp;#39;t hit at least two of these, start with a single agent and save yourself the debugging pain I&amp;#39;m about to describe.&lt;/p&gt;
&lt;h2&gt;Common multi-agent architecture patterns&lt;/h2&gt;
&lt;p&gt;Each pattern produces a different trace shape and breaks in its own way.&lt;/p&gt;
&lt;h3&gt;Orchestrator / Worker&lt;/h3&gt;
&lt;p&gt;One agent routes tasks to specialists. This is the most common pattern in the OpenAI Agents SDK, LangGraph, and custom implementations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST /api/research (http.server)
└── gen_ai.invoke_agent &amp;quot;Research Director&amp;quot;
    ├── gen_ai.request &amp;quot;chat gpt-5.4&amp;quot;                         ← plan subtasks
    ├── gen_ai.execute_tool &amp;quot;delegate_research&amp;quot;
    │   └── gen_ai.invoke_agent &amp;quot;Web Research Agent&amp;quot;
    │       ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;
    │       ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot;
    │       └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;            ← summarize
    ├── gen_ai.execute_tool &amp;quot;delegate_analysis&amp;quot;
    │   └── gen_ai.invoke_agent &amp;quot;Data Analysis Agent&amp;quot;
    │       ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;
    │       ├── gen_ai.execute_tool &amp;quot;query_database&amp;quot;
    │       └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;
    └── gen_ai.request &amp;quot;chat gpt-5.4&amp;quot;                         ← synthesize
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it breaks:&lt;/strong&gt; The orchestrator misclassifies the task and routes to the wrong specialist, who then does perfect work on the wrong problem. Or it passes insufficient context, and the specialist hallucinates what&amp;#39;s missing.&lt;/p&gt;
&lt;h3&gt;Parallel with merge&lt;/h3&gt;
&lt;p&gt;Independent agents work concurrently on the same problem, and a final agent merges results. This is what the balanced research system uses, and it&amp;#39;s the pattern I think has the most interesting debugging challenges.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Advocate workflow .............. 3.2s  (parallel)
├── gen_ai.invoke_agent &amp;quot;Advocate&amp;quot;
│   ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;         ← plan research
│   ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot;           ← find evidence
│   ├── gen_ai.execute_tool &amp;quot;fetch_benchmark&amp;quot;      ← get numbers
│   └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;         ← write brief

Skeptic workflow ............... 2.8s  (parallel)
├── gen_ai.invoke_agent &amp;quot;Skeptic&amp;quot;
│   ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;         ← plan research
│   ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot;           ← find counter-evidence
│   └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;         ← write brief

Synthesizer workflow ........... 4.1s  (sequential, after both)
└── gen_ai.invoke_agent &amp;quot;Synthesizer&amp;quot;
    └── gen_ai.request &amp;quot;chat gpt-5.4&amp;quot;              ← blind analysis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it breaks:&lt;/strong&gt; Uneven tool quality. If one agent&amp;#39;s tool calls return richer data, the merge agent naturally weights that side more heavily. The merge agent has no way to know its inputs were unequal, because it only sees the finished briefs, not the raw tool results underneath. This is the bug I had the pleasure of dealing with while crafting this blog post.&lt;/p&gt;
&lt;h3&gt;Peer handoffs&lt;/h3&gt;
&lt;p&gt;Agents transfer control directly to each other. The OpenAI Agents SDK &lt;code&gt;handoff()&lt;/code&gt; pattern works this way.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST /api/chat (http.server)
└── gen_ai.invoke_agent &amp;quot;Triage Agent&amp;quot;
    ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;
    ├── gen_ai.handoff &amp;quot;from Triage Agent to Billing Agent&amp;quot;
    └── gen_ai.invoke_agent &amp;quot;Billing Agent&amp;quot;
        ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot;
        ├── gen_ai.execute_tool &amp;quot;check_balance&amp;quot;
        ├── gen_ai.handoff &amp;quot;from Billing Agent to Dispute Specialist&amp;quot;
        └── gen_ai.invoke_agent &amp;quot;Dispute Specialist&amp;quot;
            ├── gen_ai.request &amp;quot;chat gpt-5.4&amp;quot;
            └── gen_ai.execute_tool &amp;quot;file_dispute&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How it breaks:&lt;/strong&gt; State management at the handoff. When Agent A transfers to Agent B, what gets passed? Full conversation history? A summary? Just the last message? Pass everything and you blow context windows. Summarize and you lose nuance. Bugs in the handoff protocol are the hardest to find because they look like bugs in the receiving agent.&lt;/p&gt;
&lt;h2&gt;What makes multi-agent debugging different&lt;/h2&gt;
&lt;p&gt;There are a few specific problems you only hit when multiple agents are involved.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blame attribution across boundaries.&lt;/strong&gt; When a multi-agent system returns wrong output, the question is: did the right agent receive the task? Did it get the right context? Did it do bad work with good input, or good work with bad input? Without traces that span the full agent graph, you&amp;#39;re reading each agent&amp;#39;s logs in isolation trying to reconstruct what happened at the boundaries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent cascading failures.&lt;/strong&gt; This is the one that got me. An agent returns a plausible response, the downstream agent accepts it, and the final output is wrong, but every span shows &lt;code&gt;status: ok&lt;/code&gt;. To catch these, you need to be able to compare input and output at each agent boundary and see the full prompt and response at each LLM call. Token counts and latency alone won&amp;#39;t help.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context drift across handoffs.&lt;/strong&gt; Every time an agent summarizes before passing to the next, information is lossy-compressed. After three handoffs, the original user intent can be barely recognizable. In a trace, you can see this by reading the prompts in sequence: the first agent has the full query, the second has a summary, the third has a summary of a summary. The fix is usually architectural (pass structured data instead of natural language), but you have to see the drift before you can fix it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost explosion without attribution.&lt;/strong&gt; In our research system, the Synthesizer uses &lt;code&gt;gpt-5.4&lt;/code&gt; while the researchers use &lt;code&gt;gpt-5.4-mini&lt;/code&gt;. Without per-agent cost tracking, you&amp;#39;d see total spend growing but wouldn&amp;#39;t know the Synthesizer accounts for 60% of the cost despite running only once per query.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;A debugging walkthrough with the balanced research system&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s how I actually found the bug from the opening. The Synthesizer was producing lopsided analyses, and I wanted to figure out why.&lt;/p&gt;
&lt;h3&gt;Comparing the parallel agents&lt;/h3&gt;
&lt;p&gt;First thing I did was look at both research agent workflows side by side in the trace view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Advocate workflow .......................... 3.2s  ✓
├── gen_ai.invoke_agent &amp;quot;Advocate&amp;quot; ......... 3.1s
│   ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot; . 0.6s  ← plan
│   ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot; ... 0.2s  ← &amp;quot;rust performance&amp;quot;
│   ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot; ... 0.1s  ← &amp;quot;rust adoption&amp;quot;
│   ├── gen_ai.execute_tool &amp;quot;fetch_benchmark&amp;quot; 0.1s ← rust benchmarks
│   └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot; . 1.8s  ← write brief

Skeptic workflow ........................... 2.8s  ✓
├── gen_ai.invoke_agent &amp;quot;Skeptic&amp;quot; .......... 2.7s
│   ├── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot; . 0.5s  ← plan
│   ├── gen_ai.execute_tool &amp;quot;web_search&amp;quot; ... 0.1s  ← &amp;quot;python migration costs&amp;quot;
│   └── gen_ai.request &amp;quot;chat gpt-5.4-mini&amp;quot; . 1.9s  ← write brief
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The asymmetry was immediately obvious. The Advocate made 3 tool calls. The Skeptic made 1.&lt;/p&gt;
&lt;h3&gt;Inspecting the tool results&lt;/h3&gt;
&lt;p&gt;Clicking into the Advocate&amp;#39;s &lt;code&gt;web_search&lt;/code&gt; spans, each returned 3-4 data points:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[&amp;quot;Rust programs typically run 2-5x faster than equivalent Python...&amp;quot;,
 &amp;quot;Discord switched from Go to Rust... latency drop from 50ms to 1ms&amp;quot;,
 &amp;quot;Figma rewrote their multiplayer server... memory usage by 10x&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Skeptic&amp;#39;s single &lt;code&gt;web_search&lt;/code&gt; had searched for &amp;quot;python migration costs&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[&amp;quot;No specific data found for &amp;#39;python migration costs&amp;#39;. Consider refining your search terms.&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the Skeptic wrote its brief from general knowledge with no citations, while the Advocate had 10+ data points from 3 searches.&lt;/p&gt;
&lt;h3&gt;Following it to the Synthesizer&lt;/h3&gt;
&lt;p&gt;Clicking the Synthesizer&amp;#39;s &lt;code&gt;gen_ai.request&lt;/code&gt; span and reading the prompt confirmed it. It received one well-sourced brief with citations and benchmark data, and one brief with general arguments and no data. It weighted the better-sourced one more heavily, which is exactly what you&amp;#39;d want a synthesizer to do. The problem was upstream.&lt;/p&gt;
&lt;h3&gt;The fix&lt;/h3&gt;
&lt;p&gt;Two options: improve the Skeptic&amp;#39;s prompt to try multiple search queries when the first returns weak results, or improve the &lt;code&gt;web_search&lt;/code&gt; tool to handle broader query terms. I did both. Watched the traces afterward, and both agents were producing comparably sourced briefs.&lt;/p&gt;
&lt;p&gt;The root cause was a weak tool result for one agent that cascaded through the pipeline as information asymmetry. Without seeing every tool call and every prompt in the trace, I would have blamed the Synthesizer&amp;#39;s prompt for being biased.&lt;/p&gt;
&lt;h2&gt;Auto-instrumenting multi-agent frameworks&lt;/h2&gt;
&lt;p&gt;Sentry auto-instruments the OpenAI Agents SDK, LangGraph, and other frameworks. The integration activates automatically when the package is detected. Here&amp;#39;s the setup for the balanced research system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;

from agents import Agent, Runner, function_tool, ModelSettings

sentry_sdk.init(
    send_default_pii=True,   # captures prompts and responses in spans
    traces_sample_rate=1.0,
    enable_logs=True,
)

@function_tool
def web_search(query: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Search the web for information on a topic.&amp;quot;&amp;quot;&amp;quot;
    ...

advocate = Agent(
    name=&amp;quot;Advocate&amp;quot;,
    model=&amp;quot;gpt-5.4-mini&amp;quot;,
    model_settings=ModelSettings(temperature=0.3),
    instructions=&amp;quot;Build the strongest case FOR the position...&amp;quot;,
    tools=[web_search],
)

skeptic = Agent(
    name=&amp;quot;Skeptic&amp;quot;,
    model=&amp;quot;gpt-5.4-mini&amp;quot;,
    model_settings=ModelSettings(temperature=0.3),
    instructions=&amp;quot;Build the strongest case AGAINST the position...&amp;quot;,
    tools=[web_search],
)

synthesizer = Agent(
    name=&amp;quot;Synthesizer&amp;quot;,
    model=&amp;quot;gpt-5.4&amp;quot;,
    model_settings=ModelSettings(temperature=0.5),
    instructions=&amp;quot;Produce balanced analysis from two research briefs...&amp;quot;,
)

async def analyze(topic: str):
    # Parallel execution: two independent trace trees
    advocate_result, skeptic_result = await asyncio.gather(
        Runner.run(advocate, topic),
        Runner.run(skeptic, topic),
    )

    synthesis_input = f&amp;quot;&amp;quot;&amp;quot;
    Brief A: {advocate_result.final_output}
    Brief B: {skeptic_result.final_output}
    &amp;quot;&amp;quot;&amp;quot;
    return await Runner.run(synthesizer, synthesis_input)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SENTRY_DSN&lt;/code&gt; is read from the environment. &lt;code&gt;send_default_pii=True&lt;/code&gt; is what enables prompt and response capture in spans, which is essential for debugging the handoff problems described above. The SDK creates &lt;code&gt;gen_ai.invoke_agent&lt;/code&gt; spans for each agent, &lt;code&gt;gen_ai.execute_tool&lt;/code&gt; spans for tool calls, and &lt;code&gt;gen_ai.request&lt;/code&gt; spans for LLM calls with token counts and model info.&lt;/p&gt;
&lt;p&gt;For JavaScript/TypeScript with the Vercel AI SDK or LangChain, use &lt;code&gt;tracesSampler&lt;/code&gt; to capture AI routes at 100%:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  sendDefaultPii: true,
  tracesSampler: ({ name, attributes, inheritOrSampleWith }) =&amp;gt; {
    if (attributes?.[&amp;#39;sentry.op&amp;#39;]?.startsWith(&amp;#39;gen_ai.&amp;#39;)) {
      return 1.0;
    }
    if (name?.includes(&amp;#39;/api/chat&amp;#39;) || name?.includes(&amp;#39;/api/agent&amp;#39;)) {
      return 1.0;
    }
    return inheritOrSampleWith(0.2);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more on why you should sample AI traces at 100%, see the companion post on &lt;a href=&quot;https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/&quot;&gt;sampling strategies for agentic applications&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Building multi-agent dashboards&lt;/h2&gt;
&lt;p&gt;Pre-built agent dashboards show per-model and per-tool aggregates. For multi-agent systems, you need to slice by agent. Some dashboards you can build with the &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt; (or follow our &lt;a href=&quot;https://sentry.io/cookbook/create-dashboards-with-ai-agent/&quot;&gt;hands-on dashboards cookbook&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-agent cost attribution:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;Multi-Agent Monitoring&amp;#39; &amp;quot;Cost by Agent&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.total_tokens&amp;quot; &amp;quot;count&amp;quot; \
  --where &amp;quot;span.op:gen_ai.invoke_agent&amp;quot; \
  --group-by &amp;quot;gen_ai.agent.name&amp;quot; \
  --sort &amp;quot;-sum:gen_ai.usage.total_tokens&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is how I found out the Synthesizer was 60% of my cost despite running once per query (because it uses &lt;code&gt;gpt-5.4&lt;/code&gt; instead of &lt;code&gt;gpt-5.4-mini&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool reliability by agent:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;Multi-Agent Monitoring&amp;#39; &amp;quot;Tool Errors by Agent&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;failure_rate&amp;quot; &amp;quot;count&amp;quot; \
  --where &amp;quot;span.op:gen_ai.execute_tool&amp;quot; \
  --group-by &amp;quot;gen_ai.agent.name&amp;quot; &amp;quot;gen_ai.tool.name&amp;quot; \
  --sort &amp;quot;-failure_rate&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the Skeptic&amp;#39;s &lt;code&gt;web_search&lt;/code&gt; returns empty results 15% of the time while the Advocate&amp;#39;s returns empty 3% of the time, you&amp;#39;ve found your lopsided synthesis problem before users report it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent duration comparison:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;Multi-Agent Monitoring&amp;#39; &amp;quot;Agent Duration p95&amp;quot; \
  --display bar --dataset spans \
  --query &amp;quot;p95:span.duration&amp;quot; \
  --where &amp;quot;span.op:gen_ai.invoke_agent&amp;quot; \
  --group-by &amp;quot;gen_ai.agent.name&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Agents doing similar work should take similar time. Big duration gaps between parallel agents usually mean one is making more (or fewer) tool calls than expected.&lt;/p&gt;
&lt;h2&gt;What I&amp;#39;d recommend if you&amp;#39;re building multi-agent systems&lt;/h2&gt;
&lt;p&gt;Based on debugging this system and reading a lot of traces:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Capture prompts and responses at every agent boundary.&lt;/strong&gt; This is the &lt;code&gt;send_default_pii=True&lt;/code&gt; flag. Token counts show cost. But the prompts, responses, and tool input/output data are where you&amp;#39;ll actually find bugs. The handoff boundaries between agents are where most multi-agent issues live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Name your agents clearly.&lt;/strong&gt; &amp;quot;Agent&amp;quot; and &amp;quot;Sub-Agent&amp;quot; in your trace view tells you nothing. &amp;quot;Advocate&amp;quot; and &amp;quot;Skeptic&amp;quot; and &amp;quot;Synthesizer&amp;quot; tells a story you can follow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compare parallel agents.&lt;/strong&gt; When agents run concurrently and their outputs merge, the merge agent can&amp;#39;t tell if its inputs were equally good. But you can tell from the traces. Look for asymmetry in tool call counts, token usage, and duration between agents that should be doing similar work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample at 100%.&lt;/strong&gt; This matters even more for multi-agent than single-agent. A run that fails on a specific combination of tool results might happen 1 in 50 times. At 10% sampling, you&amp;#39;ll need 500 runs before you capture one. See &lt;a href=&quot;https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/&quot;&gt;how to sample AI traces at 100%&lt;/a&gt; for the setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alert on tool failure rates per agent, not globally.&lt;/strong&gt; A tool that fails 5% globally might fail 20% for one specific agent because of how it formulates queries. Global averages hide per-agent problems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connect to your full stack.&lt;/strong&gt; A slow &lt;code&gt;web_search&lt;/code&gt; tool might be caused by rate limiting from an upstream API, not an agent issue. Multi-agent traces that sit inside your &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;existing distributed traces&lt;/a&gt; let you see everything.&lt;/p&gt;
&lt;h2&gt;Getting started&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re already using Sentry for agent monitoring, multi-agent traces work automatically. The SDKs detect agent invocations, handoffs, and tool calls.&lt;/p&gt;
&lt;p&gt;Starting fresh:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pip install sentry-sdk&lt;/code&gt; or &lt;code&gt;npm install @sentry/node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Initialize with &lt;code&gt;traces_sample_rate=1.0&lt;/code&gt; and &lt;code&gt;send_default_pii=True&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run your multi-agent workflow. Spans appear in Sentry&amp;#39;s trace view.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For setup across 10+ frameworks, see the &lt;a href=&quot;https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/&quot;&gt;AI agent observability guide&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Grave improvements: Native crash postmortems via Android tombstones</title><link>https://blog.sentry.io/native-crash-postmortems-via-android-tombstones/</link><guid isPermaLink="true">https://blog.sentry.io/native-crash-postmortems-via-android-tombstones/</guid><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://sentry.io/solutions/mobile-developers/&quot;&gt;Native crashes on Android&lt;/a&gt; have always been harder to debug than they should be.&lt;/p&gt;
&lt;p&gt;The platform has its own crash reporter (&lt;code&gt;debuggerd&lt;/code&gt;) that captures the crashing thread, every other running thread, register state, and memory maps into a file called a tombstone. Tombstones have been a part of Android for a long time; in fact, they&amp;#39;ve been there in one form or another since Android&amp;#39;s first commit.&lt;/p&gt;
&lt;p&gt;The problem: for most of Android&amp;#39;s life, you couldn&amp;#39;t read tombstones programmatically from inside your app. That left SDK-based native crash reporting (like ours) stuck replicating infrastructure the platform already had — at the cost of binary overhead, incomplete Java frame symbolication, and a C++ fork we had to maintain against a moving AOSP target.&lt;/p&gt;
&lt;p&gt;Android 11 (SDK level 30) introduced &lt;a href=&quot;https://developer.android.com/reference/android/app/ApplicationExitInfo&quot;&gt;&lt;code&gt;ApplicationExitInfo&lt;/code&gt;&lt;/a&gt;. Android 12 (SDK level 31) added access to the trace input stream for &lt;a href=&quot;https://developer.android.com/reference/android/app/ApplicationExitInfo#REASON_CRASH_NATIVE&quot;&gt;&lt;code&gt;ApplicationExitInfo.REASON_CRASH_NATIVE&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sentry&amp;#39;s Android SDK, as of version &lt;code&gt;8.30.0&lt;/code&gt;, reads that stream on all devices running Android 12 and above and ships it as a native crash event. This dramatically improves &lt;a href=&quot;https://sentry.io/for/android/&quot;&gt;crash reporting for Android apps&lt;/a&gt; that use native code, whether your team wants just basic crash alerts or deep debugging info.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s dive into how things worked before, what it took to wire it into the SDK without breaking the existing NDK integration, and the improvements these changes bring.&lt;/p&gt;
&lt;h2&gt;Before tombstones: a fork chasing a moving target&lt;/h2&gt;
&lt;p&gt;Before tombstone support, the Native SDK (&lt;code&gt;sentry-native&lt;/code&gt;) was used in the Android SDK as the primary native error reporting source. Since Android is based on Linux, a considerable part of the SDK could be reused. For those parts that couldn&amp;#39;t, work on integrating Android-specific code began in 2019.&lt;/p&gt;
&lt;p&gt;Specifically, the integration of &lt;code&gt;libunwindstack&lt;/code&gt; (the AOSP platform unwinder still used today to produce stack traces for &lt;code&gt;debuggerd&lt;/code&gt; and, in turn, tombstones) was a key moment for supporting native crashes in Sentry&amp;#39;s Android SDK.&lt;/p&gt;
&lt;p&gt;Why, you ask? Because the Native Development Kit (NDK) did not offer a general-purpose stack walker (narrator: it still does not).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;libunwindstack&lt;/code&gt; is not part of the NDK, but part of the &lt;a href=&quot;https://android.googlesource.com/platform/system/unwinding/&quot;&gt;Android Open Source Project (AOSP) platform code&lt;/a&gt; and thus is not directly accessible to app developers via the usual means. Sentry forked a repository that patched the platform code to build with the NDK, and since then, maintained that fork without any changes in the upstream patched version. This provided stack tracing capabilities inside the rather complicated Android Runtime (ART) environment, which has mixed stack-traces between classic native code, native code that is part of the VM execution, and Java/Kotlin frames that also appear as native frames, since they are either interpreted, JITed, or AOTed.&lt;/p&gt;
&lt;p&gt;While this can already be challenging for a normal stack-walker, it is also a problem from the perspective of symbolication: there are more OEM builds than Sentry can realistically collect platform binaries from. So, while a core set of libraries will likely exist in our backend stores, we cannot rely on all of them being available. Thus, symbolication on Android happens on the client-side.&lt;/p&gt;
&lt;p&gt;Considerable restructuring in the platform code, however, made manual upstream alignment very hard over time. In addition to that &lt;code&gt;libunwindstack&lt;/code&gt; is a C++ library, which means, while being light on standard library usage, it still needs to be linked against it statically in order to ensure being isolated at runtime from ABI-incompatible versions of the standard library.&lt;/p&gt;
&lt;p&gt;It also introduced a couple of challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The biggest issue always was size:&lt;/strong&gt; since we must package binaries for &lt;code&gt;x86&lt;/code&gt;, &lt;code&gt;x86_64&lt;/code&gt;, &lt;code&gt;armeabi-v7a&lt;/code&gt;, and &lt;code&gt;arm64-v8a&lt;/code&gt;, we currently add around 1MiB of stripped binary to every app that needs native error reporting or instrumentation. The Sentry SDK code only accounts for 20% of that size; the rest is libunwindstack and the C++ infra it depends on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incomplete implementation:&lt;/strong&gt; since the library size is already significant, certain features have been excluded from the build: there is currently no DEX/OAT symbolication (meaning none of the Java frames are symbolicated), and there is incomplete support for locating DWARF CFI in OAT frames, which often leads to dramatically shortened stack traces in release builds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Limited context:&lt;/strong&gt; Since the &lt;code&gt;inproc&lt;/code&gt; backend, which handles the crashes on Android, doesn&amp;#39;t stop any threads by design, it also only provides the stack trace of the crashed thread, which, in particular on Android, is often way too little context to uncover the root-cause of a crash&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So while all of these are fixable, the effort required is significant and would also lead to a long-term commitment to maintain against the moving target that is AOSP. Introducing tombstone support allows us to fix all the issues mentioned for users who run on Android 12+, which is a significantly growing portion of the incoming events and user base. At the same time, it opens the door to work on better solutions for edge cases.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/native-crash-postmortems-via-android-tombstones/android-version-distribution-chart.png&quot; alt=&quot;Android version distribution chart&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Android 12+ accounts for ~69% of 2B+ Android error events ingested over the past 30 days.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;What tombstones give us&lt;/h2&gt;
&lt;p&gt;The problems outlined above: size, incomplete traces, missing Java symbolication, and maintenance burden, all stem from replicating the platform crash infrastructure that already exists on the device.&lt;/p&gt;
&lt;p&gt;Tombstones fix each of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;All threads, fully symbolicated.&lt;/strong&gt; Where the &lt;code&gt;inproc&lt;/code&gt; backend could only capture the crashing thread, tombstones provide stack traces and register sets for every thread at the moment of the crash. On Android, the crashing thread is often just the victim of a problem that originated in another thread. Seeing all of them is the difference between a solvable crash and an enigma.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Java/Kotlin frames resolved.&lt;/strong&gt; The platform unwinder has full access to ART internals that a forked NDK build cannot have. DEX/OAT symbolication, which we deliberately excluded to limit binary size, comes for free.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No binary overhead for stack traces.&lt;/strong&gt; Tombstones are produced by the platform&amp;#39;s own &lt;code&gt;libunwindstack&lt;/code&gt;, the same library we have been forking and shipping. The ~1MiB binary weight for all supported ABIs drops to zero for apps that rely on tombstones alone.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Maintenance shifted to the platform.&lt;/strong&gt; We consume structured output instead of tracking AOSP restructuring and keeping a C++ fork buildable against the NDK.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Register memory context.&lt;/strong&gt; Memory dumps around pointer values in the crashing thread&amp;#39;s registers show the data being operated on at the point of the crash. (Not yet integrated into the Sentry event payload or UI.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Symbol resolution:&lt;/strong&gt; Since we now have modules and resolved symbols on the client, we can also strip non-actionable trace contents like runtime-internal frames before sending.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to use it&lt;/h2&gt;
&lt;p&gt;Tombstone support is available since version &lt;code&gt;8.30.0&lt;/code&gt; of &lt;code&gt;sentry-android-core&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If your app runs on Android 12+ and you enable tombstones, you will automatically get more complete reports delivered for all native crashes that affect your app. If you used Native SDK/NDK integration, you will automatically get better stack traces for all your threads and still see the context you created on the native side.&lt;/p&gt;
&lt;p&gt;If you have never used the Native SDK interfaces in your native code directly, you can evaluate your options for disabling the NDK integration. If enough users of an app moved on to Android 12+, there is no further use in running both integrations.&lt;/p&gt;
&lt;p&gt;If, however, the Native SDK interface is still in direct use, both integrations work together without any visible degradation in user experience.&lt;/p&gt;
&lt;p&gt;If you want to turn on the feature, you can do so programmatically via &lt;code&gt;SentryAndroidOptions&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;SentryAndroid.init(context) { options -&amp;gt;
    options.isTombstoneEnabled = true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or declaratively in your &lt;code&gt;AndroidManifest.xml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;meta-data android:name=&amp;quot;io.sentry.tombstone.enable&amp;quot; android:value=&amp;quot;true&amp;quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since tombstones capture every thread at the moment of the crash, you can inspect any of them directly in the issue detail view:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/native-crash-postmortems-via-android-tombstones/sentry-android-thread-list.png&quot; alt=&quot;Sentry Android thread list&quot;&gt;&lt;/p&gt;
&lt;p&gt;The &amp;quot;Most Relevant&amp;quot; view strips the trace down to the actionable frames, the ones that drive issue grouping and naming, isolating &lt;code&gt;inApp&lt;/code&gt; JNI frames, but excluding Jetpack Compose layers:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/native-crash-postmortems-via-android-tombstones/sentry-sigsegv-tombstone-crash-detail.png&quot; alt=&quot;Sentry SIGSEGV tombstone crash detail&quot;&gt;&lt;/p&gt;
&lt;p&gt;Expanding the collapsed frames reveals the complete picture: from &lt;code&gt;__libc_init&lt;/code&gt; through process startup, the Android message loop, the native/Java runtime boundary crossings, and up through the view layer to the crash site:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/native-crash-postmortems-via-android-tombstones/android-java-stack-trace.png&quot; alt=&quot;Android Java stack trace&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The implementation challenges&lt;/h2&gt;
&lt;p&gt;Tombstone support touched on many layers of the SDK because native crash reporting intersects with session management, event deduplication, envelope caching, event enrichment, and the existing NDK integration that already handles the same class of crashes through a completely different mechanism.&lt;/p&gt;
&lt;h3&gt;Sharing infrastructure with ANR detection&lt;/h3&gt;
&lt;p&gt;The most immediate architectural challenge was that the SDK already had an integration consuming &lt;code&gt;ApplicationExitInfo&lt;/code&gt;: the &lt;code&gt;ANR&lt;/code&gt; integration, which handles &lt;code&gt;REASON_ANR&lt;/code&gt;. Both integrations need the same lifecycle: query the historical exit list, skip already-reported entries, distinguish the latest (enrichable) entry from older (historical) ones, persist a &amp;quot;last reported&amp;quot; timestamp marker, wait for the previous session to flush, and block until the event is written to disk.&lt;/p&gt;
&lt;p&gt;Duplicating this would have been the faster path, but the implementation instead extracted a generic history dispatcher parameterized by a policy interface. Each integration implements the policy (target reason, historical flag, report builder), while the dispatcher owns traversal, ordering, deduplication, and flush coordination. The envelope cache&amp;#39;s timestamp marker system was similarly generalized so both ANR and tombstone markers are handled polymorphically.&lt;/p&gt;
&lt;p&gt;This refactoring had a cascading consequence for event processing. The existing ANR event processor was tightly coupled to &lt;code&gt;ANR&lt;/code&gt; assumptions and enriched every &amp;quot;backfillable&amp;quot; event (which are events that don&amp;#39;t have access to the live scope of the session they emerge from, but the scope can forensically be reconstructed) as though it were an &lt;code&gt;ANR&lt;/code&gt;. With tombstones now also flowing through as backfillable events, the processor was generalized with an enrichment strategy interface. &lt;code&gt;ANR&lt;/code&gt;-specific logic (exception synthesis from textual thread dumps, background/foreground fingerprinting, profile-based culprit identification) moved into a dedicated enricher. At the same time, the shared path (scope backfilling, options backfilling, device/OS context) became the generic default that tombstones are fully served by without needing their own enricher.&lt;/p&gt;
&lt;p&gt;A considerable part of this new infrastructure can now be reused for other &lt;code&gt;ApplicationExitInfo&lt;/code&gt; categories, likely even when the resulting artifacts won&amp;#39;t be events (but rather entries in SDK client reports).&lt;/p&gt;
&lt;h3&gt;Coexisting with the NDK integration&lt;/h3&gt;
&lt;p&gt;The deeper problem was that tombstones and the existing Sentry NDK integration (using &lt;a href=&quot;https://github.com/getsentry/sentry-native/blob/master/ndk/README.md&quot;&gt;&lt;code&gt;sentry-native-ndk&lt;/code&gt;&lt;/a&gt;) report the same crash. The Native SDK catches the signal at runtime via its own signal handler and writes an envelope to the &amp;quot;outbox&amp;quot;. The tombstone is generated by the platform&amp;#39;s &lt;code&gt;debuggerd&lt;/code&gt;, which is invoked after the Native SDK&amp;#39;s signal handler chains to the previous handler, but the tombstone only arrives through &lt;code&gt;ApplicationExitInfo&lt;/code&gt; on the next launch, after the process has been killed.&lt;/p&gt;
&lt;p&gt;If both integrations are active, every native crash produces a duplicate. We need both to get the full picture: the richer stack traces, thread coverage, and up-to-date memory maps from the tombstone, combined with user-supplied scope data from the Native SDK. So we can&amp;#39;t simply turn one off in favor of the other.&lt;/p&gt;
&lt;p&gt;Solving this required correlating the two events by timestamp (within a 5-second tolerance) and merging them into one. The correlation itself was trivial. The complexity came from the different paths the two events take before they can be merged.&lt;/p&gt;
&lt;p&gt;The Native SDK serializes envelopes to a shared app directory (the &amp;quot;outbox&amp;quot;), which acts as a signal to the Android SDK that an envelope is ready to send. For a native crash, this signal arrives too late to be picked up by the normal outbox sending infrastructure during the crash. So, on the next start, that infrastructure loads every envelope fully into memory, because its sole purpose is to send them to the backend. If we reused it for merge discovery, we would deserialize every queued envelope into memory just to find the one native crash event worth merging. On a device that has been offline and accumulated envelopes, this means a spike in memory pressure and CPU load for what is almost always a single match.&lt;/p&gt;
&lt;p&gt;Instead, a lightweight scan phase streams through each envelope file, parsing only item headers and extracting the platform and timestamp fields via streaming JSON, without deserializing the full event. A bounded input stream tracks position within each envelope item and skips unread bytes to correctly advance to the next item. Full deserialization only happens once a timestamp match is found. The resulting streaming envelope/event parsing infrastructure can likely be reused in other parts of the SDK.&lt;/p&gt;
&lt;p&gt;The merged event carries a &lt;code&gt;TombstoneMerged&lt;/code&gt; exception mechanism (alongside the existing &lt;code&gt;Tombstone&lt;/code&gt; and &lt;code&gt;signalhandler&lt;/code&gt; mechanisms) so the backend, developers, and customers can distinguish provenance.&lt;/p&gt;
&lt;h3&gt;Session and &lt;code&gt;crashedLastRun&lt;/code&gt; lifecycle&lt;/h3&gt;
&lt;p&gt;Native crash reporting interacts with session tracking in ways that require careful coordination. When the tombstone integration processes a crash, it needs to end the previous session as crashed and set &lt;code&gt;crashedLastRun&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. But the NDK integration has its own mechanism for this: a crash marker file checked by the session finalizer on next launch.&lt;/p&gt;
&lt;p&gt;A dedicated marker hint (deliberately distinct from the one used for ANRs) was introduced so that the envelope cache can recognize tombstone events during session persistence: when it sees the hint, it ends the previous session as crashed with the crash timestamp. The session finalizer then detects the already-crashed state and sets &lt;code&gt;crashedLastRun&lt;/code&gt; accordingly, without re-processing the NDK crash marker. Crucially, the native crash marker file is still cleaned up regardless of whether the tombstone integration handled the crash. Otherwise, the NDK path would re-report it on every subsequent launch.&lt;/p&gt;
&lt;h3&gt;The protobuf dependency problem&lt;/h3&gt;
&lt;p&gt;Android tombstones use a protobuf format defined in AOSP (&lt;a href=&quot;https://cs.android.com/android/platform/superproject/main/+/main:system/core/debuggerd/proto/tombstone.proto&quot;&gt;&lt;code&gt;tombstone.proto&lt;/code&gt;&lt;/a&gt;). The initial implementation used &lt;code&gt;protobuf-javalite&lt;/code&gt; for decoding, which immediately caused version conflicts for SDK consumers already using protobuf (usually via Firebase). Within a month of the initial release, we replaced it with &lt;code&gt;epitaph&lt;/code&gt;, a handwritten decoder for the tombstone protobuf encoding, free of transitive dependencies and weighing around 30KiB. We also added a scheduled CI workflow to monitor AOSP for changes to the tombstone protobuf schema, so we know early if any consequential format changes land in the platform.&lt;/p&gt;
&lt;p&gt;The unifying theme across these challenges is that native crash reporting is not a self-contained feature. It sits at the intersection of the SDK&amp;#39;s event pipeline, session lifecycle, disk caching, and the existing NDK integration, each of which had been designed with the assumption that it was the only actor in its domain.&lt;/p&gt;
&lt;p&gt;Adding tombstone support meant teaching these components to share: the history dispatcher with &lt;code&gt;ANR&lt;/code&gt; detection, the outbox with the &lt;code&gt;NDK&lt;/code&gt; integration, the session finalizer with a new crash source, and the event processor with a new category of event. We chose refactoring over duplication at each of these intersection points, which made the initial PRs larger and the review cycles a bit longer, but left the architecture at least as clean as we found it. Especially the common Java SDK core did not see any behavioral changes.&lt;/p&gt;
&lt;h2&gt;Closing the gap&lt;/h2&gt;
&lt;p&gt;Tombstone support closes a gap that has existed since Sentry first shipped native crash reporting on Android: the difference between what the platform knows about a crash and what the SDK could tell you.&lt;/p&gt;
&lt;p&gt;While that gap might seem arbitrary since we could replicate parts of the platform&amp;#39;s own crash infrastructure inside the app, it only happened by paying the cost of binary size, maintenance burden, and still incomplete results. With &lt;code&gt;ApplicationExitInfo&lt;/code&gt; providing programmatic access to the same data that &lt;code&gt;debuggerd&lt;/code&gt; produces, we can now offer richer crash context with less overhead and fewer moving parts.&lt;/p&gt;
&lt;p&gt;Of course, the limitation is real: this only works on Android 12 and above. For older devices and apps that need instrumentation of their native code beyond error reporting, the NDK integration remains available, and the two coexist cleanly. But with Android 12+ now representing 75% (according to &lt;a href=&quot;https://apilevels.com/&quot;&gt;apilevels.com&lt;/a&gt;, as of 03/2026) of cumulative usage distribution, the balance has tipped. For most apps, tombstone support is the primary native crash reporting path today, and &lt;code&gt;sentry-native-ndk&lt;/code&gt; is the fallback.&lt;/p&gt;
&lt;p&gt;Tombstone support is available in &lt;code&gt;sentry-android-core&lt;/code&gt; 8.30.0 and above. See the &lt;a href=&quot;https://docs.sentry.io/platforms/android/&quot;&gt;Android SDK docs&lt;/a&gt; for configuration details and guidance on whether to keep or drop the NDK integration for your app.&lt;/p&gt;
</content:encoded></item><item><title>Sample AI traces at 100% without sampling everything</title><link>https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/</link><guid isPermaLink="true">https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A little while ago, when agents were telling me &amp;quot;You&amp;#39;re absolutely right!&amp;quot;, I was building &lt;a href=&quot;https://webvitals.com&quot;&gt;webvitals.com&lt;/a&gt;. You put in a URL, it kicks off an API request to a Next.js API route that invokes an agent with a few tools to scan it and provide AI generated suggestions to improve your… you guessed it… &lt;a href=&quot;https://sentry.io/for/web-vitals/&quot;&gt;Web Vitals&lt;/a&gt;. Do we even care about these anymore?&lt;/p&gt;
&lt;p&gt;I had the &lt;code&gt;traceSampleRate&lt;/code&gt; set to 100% in development, but in production, I sampled it down to 10% because… well that&amp;#39;s what our instrumentation recommends. Kyle wrote a great blog post explaining that &amp;quot;&lt;a href=&quot;https://blog.sentry.io/sampling-strategy-sentry/&quot;&gt;Watching everything is watching nothing&lt;/a&gt;&amp;quot;. But AI is non-deterministic. And when I was debugging an error from a tool call, I realized I was missing very important spans emitted from the Vercel AI SDK because of that sampling strategy.&lt;/p&gt;
&lt;p&gt;An agent run with 7 tool calls doesn&amp;#39;t get partially sampled. You either capture the whole span tree or you lose it entirely. This is how head-based sampling works.&lt;/p&gt;
&lt;p&gt;I was chasing ghosts.&lt;/p&gt;
&lt;h2&gt;Agent Runs Are Span Trees, and Sampling Is All-or-Nothing&lt;/h2&gt;
&lt;p&gt;A typical agent execution looks like this in Sentry&amp;#39;s trace view:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /api/chat (http.server)
└── gen_ai.invoke_agent &amp;quot;Research Agent&amp;quot;
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← initial reasoning
    ├── gen_ai.execute_tool &amp;quot;search_docs&amp;quot;              ← tool call
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← process results
    ├── gen_ai.execute_tool &amp;quot;summarize&amp;quot;                ← second tool call
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← decides to hand off
    └── gen_ai.execute_tool &amp;quot;transfer_to_writer&amp;quot;       ← handoff via tool
        └── gen_ai.invoke_agent &amp;quot;Writer Agent&amp;quot;
            ├── gen_ai.request &amp;quot;chat gemini-2.5-flash&amp;quot;
            └── gen_ai.execute_tool &amp;quot;format_output&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s 11 spans in a single run. The sampling decision happens once, at the root: the &lt;code&gt;POST /api/chat&lt;/code&gt; HTTP transaction. Every child span inherits that decision. If the root is dropped, all 9 spans disappear.&lt;/p&gt;
&lt;p&gt;This is fundamentally different from sampling HTTP requests, where dropping one &lt;code&gt;GET /api/users&lt;/code&gt; is no big deal because the next one is basically identical.&lt;/p&gt;
&lt;p&gt;Agent runs are not identical. Each one makes different decisions, calls different tools, processes different data. An agent that hallucinated on run 67 might work perfectly on run 420. If your sample rate dropped 67, you&amp;#39;ll never know what went wrong.&lt;/p&gt;
&lt;h2&gt;How Head-Based Sampling Actually Works (and Why It Matters Here)&lt;/h2&gt;
&lt;p&gt;Both the Sentry JavaScript and Python SDKs use head-based sampling: the decision is made at the start of the trace, before any child spans exist.&lt;/p&gt;
&lt;p&gt;In the JavaScript SDK, &lt;a href=&quot;https://github.com/getsentry/sentry-javascript/blob/develop/packages/opentelemetry/src/sampler.ts#L79&quot;&gt;&lt;code&gt;SentrySampler.shouldSample()&lt;/code&gt;&lt;/a&gt; is explicit about this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// We only sample based on parameters (like tracesSampleRate or tracesSampler)
// for root spans. Non-root spans simply inherit the sampling decision
// from their parent.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Non-root spans don&amp;#39;t get a vote. If the root span was dropped, &lt;code&gt;tracesSampler&lt;/code&gt; is never called for any child, including your &lt;code&gt;gen_ai.request&lt;/code&gt; and &lt;code&gt;gen_ai.execute_tool&lt;/code&gt; spans. They inherit the parent&amp;#39;s fate.&lt;/p&gt;
&lt;p&gt;In Python, the same logic lives in &lt;a href=&quot;https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/tracing.py#L1150&quot;&gt;&lt;code&gt;Transaction._set_initial_sampling_decision()&lt;/code&gt;&lt;/a&gt;. The &lt;code&gt;traces_sampler&lt;/code&gt; callback receives a &lt;code&gt;sampling_context&lt;/code&gt; dict with &lt;code&gt;transaction_context&lt;/code&gt; (containing &lt;code&gt;op&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;) and &lt;code&gt;parent_sampled&lt;/code&gt;. It only fires for root transactions.&lt;/p&gt;
&lt;p&gt;This means head-based sampling doesn&amp;#39;t support &lt;strong&gt;independently sampling gen_ai child spans at a different rate than their parent transaction.&lt;/strong&gt; There&amp;#39;s no &amp;quot;sample 100% of LLM calls but 10% of HTTP requests.&amp;quot; If the HTTP request is dropped, the LLM calls inside it are dropped too.&lt;/p&gt;
&lt;p&gt;I&amp;#39;d love to walk through a few different scenarios to show the difference in filtering approaches based on wether or not the root span is from an agent or the application.&lt;/p&gt;
&lt;h2&gt;Scenario 1: The &lt;code&gt;gen_ai&lt;/code&gt; Span IS the Root&lt;/h2&gt;
&lt;p&gt;Sometimes your agent run &lt;em&gt;is&lt;/em&gt; the root span. Maybe it&amp;#39;s a cron job thats running an agent, a queue consumer processing an AI task, or a CLI script. In these cases, &lt;code&gt;tracesSampler&lt;/code&gt; sees the &lt;code&gt;gen_ai.*&lt;/code&gt; operation directly and you can match on it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampler: ({ name, attributes, inheritOrSampleWith }) =&amp;gt; {
    // Standalone gen_ai root spans - always sample
    if (attributes?.[&amp;#39;sentry.op&amp;#39;]?.startsWith(&amp;#39;gen_ai.&amp;#39;) || attributes?.[&amp;#39;gen_ai.system&amp;#39;]) {
      return 1.0;
    }

    return inheritOrSampleWith(0.2);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def traces_sampler(sampling_context):
    op = sampling_context.get(&amp;quot;transaction_context&amp;quot;, {}).get(&amp;quot;op&amp;quot;, &amp;quot;&amp;quot;)

    # Standalone gen_ai root spans - always sample
    if op.startswith(&amp;quot;gen_ai.&amp;quot;):
        return 1.0

    parent = sampling_context.get(&amp;quot;parent_sampled&amp;quot;)
    if parent is not None:
        return float(parent)

    return 0.2

sentry_sdk.init(dsn=&amp;quot;...&amp;quot;, traces_sampler=traces_sampler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the easy case. The hard case is next.&lt;/p&gt;
&lt;h2&gt;Scenario 2: The &lt;code&gt;gen_ai&lt;/code&gt; Spans Are Children of an HTTP Transaction&lt;/h2&gt;
&lt;p&gt;This is the common case in web applications. A user hits &lt;code&gt;POST /api/chat&lt;/code&gt;, your framework creates an &lt;code&gt;http.server&lt;/code&gt; root span, and somewhere inside that request handler your agent runs. By the time the first &lt;code&gt;gen_ai.request&lt;/code&gt; span is created, the sampling decision was already made for the HTTP transaction.&lt;/p&gt;
&lt;p&gt;The fix: &lt;strong&gt;identify which routes trigger AI calls and sample those routes at 100%.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampler: ({ name, attributes, inheritOrSampleWith }) =&amp;gt; {
    // Standalone gen_ai root spans
    if (attributes?.[&amp;#39;sentry.op&amp;#39;]?.startsWith(&amp;#39;gen_ai.&amp;#39;) || attributes?.[&amp;#39;gen_ai.system&amp;#39;]) {
      return 1.0;
    }

    // HTTP routes that serve AI features - always sample
    if (name?.includes(&amp;#39;/api/chat&amp;#39;) ||
        name?.includes(&amp;#39;/api/agent&amp;#39;) ||
        name?.includes(&amp;#39;/api/generate&amp;#39;)) {
      return 1.0;
    }

    return inheritOrSampleWith(0.2);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def traces_sampler(sampling_context):
    tx_context = sampling_context.get(&amp;quot;transaction_context&amp;quot;, {})
    op = tx_context.get(&amp;quot;op&amp;quot;, &amp;quot;&amp;quot;)
    name = tx_context.get(&amp;quot;name&amp;quot;, &amp;quot;&amp;quot;)

    # Standalone gen_ai root spans
    if op.startswith(&amp;quot;gen_ai.&amp;quot;):
        return 1.0

    # HTTP routes that serve AI features - always sample
    if op == &amp;quot;http.server&amp;quot; and any(
        p in name for p in [&amp;quot;/api/chat&amp;quot;, &amp;quot;/api/agent&amp;quot;, &amp;quot;/api/generate&amp;quot;]
    ):
        return 1.0

    # Honour parent decision in distributed traces
    parent = sampling_context.get(&amp;quot;parent_sampled&amp;quot;)
    if parent is not None:
        return float(parent)

    return 0.2

sentry_sdk.init(dsn=&amp;quot;...&amp;quot;, traces_sampler=traces_sampler)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the route strings with whatever paths your AI features live on. If your entire app is AI-powered, skip the &lt;code&gt;tracesSampler&lt;/code&gt; and just set &lt;code&gt;tracesSampleRate: 1.0&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;The Cost Math: AI API Bills Dwarf Observability Costs&lt;/h2&gt;
&lt;p&gt;The instinct to sample AI traces at a lower rate usually comes from cost concerns. Let&amp;#39;s look at the actual numbers.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Cost per event&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4 input (1K tokens)&lt;/td&gt;
&lt;td&gt;~$0.003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4 output (1K tokens)&lt;/td&gt;
&lt;td&gt;~$0.015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 2.5 Flash input (1K tokens)&lt;/td&gt;
&lt;td&gt;~$0.00015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 2.5 Flash output (1K tokens)&lt;/td&gt;
&lt;td&gt;~$0.0006&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A typical agent run (3 LLM calls, 2 tool calls)&lt;/td&gt;
&lt;td&gt;$0.02-$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sentry span events for that agent run (~9 spans)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fraction of a cent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The LLM calls themselves are 10-100x more expensive than the monitoring. You&amp;#39;re already paying for the AI call; dropping the observability span to save a fraction of a cent per call is like skipping the dashcam to save on gas.&lt;/p&gt;
&lt;h2&gt;When 100% Tracing Isn&amp;#39;t Feasible: Metrics and Logs as a Safety Net&lt;/h2&gt;
&lt;p&gt;If you genuinely can&amp;#39;t sample AI routes at 100%, because of, say, massive scale or strict budget restraints, you can still capture the important signals from every AI call using Sentry &lt;a href=&quot;https://docs.sentry.io/platforms/python/metrics/&quot;&gt;Metrics&lt;/a&gt; and &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/node/logs/&quot;&gt;Logs&lt;/a&gt;. Both are independent of trace sampling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript - emit metrics on every LLM call:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

// After every LLM call, regardless of trace sampling:
Sentry.metrics.distribution(&amp;quot;gen_ai.token_usage&amp;quot;, result.usage.totalTokens, {
  unit: &amp;quot;none&amp;quot;,
  attributes: {
    model: &amp;quot;claude-sonnet-4-6&amp;quot;,
    user_id: user.id,
    endpoint: &amp;quot;/api/chat&amp;quot;,
  },
});

Sentry.metrics.distribution(&amp;quot;gen_ai.latency&amp;quot;, responseTimeMs, {
  unit: &amp;quot;millisecond&amp;quot;,
  attributes: { model: &amp;quot;claude-sonnet-4-6&amp;quot; },
});

Sentry.metrics.count(&amp;quot;gen_ai.calls&amp;quot;, 1, {
  attributes: {
    model: &amp;quot;claude-sonnet-4-6&amp;quot;,
    status: result.error ? &amp;quot;error&amp;quot; : &amp;quot;success&amp;quot;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python - emit metrics on every LLM call:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;

sentry_sdk.metrics.distribution(
    &amp;quot;gen_ai.token_usage&amp;quot;,
    result.usage.total_tokens,
    attributes={
        &amp;quot;model&amp;quot;: &amp;quot;claude-sonnet-4-6&amp;quot;,
        &amp;quot;user_id&amp;quot;: str(user.id),
        &amp;quot;endpoint&amp;quot;: &amp;quot;/api/chat&amp;quot;,
    },
)

sentry_sdk.metrics.distribution(
    &amp;quot;gen_ai.latency&amp;quot;,
    response_time_ms,
    unit=&amp;quot;millisecond&amp;quot;,
    attributes={&amp;quot;model&amp;quot;: &amp;quot;claude-sonnet-4-6&amp;quot;},
)

sentry_sdk.metrics.count(
    &amp;quot;gen_ai.calls&amp;quot;,
    1,
    attributes={
        &amp;quot;model&amp;quot;: &amp;quot;claude-sonnet-4-6&amp;quot;,
        &amp;quot;status&amp;quot;: &amp;quot;error&amp;quot; if error else &amp;quot;success&amp;quot;,
    },
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also log every call with structured attributes for searchability:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.logger.info(&amp;quot;LLM call completed&amp;quot;, {
  model: &amp;quot;claude-sonnet-4-6&amp;quot;,
  user_id: user.id,
  input_tokens: result.usage.promptTokens,
  output_tokens: result.usage.completionTokens,
  latency_ms: responseTimeMs,
  status: &amp;quot;success&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sentry_sdk.logger.info(
    &amp;quot;LLM call completed&amp;quot;,
    model=&amp;quot;claude-sonnet-4-6&amp;quot;,
    user_id=str(user.id),
    input_tokens=result.usage.prompt_tokens,
    output_tokens=result.usage.completion_tokens,
    latency_ms=response_time_ms,
    status=&amp;quot;success&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s what each telemetry layer gives you:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Traces (sampled)&lt;/th&gt;
&lt;th&gt;Metrics (100%)&lt;/th&gt;
&lt;th&gt;Logs (100%)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Full span tree with prompts/responses&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token usage distributions (p50, p99)&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost attribution by model/user&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rates by model/endpoint&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency distributions&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Searchable per-call records&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The recommended approach:&lt;/strong&gt; Use &lt;code&gt;tracesSampler&lt;/code&gt; to capture 100% of AI-related routes. If that&amp;#39;s not possible, combine a lower trace rate with &lt;a href=&quot;https://sentry.io/product/metrics/&quot;&gt;metrics&lt;/a&gt; and logs emitted on every call. Traces give you the debugging depth; metrics and logs give you the aggregate picture.&lt;/p&gt;
&lt;p&gt;Once you&amp;#39;re emitting these metrics, you can build custom dashboards that go beyond what the &lt;a href=&quot;https://docs.sentry.io/ai/monitoring/agents/dashboards/&quot;&gt;pre-built AI Agents dashboard&lt;/a&gt; shows. The &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt; makes this scriptable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Find your most expensive users - the pre-built dashboard doesn&amp;#39;t group by user
sentry dashboard create &amp;#39;AI Cost Attribution&amp;#39;
sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;Most Expensive Users&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.total_tokens&amp;quot; \
  --where &amp;quot;span.op:gen_ai.request&amp;quot; \
  --group-by &amp;quot;user.id&amp;quot; \
  --sort &amp;quot;-sum:gen_ai.usage.total_tokens&amp;quot; \
  --limit 20

# Cost per conversation - find runaway multi-turn sessions
sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;Cost per Conversation&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.total_tokens&amp;quot; &amp;quot;count&amp;quot; \
  --where &amp;quot;span.op:gen_ai.request&amp;quot; \
  --group-by &amp;quot;gen_ai.conversation.id&amp;quot; \
  --sort &amp;quot;-sum:gen_ai.usage.total_tokens&amp;quot; \
  --limit 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The pre-built dashboard gives you per-model and per-tool aggregates. Custom dashboards answer the business questions: &lt;em&gt;who&amp;#39;s driving cost, which features justify their AI spend, and which conversations are spiraling.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Full Production Config&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s a complete setup that samples AI routes at 100%, everything else at your baseline, and emits metrics as a safety net:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampler: ({ name, attributes, inheritOrSampleWith }) =&amp;gt; {
    if (attributes?.[&amp;#39;sentry.op&amp;#39;]?.startsWith(&amp;#39;gen_ai.&amp;#39;) || attributes?.[&amp;#39;gen_ai.system&amp;#39;]) {
      return 1.0;
    }
    if (name?.includes(&amp;#39;/api/chat&amp;#39;) || name?.includes(&amp;#39;/api/agent&amp;#39;)) {
      return 1.0;
    }
    return inheritOrSampleWith(0.2);
  },
});

// Wrapper for any LLM call - emit metrics regardless of sampling
function trackLLMCall(model, usage, latencyMs, userId) {
  Sentry.metrics.distribution(&amp;quot;gen_ai.token_usage&amp;quot;, usage.totalTokens, {
    attributes: { model, user_id: userId },
  });
  Sentry.metrics.distribution(&amp;quot;gen_ai.latency&amp;quot;, latencyMs, {
    unit: &amp;quot;millisecond&amp;quot;,
    attributes: { model },
  });
  Sentry.metrics.count(&amp;quot;gen_ai.calls&amp;quot;, 1, {
    attributes: { model, status: &amp;quot;success&amp;quot; },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;

def traces_sampler(sampling_context):
    tx = sampling_context.get(&amp;quot;transaction_context&amp;quot;, {})
    op, name = tx.get(&amp;quot;op&amp;quot;, &amp;quot;&amp;quot;), tx.get(&amp;quot;name&amp;quot;, &amp;quot;&amp;quot;)

    if op.startswith(&amp;quot;gen_ai.&amp;quot;):
        return 1.0
    if op == &amp;quot;http.server&amp;quot; and any(
        p in name for p in [&amp;quot;/api/chat&amp;quot;, &amp;quot;/api/agent&amp;quot;]
    ):
        return 1.0

    parent = sampling_context.get(&amp;quot;parent_sampled&amp;quot;)
    if parent is not None:
        return float(parent)
    return 0.2

sentry_sdk.init(
    dsn=&amp;quot;...&amp;quot;,
    traces_sampler=traces_sampler,
)

# Wrapper for any LLM call - emit metrics regardless of sampling
def track_llm_call(model, usage, latency_ms, user_id):
    sentry_sdk.metrics.distribution(
        &amp;quot;gen_ai.token_usage&amp;quot;, usage.total_tokens,
        attributes={&amp;quot;model&amp;quot;: model, &amp;quot;user_id&amp;quot;: str(user_id)},
    )
    sentry_sdk.metrics.distribution(
        &amp;quot;gen_ai.latency&amp;quot;, latency_ms,
        unit=&amp;quot;millisecond&amp;quot;,
        attributes={&amp;quot;model&amp;quot;: model},
    )
    sentry_sdk.metrics.count(
        &amp;quot;gen_ai.calls&amp;quot;, 1,
        attributes={&amp;quot;model&amp;quot;: model, &amp;quot;status&amp;quot;: &amp;quot;success&amp;quot;},
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Quick Reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;AI is the core product&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tracesSampleRate: 1.0&lt;/code&gt; - sample everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI is one feature in a larger app&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tracesSampler&lt;/code&gt; with AI routes at 1.0, baseline for the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can&amp;#39;t afford 100% on AI routes&lt;/td&gt;
&lt;td&gt;Lower trace rate + metrics/logs on every call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already using &lt;code&gt;tracesSampler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add AI route matching to your existing logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample rate is already 1.0&lt;/td&gt;
&lt;td&gt;No change needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The underlying principle: agent runs are high-value, low-volume (relative to HTTP traffic), and expensive to reproduce. Sample them accordingly.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re just getting started with AI monitoring, check out our companion post on &lt;a href=&quot;https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/&quot;&gt;the developer&amp;#39;s guide to AI agent monitoring&lt;/a&gt;, which covers the full setup across 10+ frameworks, the pre-built dashboards, and a real debugging walkthrough.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For framework-specific setup, see our&lt;/em&gt; &lt;a href=&quot;https://docs.sentry.io/ai/monitoring/agents/&quot;&gt;&lt;em&gt;AI monitoring docs&lt;/em&gt;&lt;/a&gt;&lt;em&gt;. If you&amp;#39;re using an AI coding assistant, install the&lt;/em&gt; &lt;a href=&quot;https://cli.sentry.dev/agentic-usage/&quot;&gt;&lt;em&gt;Sentry CLI skill&lt;/em&gt;&lt;/a&gt; &lt;em&gt;(&lt;code&gt;npx skills add &amp;lt;https://cli.sentry.dev&amp;gt;&lt;/code&gt;) to configure your sampling, build custom dashboards, and investigate issues directly from your editor.&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>AI agent observability: The developer&apos;s guide to agent monitoring</title><link>https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/</link><guid isPermaLink="true">https://blog.sentry.io/ai-agent-observability-developers-guide-to-agent-monitoring/</guid><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most discussions about agent observability read like outdated compliance checklists with &amp;quot;AI&amp;quot; substituted for older technologies. They emphasize comprehensive logging, evaluation metrics, and governance frameworks—but provide no actual code examples or guidance for real debugging scenarios.&lt;/p&gt;
&lt;p&gt;Effective &lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;agent monitoring&lt;/a&gt; requires two essential components: dashboards showing aggregate behavior across all agents, and detailed traces explaining specific failures. Most platforms provide only one. Here&amp;#39;s what having both looks like in practice.&lt;/p&gt;
&lt;h2&gt;What is Agent Observability?&lt;/h2&gt;
&lt;p&gt;Agent observability provides complete visibility into AI agent operations: model invocations, tool selections, decision sequences, handoffs, token consumption, and associated costs.&lt;/p&gt;
&lt;p&gt;Traditional application monitoring focuses on requests, errors, and response times. This works adequately for stateless HTTP services where requests are independent.&lt;/p&gt;
&lt;p&gt;AI agents operate fundamentally differently. A single agent execution might involve multiple model calls, tool invocations, sub-agent transfers, and reasoning loops—all interdependent. When outputs are incorrect, failure points could be anywhere: incorrect tool responses, context window limitations, wrong function selection, or lost state during handoffs.&lt;/p&gt;
&lt;p&gt;Agent observability provides comprehensive visibility into the complete decision-making process across these interconnected operations. Agent quality assessment, workflow debugging, and cost control all require this visibility level.&lt;/p&gt;
&lt;h3&gt;Why Traditional Monitoring Fails for AI Agents&lt;/h3&gt;
&lt;p&gt;Standard &lt;a href=&quot;https://sentry.io/solutions/application-performance-monitoring/&quot;&gt;APM tools&lt;/a&gt; report that &lt;code&gt;POST /api/chat&lt;/code&gt; returned status 200 in 4.2 seconds. They won&amp;#39;t reveal that internally, the agent executed 5 model calls, with the third call selecting an incorrect tool that returned outdated information, which the model then accurately summarized as garbage.&lt;/p&gt;
&lt;p&gt;An &amp;quot;log everything later&amp;quot; approach produces dashboards showing counts and averages without enabling deeper investigation. An agent producing incorrect output might have completed 12 model calls, executed 4 tools, transferred to a sub-agent, then generated incorrect output. Aggregate metrics indicate error rate increases. They don&amp;#39;t indicate where reasoning failed.&lt;/p&gt;
&lt;p&gt;The solution requires &lt;strong&gt;structured tracing&lt;/strong&gt; based on consistent standards, allowing dashboards, traces, and alerts to communicate uniformly.&lt;/p&gt;
&lt;h2&gt;The OpenTelemetry Standard for Agent Observability&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/gen-ai/&quot;&gt;OpenTelemetry &lt;code&gt;gen_ai&lt;/code&gt; semantic conventions&lt;/a&gt; establish standardized instrumentation for agent systems. Instead of custom logging, every AI operation produces a structured span containing consistent attributes. Core operations include:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Span Operation&lt;/th&gt;
&lt;th&gt;Captured Information&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single model call: model name, prompt, response, token counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.invoke_agent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete agent lifecycle from task initiation to final output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.execute_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tool/function invocation: name, input, output, duration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;These compose hierarchically:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /api/chat (http.server)
└── gen_ai.invoke_agent &amp;quot;Research Agent&amp;quot;
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← initial reasoning
    ├── gen_ai.execute_tool &amp;quot;search_docs&amp;quot;              ← tool call
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← process results
    ├── gen_ai.execute_tool &amp;quot;summarize&amp;quot;                ← second tool call
    ├── gen_ai.request &amp;quot;chat claude-sonnet-4-6&amp;quot;        ← decides to hand off
    └── gen_ai.execute_tool &amp;quot;transfer_to_writer&amp;quot;       ← handoff via tool
        └── gen_ai.invoke_agent &amp;quot;Writer Agent&amp;quot;
            ├── gen_ai.request &amp;quot;chat gemini-2.5-flash&amp;quot;
            └── gen_ai.execute_tool &amp;quot;format_output&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an open standard, not proprietary. Any platform following it can ingest these spans. The span operation follows the pattern &lt;code&gt;gen_ai.{operation_name}&lt;/code&gt;. For manual instrumentation, &lt;code&gt;gen_ai.request&lt;/code&gt; covers all model calls. SDK auto-instrumentation may generate more specific operations like &lt;code&gt;gen_ai.chat&lt;/code&gt; or &lt;code&gt;gen_ai.embeddings&lt;/code&gt; depending on API calls. Because these are structured spans rather than unstructured logs, they enable both dashboards and trace visualization.&lt;/p&gt;
&lt;h2&gt;Key Metrics for AI Agent Monitoring&lt;/h2&gt;
&lt;p&gt;Before selecting tools, track these measurements for production agents:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reliability metrics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent error rate&lt;/strong&gt; — percentage of agent executions that fail or produce errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool failure rate&lt;/strong&gt; — identifies unreliable tools and their impact on agent success&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency (p50, p95)&lt;/strong&gt; — per-agent and per-model tracking to identify regressions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cost metrics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Token usage&lt;/strong&gt; — input, output, cached, and reasoning tokens per model. Cached and reasoning tokens are subsets, not cumulative. Incorrect calculation means fictional cost dashboards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost per model&lt;/strong&gt; — compare similar workloads. Example: &lt;code&gt;claude-sonnet-4-6&lt;/code&gt; costs $10.8K weekly while &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; handles equivalent volume for $645.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost per user/tier&lt;/strong&gt; — identifies which users or pricing levels consume most AI resources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Quality metrics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool call frequency&lt;/strong&gt; — tracks how often agents invoke each tool and invocation sequence&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token efficiency&lt;/strong&gt; — average tokens per successful completion. Growing numbers suggest inflating prompts or context windows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache hit rate&lt;/strong&gt; — percentage of input tokens served from cache. If caching is enabled but this metric isn&amp;#39;t improving, something needs investigation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Comprehensive platforms following OpenTelemetry conventions surface these metrics automatically from trace data.&lt;/p&gt;
&lt;h2&gt;Auto-instrumentation for 10+ Frameworks&lt;/h2&gt;
&lt;p&gt;Sentry auto-instruments major AI frameworks in &lt;a href=&quot;https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/ai-agents-module/&quot;&gt;Python&lt;/a&gt; and &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/node/ai-agent-monitoring/&quot;&gt;Node.js&lt;/a&gt;, including OpenAI, Anthropic, Google GenAI, LangChain, LangGraph, Pydantic AI, OpenAI Agents SDK, Vercel AI SDK, and others. Manual span creation isn&amp;#39;t needed. Installation, tracing enablement, and automatic pickup occur.&lt;/p&gt;
&lt;p&gt;Complete setup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;

sentry_sdk.init(
    dsn=&amp;quot;YOUR_DSN&amp;quot;,
    traces_sample_rate=1.0,
)
# OpenAI, Anthropic, LangChain, LangGraph, Pydantic AI,
# Google GenAI -- all auto-instrumented when detected.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s the entire configuration. Making Anthropic or OpenAI calls produces visible spans.&lt;/p&gt;
&lt;h2&gt;Pre-built Agent Monitoring Dashboards&lt;/h2&gt;
&lt;p&gt;Most observability platforms include pre-built agent monitoring dashboards. Once instrumentation is active, Sentry&amp;#39;s &lt;a href=&quot;https://docs.sentry.io/ai/monitoring/agents/dashboard/&quot;&gt;AI Agents dashboard&lt;/a&gt; provides three views:&lt;/p&gt;
&lt;h3&gt;AI Agents Overview&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/ai-agent-observability-developers-guide-to-agent-monitoring/overview.png&quot; alt=&quot;AI Agents Overview Dashboard&quot;&gt;&lt;/p&gt;
&lt;p&gt;Displays agent runs, duration, total model calls, tokens consumed, and tool invocations. This is the &amp;quot;is everything functioning?&amp;quot; view.&lt;/p&gt;
&lt;h3&gt;AI Agents Model Details&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/ai-agent-observability-developers-guide-to-agent-monitoring/agent-models-dash.png&quot; alt=&quot;AI Agents Model Details Dashboard&quot;&gt;&lt;/p&gt;
&lt;p&gt;Per-model cost projections, token breakdown (input/output/cached/reasoning), and latency. This automatically displays cost metrics.&lt;/p&gt;
&lt;h3&gt;AI Agents Tool Details&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/ai-agent-observability-developers-guide-to-agent-monitoring/agent-tools-dash.png&quot; alt=&quot;AI Agents Tool Details Dashboard&quot;&gt;&lt;/p&gt;
&lt;p&gt;Per-tool invocation frequency, error rates, and p95 latency. A tool failing 12% of the time appears here before users report problems.&lt;/p&gt;
&lt;p&gt;These dashboards appear immediately once spans flow. However, they display aggregates: per-model totals, per-tool error rates, overall agent counts. They answer technical questions and highlight problems—but what about business-level inquiries?&lt;/p&gt;
&lt;h2&gt;Custom Agent Monitoring Dashboards&lt;/h2&gt;
&lt;p&gt;Pre-built dashboards show aggregate health signals. They don&amp;#39;t show who drives AI costs, which features justify spending, or whether caching strategies save money. Addressing these questions requires slicing trace data by custom dimensions: user tier, feature flag, experiment group.&lt;/p&gt;
&lt;p&gt;Some platforms enable custom queries against span data. With the &lt;a href=&quot;https://cli.sentry.dev/&quot;&gt;Sentry CLI&lt;/a&gt;, you can script this—and its &lt;a href=&quot;https://cli.sentry.dev/agentic-usage/&quot;&gt;agent skill system&lt;/a&gt; allows AI coding assistants like Claude Code to build dashboards:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Who are my most expensive users?&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard create &amp;#39;AI Cost Attribution&amp;#39;

sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;Most Expensive Users&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.total_tokens&amp;quot; \
  --where &amp;quot;span.op:gen_ai.request&amp;quot; \
  --group-by &amp;quot;user.id&amp;quot; \
  --sort &amp;quot;-sum:gen_ai.usage.total_tokens&amp;quot; \
  --limit 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Which pricing tier is eating my AI budget?&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tag users with their plan, then group in the dashboard:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sentry_sdk.set_tag(&amp;quot;user_tier&amp;quot;, user.plan)  # &amp;quot;free&amp;quot;, &amp;quot;pro&amp;quot;, &amp;quot;enterprise&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;AI Cost by Tier&amp;quot; \
  --display bar --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.total_tokens&amp;quot; \
  --where &amp;quot;span.op:gen_ai.request&amp;quot; \
  --group-by &amp;quot;user_tier&amp;quot; \
  --sort &amp;quot;-sum:gen_ai.usage.total_tokens&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reveals that free-tier users consume 60% of AI budget. The same tagging pattern works for any dimension: &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;feature_flag&lt;/code&gt;, &lt;code&gt;experiment_group&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Which agents are token-hungry?&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;Avg Tokens per Agent&amp;quot; \
  --display table --dataset spans \
  --query &amp;quot;avg:gen_ai.usage.total_tokens&amp;quot; &amp;quot;count&amp;quot; \
  --where &amp;quot;span.op:gen_ai.invoke_agent&amp;quot; \
  --group-by &amp;quot;gen_ai.agent.name&amp;quot; \
  --sort &amp;quot;-avg:gen_ai.usage.total_tokens&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &amp;quot;Research Agent&amp;quot; averages 15K tokens per run while &amp;quot;Summarizer Agent&amp;quot; averages 2K, you know where to focus prompt optimization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;Is my prompt caching actually saving money?&amp;quot;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry dashboard widget add &amp;#39;AI Cost Attribution&amp;#39; &amp;quot;Cache Hit Rate&amp;quot; \
  --display line --dataset spans \
  --query &amp;quot;sum:gen_ai.usage.input_tokens.cached&amp;quot; &amp;quot;sum:gen_ai.usage.input_tokens&amp;quot; \
  --where &amp;quot;span.op:gen_ai.request&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If cached-to-total ratio isn&amp;#39;t improving after enabling caching, your prompt structure needs investigation.&lt;/p&gt;
&lt;h2&gt;Why Tracing Matters for Agent Monitoring&lt;/h2&gt;
&lt;p&gt;Dashboards show totals. Traces show decisions.&lt;/p&gt;
&lt;p&gt;A dashboard indicates error rates increased or latency spiked. A trace identifies which agent, which model call, and which tool caused it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;Distributed tracing&lt;/a&gt; already captures complete span hierarchies for requests: browser interactions, HTTP calls, server routing, database queries. Agent observability integrates into this. Your &lt;code&gt;gen_ai.*&lt;/code&gt; spans appear as children within existing traces, so model calls, tool executions, MCP server interactions, and sub-agent transfers sit alongside regular application spans. No separate system required.&lt;/p&gt;
&lt;p&gt;This integration is powerful. You&amp;#39;re examining agent data within full request context, from user click to final tool response, with agent decisions as one layer in the entire stack.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s what this looks like in Sentry&amp;#39;s trace view:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/ai-agent-observability-developers-guide-to-agent-monitoring/agent-detailed-trace-view.png&quot; alt=&quot;Distributed trace showing full agent workflow&quot;&gt;&lt;/p&gt;
&lt;p&gt;Single request, end-to-end: from user clicking &amp;quot;Send Message&amp;quot; through API, agent orchestration with model calls and MCP server interactions, through handoff to second agent. Clicking any span reveals model, tokens, cost, and system prompt details.&lt;/p&gt;
&lt;h2&gt;Agent Observability Best Practices&lt;/h2&gt;
&lt;p&gt;Whatever platform you choose, implement these practices:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use structured tracing, not logs.&lt;/strong&gt; Unstructured logs can&amp;#39;t reconstruct reasoning chains. OpenTelemetry &lt;code&gt;gen_ai&lt;/code&gt; spans provide searchable, filterable hierarchies powering dashboards and trace views simultaneously.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sample AI traces at 100%.&lt;/strong&gt; Agent runs are span hierarchies. Sampling drops complete executions, not individual calls. If &lt;code&gt;tracesSampleRate&lt;/code&gt; is below 1.0, you&amp;#39;re losing entire agent runs. Use &lt;code&gt;tracesSampler&lt;/code&gt; to keep AI routes at 100% while sampling everything else at baseline. (&lt;a href=&quot;https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/&quot;&gt;Detailed sampling guide&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Track cost by user, not just by model.&lt;/strong&gt; The pre-built dashboard shows per-model totals. You need per-user and per-tier attribution for business decisions about rate limiting, pricing, and model routing.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor tool reliability separately.&lt;/strong&gt; A tool failing 5% of the time might not appear in overall error rates, but causes 1 in 20 agent runs to produce bad output. Your dashboard should surface per-tool error rates distinctly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect AI monitoring to your full stack.&lt;/strong&gt; Agent failure might stem from slow database queries, failed external API calls, or frontend timeouts. Isolated AI monitoring can&amp;#39;t reveal these root causes.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Full-Stack Agent Observability&lt;/h2&gt;
&lt;p&gt;Agent observability becomes most powerful when layered on top of comprehensive APM platforms, linking agent spans to errors, performance traces, session replays, and logs across your entire system.&lt;/p&gt;
&lt;p&gt;Isolated &lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;AI monitoring&lt;/a&gt; shows &lt;code&gt;gen_ai&lt;/code&gt; spans separately. You see that Research Agent completed 8 model calls costing $0.04. What remains invisible is why it made 8 instead of 3: your &lt;code&gt;search_docs&lt;/code&gt; tool executes a slow Postgres query timing out, causing the agent to retry with rephrased queries repeatedly.&lt;/p&gt;
&lt;p&gt;When agent spans share context with your broader infrastructure, everything clarifies. Errors include their complete span hierarchy. Session replays show user interactions triggering bad agent runs. Upstream issues (sluggish vector databases, unreliable external APIs) appear in the same trace as resulting agent behavior.&lt;/p&gt;
&lt;h3&gt;Four Steps to First Trace&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Install the SDK: &lt;code&gt;pip install sentry-sdk&lt;/code&gt; or &lt;code&gt;npm install @sentry/node&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Initialize with tracing enabled&lt;/li&gt;
&lt;li&gt;Make an AI call; spans and dashboards populate automatically&lt;/li&gt;
&lt;li&gt;(Optional) Install the CLI skill for your AI assistant:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add https://cli.sentry.dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your framework is auto-instrumented, you&amp;#39;re complete. If not, &lt;a href=&quot;https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation/ai-agents-module/&quot;&gt;manual instrumentation&lt;/a&gt; requires approximately 10 lines per span type.&lt;/p&gt;
&lt;p&gt;For comprehensive guidance on capturing 100% of AI traces, see our companion post on &lt;a href=&quot;https://blog.sentry.io/sample-ai-traces-at-100-percent-without-sampling-everything/&quot;&gt;sampling strategies for agentic applications&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Try Sentry at no cost&lt;/a&gt; - AI monitoring is included across all plans.&lt;/p&gt;
&lt;h2&gt;AI Agent Monitoring FAQs&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What is agent observability?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Agent observability is complete visibility into AI agent operations: model calls, tool selections, decision chains, handoffs, token consumption, and costs. It transcends traditional monitoring by tracking complete reasoning sequences across multi-turn interactions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How is agent monitoring different from LLM monitoring?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;LLM monitoring&lt;/a&gt; measures individual model calls (latency, tokens, errors). Agent monitoring tracks complete agent cycles: multi-step reasoning, tool execution, agent-to-agent transfers, and how individual calls combine into workflows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What metrics should I track for AI agents?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Minimum metrics: agent error rate, tool failure rate, latency (p50/p95), token usage per model, cost per user/tier, and cache hit rate. These divide into reliability (functioning properly?), cost (expenditure?), and quality (improving?) categories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What tools support agent observability?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;OpenTelemetry &lt;code&gt;gen_ai&lt;/code&gt; semantic conventions represent the emerging standard. Sentry, LangSmith, Langfuse, Arize, and Datadog all provide agent observability with distinct approaches. Sentry distinguishes itself through full-stack context: agent data connected to errors, performance traces, session replays, and logs unified in one system.&lt;/p&gt;
</content:encoded></item><item><title>Send your existing OpenTelemetry traces to Sentry</title><link>https://blog.sentry.io/send-your-existing-opentelemetry-traces/</link><guid isPermaLink="true">https://blog.sentry.io/send-your-existing-opentelemetry-traces/</guid><pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You spent months instrumenting your app with OpenTelemetry. The idea of ripping it out to adopt a new observability backend is not an option.&lt;/p&gt;
&lt;p&gt;Sentry&amp;#39;s OTLP endpoint means you don&amp;#39;t have to. In fact, two environment variables are all you need and your existing traces start showing up in &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;Sentry&amp;#39;s trace explorer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sentry&amp;#39;s OTLP support is currently in open beta. This means you can start using it today, but there are some known limitations we&amp;#39;ll cover later.&lt;/p&gt;
&lt;h2&gt;Why OTLP: keep your instrumentation, just change the destination&lt;/h2&gt;
&lt;p&gt;The main advantage of using OpenTelemetry is that &lt;strong&gt;your instrumentation stays vendor-neutral&lt;/strong&gt;. Your instrumentation code uses OpenTelemetry&amp;#39;s standard APIs, and OTLP (the protocol) sends that data to any compatible backend. This means you can switch observability backends anytime by changing a few configuration lines. This is particularly useful if you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are already heavily invested in the OpenTelemetry ecosystem&lt;/li&gt;
&lt;li&gt;Want to keep your instrumentation flexible or already use OpenTelemetry in other parts of your stack&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;#39;re starting from scratch and only need Sentry, the native Sentry SDK provides full support for all Sentry features (including span events, session replay, and profiling), while OTLP support is still in beta and has some limitations. We&amp;#39;ll compare both approaches later in this guide.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before we start, you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A Sentry account (the free tier works fine)&lt;/li&gt;
&lt;li&gt;Node.js 18 or later installed&lt;/li&gt;
&lt;li&gt;Basic familiarity with Express.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you don&amp;#39;t have a Sentry project yet, create one now. Select &lt;strong&gt;Express&lt;/strong&gt; as the platform when prompted. You can skip the DSN setup instructions because you&amp;#39;ll use the OTLP endpoint instead.&lt;/p&gt;
&lt;h2&gt;Get your Sentry OTLP credentials&lt;/h2&gt;
&lt;p&gt;Sentry provides dedicated OTLP endpoints for each project. To find those:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; in the left sidebar.&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;Organization&lt;/strong&gt; section in the &lt;strong&gt;Settings&lt;/strong&gt; sidebar, click &lt;strong&gt;Projects&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Find your project in the list and click on it to open the project settings.&lt;/li&gt;
&lt;li&gt;In the project settings sidebar, click &lt;strong&gt;Client Keys (DSN)&lt;/strong&gt; under the &lt;strong&gt;SDK Setup&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;OpenTelemetry&lt;/strong&gt; tab. Click the &lt;strong&gt;Expand&lt;/strong&gt; button to see all OTLP endpoint values.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/send-your-existing-opentelemetry-traces/sentry-opentelemetry-client-keys-dsn-otlp-config.jpg&quot; alt=&quot;Sentry OpenTelemetry configuration&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sentry UI showing Settings &amp;gt; Client Keys (DSN) &amp;gt; OpenTelemetry tab with OTLP endpoints visible&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Keep this tab open. We&amp;#39;ll use the following values in the next step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OTLP Traces Endpoint:&lt;/strong&gt; The URL where Sentry receives traces (which looks like &lt;code&gt;https://o{ORG_ID}.ingest.us.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/traces&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OTLP Traces Endpoint Headers:&lt;/strong&gt; The authentication header value. Copy only the value after &lt;code&gt;x-sentry-auth=&lt;/code&gt; (which looks like sentry &lt;code&gt;sentry_key={YOUR_PUBLIC_KEY}&lt;/code&gt;). The demo app&amp;#39;s &lt;code&gt;instrument.js&lt;/code&gt; file sends this as the &lt;code&gt;x-sentry-auth&lt;/code&gt; header. You&amp;#39;re just providing the value, not the header name.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Connect your OpenTelemetry app to Sentry&lt;/h2&gt;
&lt;p&gt;For this example, we&amp;#39;ll start with a sample book recommendation service that you can grab from our GitHub repo. It already has OpenTelemetry tracing instrumentation wrapped into it. &lt;strong&gt;You don&amp;#39;t need to change your instrumentation code.&lt;/strong&gt; Just point it at Sentry&amp;#39;s OTLP endpoint.&lt;/p&gt;
&lt;h3&gt;Clone the starter app&lt;/h3&gt;
&lt;p&gt;Run the following commands to clone the book recommendation app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/getsentry/otlp-tracing-sentry.git
cd otlp-tracing-sentry
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This app includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenTelemetry SDK (already configured)&lt;/li&gt;
&lt;li&gt;Custom tracing spans throughout the code&lt;/li&gt;
&lt;li&gt;Multi-level trace instrumentation (database queries, API calls, and parallel operations)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Configure Sentry as the OTLP destination&lt;/h3&gt;
&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now edit &lt;code&gt;.env&lt;/code&gt; and add your Sentry OTLP credentials from the previous step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://o{YOUR_ORG_ID}.ingest.us.sentry.io/api/{YOUR_PROJECT_ID}/integration/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_HEADERS=sentry sentry_key={YOUR_PUBLIC_KEY}
OTEL_SERVICE_NAME=book-recommendation-service
PORT=3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholder values with your actual Sentry credentials. The &lt;code&gt;OTEL_SERVICE_NAME&lt;/code&gt; will help you filter traces later in Sentry.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s it. You&amp;#39;ve just connected OpenTelemetry to Sentry with two lines of configuration.&lt;/p&gt;
&lt;h2&gt;Generate a trace and watch it appear in Sentry&lt;/h2&gt;
&lt;p&gt;Start the application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OpenTelemetry tracing initialized
Service: book-recommendation-service
Book Recommendation Service running on http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate a trace&lt;/h3&gt;
&lt;p&gt;In a new terminal window, send a request to create a book recommendation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://localhost:3000/recommend \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;userId&amp;quot;: &amp;quot;user123&amp;quot;}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ll get a JSON response with book recommendations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;userId&amp;quot;: &amp;quot;user123&amp;quot;,
  &amp;quot;userName&amp;quot;: &amp;quot;Alice Johnson&amp;quot;,
  &amp;quot;recommendations&amp;quot;: [
    {
      &amp;quot;bookId&amp;quot;: 201,
      &amp;quot;title&amp;quot;: &amp;quot;Project Hail Mary&amp;quot;,
      &amp;quot;score&amp;quot;: 0.95,
      &amp;quot;availability&amp;quot;: 6
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;View the trace in Sentry&lt;/h3&gt;
&lt;p&gt;Now let&amp;#39;s see what this looks like in the Sentry Traces view:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to your Sentry project.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Explore&lt;/strong&gt; in the left sidebar, then click &lt;strong&gt;Traces&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/send-your-existing-opentelemetry-traces/sentry-traces-explorer-span-samples-ml-service.jpg&quot; alt=&quot;Sentry Traces Explorer&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sentry Traces page showing span samples&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The page displays a list of span samples from your traces. Each row represents a span with its duration and description. Click on the &lt;strong&gt;Trace Samples&lt;/strong&gt; tab to switch to viewing complete traces.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/send-your-existing-opentelemetry-traces/sentry-trace-samples-post-recommend-waterfall.jpg&quot; alt=&quot;Sentry Trace Samples&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Trace Samples tab showing the expanded trace with all spans&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click on a trace to open the waterfall view. You&amp;#39;ll see a multi-level trace showing the complete request flow, including nested operations, parallel operations, and how long each one takes.&lt;/p&gt;
&lt;h3&gt;Explore span attributes&lt;/h3&gt;
&lt;p&gt;Click on any span in the waterfall to see its attributes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/send-your-existing-opentelemetry-traces/sentry-trace-waterfall-span-attributes-postgresql.jpg&quot; alt=&quot;Sentry Trace Waterfall with Span Attributes&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Trace waterfall view with span attributes panel&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The waterfall view makes it easier to see where your app spends its time, instead of guessing which async call wandered off on its own.&lt;/p&gt;
&lt;p&gt;For example, the &lt;code&gt;getUserProfile&lt;/code&gt; span includes attributes like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;action&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;SELECT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;category&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;db&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;db.operation&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;SELECT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;db.system&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;postgresql&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These attributes make your traces searchable. You can filter traces by user ID, database operations, or any custom attribute you add.&lt;/p&gt;
&lt;h2&gt;How the OpenTelemetry instrumentation works&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s look at how the app creates these traces. Here&amp;#39;s what&amp;#39;s happening behind the scenes so you can reuse the same patterns in your own app.&lt;/p&gt;
&lt;h3&gt;OpenTelemetry SDK initialization&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;instrument.js&lt;/code&gt; file sets up the OpenTelemetry SDK and configures the OTLP exporter:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;





// Configure the OTLP trace exporter
const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
  headers: {
    &amp;#39;x-sentry-auth&amp;#39;: process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS || &amp;#39;&amp;#39;,
  },
});

// Create SDK instance
const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: &amp;#39;book-recommendation-service&amp;#39;,
  }),
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are the key parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OTLPTraceExporter&lt;/code&gt; sends traces to Sentry&amp;#39;s OTLP endpoint.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NodeSDK&lt;/code&gt; initializes OpenTelemetry with automatic instrumentation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getNodeAutoInstrumentations()&lt;/code&gt; automatically traces HTTP requests, database calls, and other operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Custom spans&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;index.js&lt;/code&gt; file imports &lt;code&gt;instrument.js&lt;/code&gt; first, then creates custom spans for business operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

const tracer = trace.getTracer(&amp;#39;book-recommendation-service&amp;#39;, &amp;#39;1.0.0&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how we create a span for the database query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getUserProfile(userId) {
  return tracer.startActiveSpan(&amp;#39;getUserProfile&amp;#39;, async (span) =&amp;gt; {
    span.setAttribute(&amp;#39;db.system&amp;#39;, &amp;#39;postgresql&amp;#39;);
    span.setAttribute(&amp;#39;db.operation&amp;#39;, &amp;#39;SELECT&amp;#39;);
    span.setAttribute(&amp;#39;user.id&amp;#39;, userId);

    await delay(50);

    const profile = {
      userId,
      name: &amp;#39;Alice Johnson&amp;#39;,
      preferences: [&amp;#39;fiction&amp;#39;, &amp;#39;mystery&amp;#39;, &amp;#39;sci-fi&amp;#39;]
    };

    span.end();
    return profile;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;startActiveSpan&lt;/code&gt; method creates a new span and makes it the &amp;quot;active&amp;quot; span. Any child spans created inside this function automatically become children of this span.&lt;/p&gt;
&lt;h3&gt;Nested spans&lt;/h3&gt;
&lt;p&gt;We can create nested operations by starting new spans within a parent span:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function getReadingHistory(userId) {
  return tracer.startActiveSpan(&amp;#39;getReadingHistory&amp;#39;, async (span) =&amp;gt; {
    span.setAttribute(&amp;#39;db.system&amp;#39;, &amp;#39;postgresql&amp;#39;);
    span.setAttribute(&amp;#39;user.id&amp;#39;, userId);

    await delay(60);
    const history = [
      { bookId: 101, title: &amp;#39;The Great Gatsby&amp;#39;, rating: 5 },
      { bookId: 102, title: &amp;#39;1984&amp;#39;, rating: 4 },
      { bookId: 103, title: &amp;#39;To Kill a Mockingbird&amp;#39;, rating: 5 }
    ]; // Get data

    // Nested operation
    const filtered = await tracer.startActiveSpan(&amp;#39;filterRecentBooks&amp;#39;, async (childSpan) =&amp;gt; {
      childSpan.setAttribute(&amp;#39;books.count&amp;#39;, history.length);
      await delay(20);
      const recent = history.slice(0, 2);
      childSpan.end();
      return recent;
    });

    span.end();
    return filtered;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates the nested structure we saw in the Sentry waterfall view.&lt;/p&gt;
&lt;h3&gt;Parallel operations&lt;/h3&gt;
&lt;p&gt;For operations that run concurrently, use &lt;code&gt;Promise.all&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function checkBookAvailability(bookIds) {
  return tracer.startActiveSpan(&amp;#39;checkBookAvailability&amp;#39;, async (span) =&amp;gt; {

    const checks = await Promise.all([
      tracer.startActiveSpan(&amp;#39;checkWarehouse1&amp;#39;, async (s) =&amp;gt; {
        s.setAttribute(&amp;#39;warehouse.id&amp;#39;, &amp;#39;US-EAST-1&amp;#39;);
        await delay(40);
        s.end();
        return { warehouse: &amp;#39;US-EAST-1&amp;#39;, available: 2 };
      }),
      tracer.startActiveSpan(&amp;#39;checkWarehouse2&amp;#39;, async (s) =&amp;gt; {
        s.setAttribute(&amp;#39;warehouse.id&amp;#39;, &amp;#39;US-WEST-1&amp;#39;);
        await delay(45);
        s.end();
        return { warehouse: &amp;#39;US-WEST-1&amp;#39;, available: 3 };
      })
    ]);

    span.end();
    return checks;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sentry shows these spans running in parallel on the waterfall view, making it clear which operations we can optimize.&lt;/p&gt;
&lt;h2&gt;OTLP vs native Sentry SDK&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re already on OpenTelemetry, you can stay there until it makes sense to change it up.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re starting fresh and only using Sentry, use the native SDK — you&amp;#39;ll get more features and less config. Here&amp;#39;s how they differ in implementation.&lt;/p&gt;
&lt;h3&gt;Setup and configuration&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OTLP:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// instrument.js



const traceExporter = new OTLPTraceExporter({
  url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
  headers: {
    &amp;#39;x-sentry-auth&amp;#39;: process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS
  },
});

const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Native Sentry SDK:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// instrument.js


Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Creating spans&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OTLP:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
const tracer = trace.getTracer(&amp;#39;my-service&amp;#39;, &amp;#39;1.0.0&amp;#39;);

async function getUserProfile(userId) {
  return tracer.startActiveSpan(&amp;#39;getUserProfile&amp;#39;, async (span) =&amp;gt; {
    span.setAttribute(&amp;#39;user.id&amp;#39;, userId);
    // Your code here
    span.end();
    return result;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Native Sentry SDK:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

async function getUserProfile(userId) {
  return Sentry.startSpan(
    {
      op: &amp;#39;db.query&amp;#39;,
      name: &amp;#39;getUserProfile&amp;#39;,
      attributes: { &amp;#39;user.id&amp;#39;: userId },
    },
    async () =&amp;gt; {
      // Your code here
      return result;
    }
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With OTLP, you must manually call &lt;code&gt;span.end()&lt;/code&gt;. The native Sentry SDK automatically ends the span when the callback completes.&lt;/p&gt;
&lt;h3&gt;When to use OTLP&lt;/h3&gt;
&lt;p&gt;Use OpenTelemetry with OTLP if you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Already have OpenTelemetry instrumentation in your codebase&lt;/li&gt;
&lt;li&gt;Send traces to multiple observability backends&lt;/li&gt;
&lt;li&gt;Need vendor-neutral instrumentation&lt;/li&gt;
&lt;li&gt;Work with AI or LLM frameworks that use OpenTelemetry by default&lt;/li&gt;
&lt;li&gt;Use the OpenTelemetry Collector for processing traces&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;When to use native Sentry&lt;/h3&gt;
&lt;p&gt;Use the native Sentry SDK if you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Are starting from scratch without existing instrumentation&lt;/li&gt;
&lt;li&gt;Use Sentry as your only observability backend&lt;/li&gt;
&lt;li&gt;Need features that are currently limited in the OTLP beta (such as span events, full span link support, and searchable array attributes)&lt;/li&gt;
&lt;li&gt;Want automatic integration with &lt;a href=&quot;https://sentry.io/product/error-monitoring/&quot;&gt;Sentry error tracking&lt;/a&gt; and &lt;a href=&quot;https://sentry.io/product/session-replay/&quot;&gt;session replay&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Known limitations (open beta)&lt;/h2&gt;
&lt;p&gt;As of the time this was published, Sentry&amp;#39;s OTLP support is in open beta and a few things don&amp;#39;t work yet. Here&amp;#39;s what to watch out for.&lt;/p&gt;
&lt;h3&gt;Span events are dropped&lt;/h3&gt;
&lt;p&gt;OpenTelemetry span events are &lt;strong&gt;not supported&lt;/strong&gt;. If your instrumentation adds events to spans, they will be dropped during ingestion.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This event will be dropped
span.addEvent(&amp;#39;cache-miss&amp;#39;, { key: &amp;#39;user:123&amp;#39; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need to track events, use span attributes or create separate spans.&lt;/p&gt;
&lt;h3&gt;Span links have limited support&lt;/h3&gt;
&lt;p&gt;Span links are ingested and displayed in the trace view, but you cannot search, filter, or aggregate by them. You can see the links when viewing a trace, but they won&amp;#39;t appear in trace queries.&lt;/p&gt;
&lt;h3&gt;Array attributes have limited support&lt;/h3&gt;
&lt;p&gt;Array attributes work the same way as span links. Sentry ingests and displays them, but you cannot use them in search queries or aggregations.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// This array attribute will display but won&amp;#39;t be searchable
span.setAttribute(&amp;#39;book.genres&amp;#39;, [&amp;#39;fiction&amp;#39;, &amp;#39;mystery&amp;#39;, &amp;#39;sci-fi&amp;#39;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need searchable arrays, consider using separate attributes or joining the array into a string.&lt;/p&gt;
&lt;h2&gt;Further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/concepts/otlp/&quot;&gt;Sentry: OTLP documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/languages/js/&quot;&gt;OpenTelemetry: JavaScript SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/product/performance/&quot;&gt;Sentry: Tracing concepts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/&quot;&gt;OpenTelemetry: Semantic conventions&lt;/a&gt; for standardized attribute names&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sentry.io/cookbook/route-opentelemetry-traces-to-sentry/&quot;&gt;Sentry: OpenTelemetry traces cookbook&lt;/a&gt; for a hands-on Next.js walkthrough&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;OpenTelemetry traces FAQs&lt;/h2&gt;
&lt;h3&gt;Why aren&amp;#39;t my traces appearing in Sentry?&lt;/h3&gt;
&lt;p&gt;Verify the OTLP endpoint and headers in your &lt;code&gt;.env&lt;/code&gt; match the values from &lt;strong&gt;Settings &amp;gt; Client Keys (DSN) &amp;gt; OpenTelemetry (OTLP)&lt;/strong&gt;. Traces can take 30-60 seconds to appear after being sent. Check the console for &lt;code&gt;OpenTelemetry tracing initialized&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To verify the exporter is sending data, enable debug logging by adding this to instrument.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the &lt;a href=&quot;https://opentelemetry.io/docs/languages/js/getting-started/nodejs/#troubleshooting&quot;&gt;OpenTelemetry troubleshooting guide&lt;/a&gt; for more options.&lt;/p&gt;
&lt;h3&gt;Why are my spans appearing flat instead of nested?&lt;/h3&gt;
&lt;p&gt;You&amp;#39;re likely using &lt;code&gt;startSpan&lt;/code&gt; instead of &lt;code&gt;startActiveSpan&lt;/code&gt;. The active span becomes the parent for any child spans created within its scope.&lt;/p&gt;
&lt;h3&gt;How do I reduce memory usage from tracing?&lt;/h3&gt;
&lt;p&gt;Lower your sampling rate, in production, capturing 100% of traces is rarely necessary. Add these to your &lt;code&gt;.env&lt;/code&gt; to sample 10%:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OTEL_TRACES_SAMPLER=traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;How do I add more context to my spans?&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;span.setAttribute()&lt;/code&gt; to attach user IDs, request IDs, feature flags, or any other relevant data. The more context you add, the easier it is to filter and debug in Sentry.&lt;/p&gt;
&lt;h3&gt;How do I find slow operations in my app?&lt;/h3&gt;
&lt;p&gt;Add spans around suspected bottlenecks, then use Sentry&amp;#39;s waterfall view to see exactly where time is being spent.&lt;/p&gt;
&lt;h3&gt;Can I set up alerts on trace data?&lt;/h3&gt;
&lt;p&gt;Yes. Sentry alerts can notify you when traces exceed performance thresholds or show error patterns.&lt;/p&gt;
&lt;h3&gt;Can I trace requests across multiple services?&lt;/h3&gt;
&lt;p&gt;Yes. OpenTelemetry&amp;#39;s automatic context propagation handles this, no extra instrumentation needed.&lt;/p&gt;
</content:encoded></item><item><title>Logging in Next.js is hard (But it doesn&apos;t have to be)</title><link>https://blog.sentry.io/logging-in-next-js-is-hard-but-it-doesnt-have-to-be/</link><guid isPermaLink="true">https://blog.sentry.io/logging-in-next-js-is-hard-but-it-doesnt-have-to-be/</guid><description>Learn how to capture trace-connected logs across all Next.js runtimes (Edge, Node.js, and browser) using LogTape or the Sentry SDK.</description><pubDate>Mon, 30 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A typical &lt;a href=&quot;https://sentry.io/for/nextjs/&quot;&gt;Next.js&lt;/a&gt; deployment can execute code in up to three different runtimes: &lt;a href=&quot;https://vercel.com/docs/functions/runtimes/edge&quot;&gt;Edge&lt;/a&gt;, Node.js, and the browser.&lt;/p&gt;
&lt;p&gt;You may already be capturing logs from server-side code, but if you are not capturing the full request from middleware through server rendering to the browser, you are missing critical debugging information when problems arise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: A typical Next.js deployment can run in up to three environments: Node, Edge, and the browser. Most JavaScript logging libraries target Node; far fewer are compatible with Edge and the browser. LogTape and Sentry both provide runtime-agnostic logging in JavaScript.&lt;/p&gt;
&lt;h2&gt;Why logging in Next.js is hard&lt;/h2&gt;
&lt;h3&gt;Problem 1: Most Loggers Assume Node.js&lt;/h3&gt;
&lt;p&gt;Most loggers were built specifically for Node.js, relying on APIs like &lt;code&gt;AsyncLocalStorage&lt;/code&gt; or &lt;code&gt;fs&lt;/code&gt; that aren&amp;#39;t available in the browser or Edge runtimes.&lt;/p&gt;
&lt;p&gt;You&amp;#39;ll often see &lt;a href=&quot;https://blog.sentry.io/javascript-logging-library-definitive-guide/#pino-3&quot;&gt;Pino&lt;/a&gt; (or wrappers such as &lt;a href=&quot;https://github.com/sainsburys-tech/next-logger&quot;&gt;Next-Logger&lt;/a&gt;) suggested as the best logger for Next.js, but neither is actually a good choice for Next.js.&lt;/p&gt;
&lt;p&gt;Pino, and by extension Next-Logger, is designed for Node.js, and uses a polyfill to work in the browser. But that polyfill means surrendering the performance benefits the library has in Node, and you &lt;em&gt;still&lt;/em&gt; cannot capture logs in Edge functions or middleware (running on Edge).&lt;/p&gt;
&lt;h3&gt;Problem 2: Missing out on client-side logging&lt;/h3&gt;
&lt;p&gt;It&amp;#39;s easy to assume you don&amp;#39;t even need to capture client-side logs, because your &amp;quot;frontend code&amp;quot; is all server-side.&lt;/p&gt;
&lt;p&gt;By default, Next.js uses &lt;a href=&quot;https://nextjs.org/docs/app/getting-started/server-and-client-components&quot;&gt;Server Components&lt;/a&gt; for all pages and components. So by default, any logs emitted from your &amp;quot;frontend code&amp;quot; will actually be captured in your server-side logs.&lt;/p&gt;
&lt;p&gt;However, once you add a &lt;a href=&quot;https://nextjs.org/docs/app/getting-started/server-and-client-components#using-client-components&quot;&gt;&lt;code&gt;use client&lt;/code&gt; boundary&lt;/a&gt; for interactive components, &lt;em&gt;that&lt;/em&gt; code will be executed in the browser.&lt;/p&gt;
&lt;p&gt;Your &amp;quot;frontend code&amp;quot; is really a mix of &lt;a href=&quot;https://nextjs.org/docs/app/getting-started/server-and-client-components&quot;&gt;Server and Client Components&lt;/a&gt; that work together to render a single page and log to two different places.&lt;/p&gt;
&lt;p&gt;We have to solve that fragmentation and make sure all frontend code, no matter where it runs, is captured and logged to the same place.&lt;/p&gt;
&lt;h3&gt;Problem 3: Trace-connected structured logging&lt;/h3&gt;
&lt;p&gt;Logging is only one part of observability; on its own, it is most useful when you are debugging locally. In production, once you&amp;#39;re collecting logs from dozens, hundreds, or even thousands of requests, you need a way to tie related logs together for querying and aggregation.&lt;/p&gt;
&lt;p&gt;Tracing adds a unique ID to each request in your app and appends that ID as structured data to every log you send. Then later, you can query logs based on that ID to find every related log from that same request, along with other telemetry in your monitoring platform, such as &lt;a href=&quot;https://docs.sentry.io/product/#error-monitoring&quot;&gt;errors in Sentry&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Adding tracing to Next.js is actually easy, but it is still a step you have to take, and there are several ways to do it.&lt;/p&gt;
&lt;p&gt;We&amp;#39;re going to pair a JavaScript logging library with Sentry to instrument Next.js with trace-connected logs. In a future post, we&amp;#39;ll cover and compare another way to instrument Next.js with tracing, using OpenTelemetry.&lt;/p&gt;
&lt;h2&gt;What to look for in a Next.js logger&lt;/h2&gt;
&lt;p&gt;What should that logger do? I recently compared all of the current &lt;a href=&quot;https://blog.sentry.io/javascript-logging-library-definitive-guide/&quot;&gt;popular JavaScript logging libraries&lt;/a&gt; and broke down &lt;em&gt;why&lt;/em&gt; you should be using a logging library in the first place. The same holds mostly true for Next.js, but we need to be even more specific.&lt;/p&gt;
&lt;p&gt;When evaluating a logging solution for Next.js, consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Runtime Match:&lt;/strong&gt; Needs to run on Node, Browser, and Edge runtimes (if using Edge).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tracing Support:&lt;/strong&gt; Next.js apps are multi-service by default. Tracing connects logs from multiple sources under a single trace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production Features:&lt;/strong&gt; Filtering for data redaction and noise reduction; context management and child loggers to improve structured logging and make later querying and aggregation easier.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you might expect, feature-wise, libraries have been coalescing and imitating one another&amp;#39;s good practices, to the point where they have become similar.&lt;/p&gt;
&lt;p&gt;Still, the biggest difference to keep an eye out for is runtime support and performance.&lt;/p&gt;
&lt;p&gt;There are two practical fits that cover the full scope of a Next.js app, and they are not mutually exclusive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/&quot;&gt;LogTape&lt;/a&gt; with the &lt;a href=&quot;https://logtape.org/sinks/sentry&quot;&gt;Sentry sink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Sentry.logger&lt;/code&gt; with the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/&quot;&gt;Sentry Next.js SDK&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;LogTape&lt;/h3&gt;
&lt;p&gt;Mentioned in my &lt;a href=&quot;https://blog.sentry.io/javascript-logging-library-definitive-guide/#logtape-6&quot;&gt;other post&lt;/a&gt;, LogTape is one of the newest libraries on the scene and is quickly becoming a favorite dedicated logging library. It&amp;#39;s built from the ground up with no dependencies and runs natively in &lt;em&gt;all&lt;/em&gt; JavaScript runtimes.&lt;/p&gt;
&lt;p&gt;LogTape&amp;#39;s &lt;a href=&quot;https://logtape.org/manual/contexts&quot;&gt;context management&lt;/a&gt; and &lt;a href=&quot;https://logtape.org/manual/categories&quot;&gt;categories&lt;/a&gt; are especially useful for tagging and organizing your logs for more efficient querying later.&lt;/p&gt;
&lt;h3&gt;Configure categories and the Sentry sink&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;


await configure({
  sinks: {
    sentry: getSentrySink(),
  },
  loggers: [
    {
      category: [&amp;quot;next-app&amp;quot;],
      lowestLevel: &amp;quot;info&amp;quot;,
      sinks: [&amp;quot;sentry&amp;quot;],
    },
    {
      category: [&amp;quot;next-app&amp;quot;, &amp;quot;middleware&amp;quot;],
      lowestLevel: &amp;quot;info&amp;quot;,
      sinks: [&amp;quot;sentry&amp;quot;],
    },
    {
      category: [&amp;quot;next-app&amp;quot;, &amp;quot;client&amp;quot;],
      lowestLevel: &amp;quot;info&amp;quot;,
      sinks: [&amp;quot;sentry&amp;quot;],
    },
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each logger you define can be filtered in Sentry (for example &lt;code&gt;category: next-app.client&lt;/code&gt;) to fetch only the logs from a particular category.&lt;/p&gt;
&lt;h3&gt;Explicit context in a client component&lt;/h3&gt;
&lt;p&gt;You can also add &lt;a href=&quot;https://logtape.org/manual/contexts&quot;&gt;contexts&lt;/a&gt; to loggers to automatically append data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;quot;use client&amp;quot;;




const logger = getLogger([&amp;quot;next-app&amp;quot;, &amp;quot;client&amp;quot;]);


  // Automatically includes the orderId in all future logs from this component
  const ctx = useMemo(() =&amp;gt; logger.with({ orderId }), [orderId]);

  useEffect(() =&amp;gt; {
    const fromQuery = new URLSearchParams(window.location.search).get(&amp;quot;orderId&amp;quot;);
    if (fromQuery &amp;amp;&amp;amp; fromQuery !== orderId) {
      ctx.with({ fromQuery }).warn(
        &amp;quot;Confirmation orderId {orderId} does not match URL query {fromQuery}; check redirects and rewrites.&amp;quot;,
      );
    }
  }, [orderId, ctx]);

  return &amp;lt;p&amp;gt;Thanks for your order.&amp;lt;/p&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can read the full &lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/&quot;&gt;Structured Logging with LogTape&lt;/a&gt; post to get a deeper look at best practices for structured logging with LogTape and Sentry.&lt;/p&gt;
&lt;h3&gt;Sentry Next.js SDK&lt;/h3&gt;
&lt;p&gt;But, you might not need an additional logging library at all.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Sentry&amp;#39;s Next.js SDK&lt;/a&gt; includes &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;&lt;strong&gt;Sentry Logs&lt;/strong&gt;&lt;/a&gt;, a logging library built into Sentry&amp;#39;s SDKs for multiple platforms, not just JavaScript.&lt;/p&gt;
&lt;p&gt;Sentry&amp;#39;s &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/&quot;&gt;Logger for Next.js&lt;/a&gt; is also runtime-agnostic, providing logging everywhere your Next.js app can run. And if you are already using Sentry, or plan to use Sentry for capturing errors and tracing anyway, you can add logging without adding any new dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
});

Sentry.logger.info(&amp;quot;Checkout completed&amp;quot;, {
  orderId: order.id,
  userId: user.id,
  userTier: user.subscription,
  cartValue: cart.total,
  itemCount: cart.items.length,
  paymentMethod: &amp;quot;stripe&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/#scope-attributes&quot;&gt;Scopes&lt;/a&gt; and contexts work a little differently than in LogTape, but the functionality is similar.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;Sentry.withScope&lt;/code&gt; to set context data that will automatically be included on every log emitted inside the callback.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;quot;use client&amp;quot;;



function setGlobalAndIsolationScopes() {
  Sentry.getGlobalScope().setAttributes({ service: &amp;quot;checkout&amp;quot;, version: &amp;quot;2.1.0&amp;quot; });
  Sentry.getIsolationScope().setAttributes({ org_id: &amp;quot;org_demo_001&amp;quot;, user_tier: &amp;quot;pro&amp;quot; });
}

function calcShipping() {
  Sentry.logger.info(&amp;quot;calcShipping: rate lookup&amp;quot;, { carrier: &amp;quot;demo_carrier&amp;quot; });
  return 12.5;
}

function checkout() {
  const shipping = calcShipping();
  Sentry.logger.info(&amp;quot;checkout: shipping computed&amp;quot;, { shipping_usd: shipping });
}

function onCheckout() {
  setGlobalAndIsolationScopes();
  Sentry.withScope((scope) =&amp;gt; {
    scope.setAttribute(&amp;quot;checkout_id&amp;quot;, crypto.randomUUID());
    checkout(); // nested logs inherit global + isolation + checkout_id
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the Sentry Log Explorer, opening the &lt;code&gt;checkout: shipping computed&lt;/code&gt; entry shows the fields passed to that log call (&lt;code&gt;shipping_usd&lt;/code&gt; at &lt;code&gt;12.5&lt;/code&gt;) and the merged attributes applied to the same scope up till that point (&lt;code&gt;service&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;org_id&lt;/code&gt;, &lt;code&gt;user_tier&lt;/code&gt; and &lt;code&gt;checkout_id&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Getting trace-connected logs end to end&lt;/h2&gt;
&lt;p&gt;Finally, we want more than just messages from our logs. We want structured data with useful debugging information. When we see a log, we want to know where it came from, what triggered it, and what else happened as a part of that request.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;Tracing&lt;/a&gt; monitors the execution and timing of requests throughout an app. If there is a function or service that ends up causing slowdowns or even errors for users of the app, tracing data is how we collect information and ultimately discover the problem.&lt;/p&gt;
&lt;p&gt;When we configure Sentry, every request will be assigned a unique &amp;quot;Trace ID&amp;quot; that will link all data connected to that request together.&lt;/p&gt;
&lt;p&gt;Use &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Sentry&amp;#39;s setup wizard&lt;/a&gt; to automatically instrument your Next.js app with tracing and logs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @sentry/wizard@latest -i nextjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should end up with three files similar to the following.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: process.env.NODE_ENV === &amp;quot;development&amp;quot; ? 1.0 : 0.1,
  enableLogs: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;They&amp;#39;ll be named &lt;code&gt;instrumentation-client.ts&lt;/code&gt;, &lt;code&gt;sentry.server.config.ts&lt;/code&gt;, and &lt;code&gt;sentry.edge.config.ts&lt;/code&gt;. One entry point per runtime.&lt;/p&gt;
&lt;p&gt;Then use &lt;code&gt;Sentry.logger&lt;/code&gt; as above, or integrate with an existing logger, like LogTape.&lt;/p&gt;
&lt;p&gt;If your app was already instrumented with &lt;code&gt;console.log&lt;/code&gt;, you should try to upgrade to structured logging, but you can still &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/#console&quot;&gt;forward console output to Sentry&lt;/a&gt; with the &lt;code&gt;consoleLoggingIntegration&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s all you need to do. Every request will now contain a trace ID that will follow through the entire app, across runtimes, allowing you to query for all of the logs and traces related to that request.&lt;/p&gt;
&lt;h2&gt;Querying the logs&lt;/h2&gt;
&lt;p&gt;With logs throughout the application and connected with a trace ID, you can start querying for logs for debugging, custom dashboards, alerts, and more.&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;https://sentry.io/explore/logs/&quot;&gt;Explore &amp;gt; Logs&lt;/a&gt; you can search for logs based on any of the structured data properties.&lt;/p&gt;
&lt;p&gt;Sentry will automatically inject several useful attributes, like the &lt;code&gt;environment&lt;/code&gt;, which is showing us this was from the development server. In this log, there was also &lt;code&gt;browser&lt;/code&gt; attribute present which was automatically applied and shows us that this request came from Chrome. You&amp;#39;ll also notice the Chrome icon on the right. Server-side logs won&amp;#39;t contain either of these.&lt;/p&gt;
&lt;p&gt;If we wanted to filter the logs down to include only the logs from the browser, we could search &lt;code&gt;has: browser&lt;/code&gt; or &lt;code&gt;browser.name: Chrome&lt;/code&gt; if we wanted to see a specific browser.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s not uncommon to add a &lt;code&gt;service&lt;/code&gt; or &lt;code&gt;component&lt;/code&gt; attribute to logs, to make it more clear where a log was emitted from. You can use scopes with the Sentry logger or categories with LogTape to broadly append a queryable attribute like this to all logs in a stack.&lt;/p&gt;
&lt;p&gt;Read my post about &lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/&quot;&gt;structured logging&lt;/a&gt; to get a better idea about the type of data you might want to append to your logs.&lt;/p&gt;
&lt;p&gt;Every attribute shown here, including any additional data you append to the logs, is queryable. To see the other logs that were a part of this same request, we just need to click on the &lt;code&gt;trace&lt;/code&gt; ID.&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;Setting up a logger that captures the full surface area of your Next.js app is the first major step, but how you instrument your logs, and make use of that data is what really matters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement &lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/&quot;&gt;structured logs&lt;/a&gt; with a lot of high cardinality data.&lt;/li&gt;
&lt;li&gt;Add contextual data, like the name of the service or component that triggered the log.&lt;/li&gt;
&lt;li&gt;Audit your existing logs and start &lt;a href=&quot;https://sentry.io/cookbook/structured-logging-logtape/&quot;&gt;replacing old &lt;code&gt;console.log&lt;/code&gt; statements&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Learn more about &lt;a href=&quot;https://docs.sentry.io/product/explore/logs/&quot;&gt;how to query logs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;structured logs&lt;/a&gt;, packed with useful high-cardinality data to query, you (or your LLM) will be able to quickly debug new issues, as they come in. You can write queries yourself, and configure dashboards to visualize aggregate data.&lt;/p&gt;
&lt;p&gt;Try using &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;Sentry&amp;#39;s Seer AI&lt;/a&gt; to query logs with natural language. You can use the &lt;a href=&quot;https://docs.sentry.io/ai/mcp/&quot;&gt;Sentry MCP server&lt;/a&gt;, or click the &amp;quot;Ask Seer&amp;quot; button on the log explorer page. Rolling out now, you can even ask Seer to create custom dashboard widgets for you from your log data, or other data you might correlate with logs.&lt;/p&gt;
&lt;p&gt;Add logs now, cover all of your surfaces, and tomorrow&amp;#39;s bugs will be much more approachable.&lt;/p&gt;
</content:encoded></item><item><title>Next.js observability gaps and how to close them</title><link>https://blog.sentry.io/next-js-observability-gaps-how-to-close-them/</link><guid isPermaLink="true">https://blog.sentry.io/next-js-observability-gaps-how-to-close-them/</guid><description>Next.js hides errors across three runtimes. Here&apos;s how to close the gaps in hydration errors, server actions, ORM queries, and AI monitoring.</description><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;This blog is based on a recent live workshop. You can&lt;/em&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=J28NikORG90&quot;&gt;&lt;em&gt;watch the the full livestream&lt;/em&gt;&lt;/a&gt; &lt;em&gt;on Youtube.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Next.js&lt;/a&gt; gives you a lot for free; server-side rendering, file-based routing, edge runtimes. What it doesn’t give you is a clear picture of what’s actually happening in production. The framework’s three-runtime architecture (client, server, edge) means errors can surface in one layer while originating in another, database queries hide behind ORM abstractions, and server actions swallow useful error messages before they ever reach the browser.&lt;/p&gt;
&lt;p&gt;This post walks through a few specific observability gaps in Next.js apps, why they exist, and how to close them with Sentry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Next.js production builds strip error details from server actions. The client sees “An error occurred in a server component render” with zero context. Sentry captures the original server-side exception with full stack traces.&lt;/li&gt;
&lt;li&gt;Hydration errors are among the most common and least helpful errors in React. Sentry provides an HTML diff view that shows exactly which DOM nodes diverged between server and client renders.&lt;/li&gt;
&lt;li&gt;Logs and metrics aren&amp;#39;t sampled like traces. You get 100% of that data regardless of your &lt;code&gt;tracesSampleRate&lt;/code&gt;, so use them for anything that can&amp;#39;t afford gaps.&lt;/li&gt;
&lt;li&gt;Server actions don’t emit OpenTelemetry spans, so they need manual instrumentation with &lt;code&gt;withServerActionInstrumentation&lt;/code&gt; to appear in your traces.&lt;/li&gt;
&lt;li&gt;Database queries through ORMs like Drizzle are invisible to tracing by default. Adding an integration for your database client (like libSQL for Turso) surfaces every query as a span.&lt;/li&gt;
&lt;li&gt;AI agent monitoring using the Vercel AI SDK integration gives you per-model token usage, cost breakdowns, and tool call traces without leaving Sentry.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Three runtimes, three config files&lt;/h2&gt;
&lt;p&gt;Next.js runs code in different environments. Running the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Sentry wizard&lt;/a&gt; gets you started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx @sentry/wizard@latest -i nextjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The wizard creates separate initialization files for each: &lt;code&gt;instrumentation-client.ts&lt;/code&gt; for the browser, &lt;code&gt;sentry.server.config.ts&lt;/code&gt; for Node.js, and &lt;code&gt;sentry.edge.config.ts&lt;/code&gt; for edge runtimes.&lt;/p&gt;
&lt;p&gt;This generates configuration files for each runtime, a global error boundary (&lt;code&gt;global-error.tsx&lt;/code&gt;), and wraps your &lt;code&gt;next.config.ts&lt;/code&gt; with &lt;code&gt;withSentryConfig&lt;/code&gt;. The &lt;code&gt;next.config.ts&lt;/code&gt; wrapper handles source map uploads for readable stack traces and configures tunnel routing, which sends Sentry data through your own server to avoid ad blockers.&lt;/p&gt;
&lt;p&gt;A few things worth noting about the config:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sample rates matter.&lt;/strong&gt; Set &lt;code&gt;tracesSampleRate&lt;/code&gt; to &lt;code&gt;1.0&lt;/code&gt; in development, &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/&quot;&gt;10–20% in production&lt;/a&gt;. Going higher burns through quota fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sendDefaultPii&lt;/code&gt;&lt;/strong&gt; attaches user IP addresses to replays and events. Optional, but useful for correlating sessions to real users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edge config can differ.&lt;/strong&gt; If your middleware just reroutes requests, you can safely disable tracing in the edge config to reduce noise.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;One more thing about the setup&lt;/strong&gt;: call &lt;strong&gt;&lt;code&gt;Sentry.setUser()&lt;/code&gt;&lt;/strong&gt; once after authentication to propagate user context across errors, logs, traces, and replays.&lt;/p&gt;
&lt;h2&gt;Hydration errors: common and not very helpful&lt;/h2&gt;
&lt;p&gt;Hydration is the process where React attaches event handlers to server-rendered HTML, making it interactive. Hydration errors happen when the markup rendered by React on the client doesn’t match the initial server-rendered HTML, or when invalid HTML was sent by the server, and React couldn’t fix it.&lt;/p&gt;
&lt;p&gt;The classic cause: a theme toggle that reads from &lt;code&gt;localStorage&lt;/code&gt;. The server renders the light theme (it has no access to &lt;code&gt;localStorage&lt;/code&gt;), the client reads the stored dark theme preference, and React throws a hydration error because the HTML doesn’t match.&lt;/p&gt;
&lt;p&gt;In production, the browser gives you almost nothing useful. You get a minified React error pointing to a decoder URL, and a stack trace full of chunk files.&lt;/p&gt;
&lt;h3&gt;The HTML diff that actually helps&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/next-js-observability-gaps-how-to-close-them/inline-0.png&quot; alt=&quot;Sentry issue detail page showing a Hydration Error Diff with side-by-side server (red) and client (green) renders, followed by a Session Replay from user Sergiy Dybskiy on the nextjs-conf-scheduler project.&quot;&gt;&lt;/p&gt;
&lt;p&gt;To help you debug hydration errors, Sentry provides a diff tool that shows the differences between client-rendered and server-rendered HTML. If you have Session Replay enabled, Sentry will detect hydration errors and bring them into your issue stream.&lt;/p&gt;
&lt;p&gt;The diff shows before (server) and after (client) in a format that looks like a GitHub PR review, displaying a diff of the page before and after React has hydrated helps you find the element or attribute that caused the error. The easiest ones to spot are text content mismatches, incorrectly nested HTML elements, and attribute changes.&lt;/p&gt;
&lt;p&gt;If you’re already using Session Replay, you get automatic grouped hydration error issues for free. They’re generated from Replays, so they have no impact on your error quota.&lt;/p&gt;
&lt;p&gt;The fix for theme-related hydration errors is usually straightforward: defer the theme read to a &lt;code&gt;useEffect&lt;/code&gt; so the initial server and client renders match, then apply the stored preference after hydration completes.&lt;/p&gt;
&lt;h2&gt;Server actions are a tracing blind spot&lt;/h2&gt;
&lt;p&gt;Server actions are Next.js’s pattern for handling form submissions and mutations, essentially typed POST requests. Sentry automatically instruments most operations, but server actions require manual setup.&lt;/p&gt;
&lt;p&gt;The reason: server actions don’t emit OTel spans Sentry can hook into. Because of how Turbopack bundles them, auto-instrumentation is very hard and extremely error-prone. It would require building a Next.js server actions compiler, which is not something that seems reasonable to do.&lt;/p&gt;
&lt;p&gt;Without instrumentation, a server action shows up as an anonymous HTTP POST. With it, you get a named span, timing data, and (&lt;em&gt;critically&lt;/em&gt;) &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/distributed-tracing/&quot;&gt;distributed trace&lt;/a&gt; continuity between client and server.&lt;/p&gt;
&lt;h3&gt;Wrapping a server action&lt;/h3&gt;
&lt;p&gt;Wrap your server actions with &lt;code&gt;Sentry.withServerActionInstrumentation()&lt;/code&gt;. Here’s what that looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;&amp;quot;use server&amp;quot;;





  return Sentry.withServerActionInstrumentation(
    &amp;quot;login&amp;quot;, // Name that appears in Sentry traces
    {
      headers: await headers(), // Connects client and server traces
      formData,
      recordResponse: true,
    },
    async () =&amp;gt; {
      // Your actual login logic
      const result = await authenticateUser(formData);
      return result;
    },
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;withServerActionInstrumentation&lt;/code&gt; wrapper creates named spans for each action, captures timing and errors, connects client and server traces via headers, and attaches form data to Sentry events.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;headers&lt;/code&gt; parameter is what makes distributed tracing work. Sentry reads the trace ID and baggage from the request headers to stitch together the client-initiated trace with the server-side execution. Without it, you get two disconnected traces instead of one continuous picture.&lt;/p&gt;
&lt;h3&gt;Production error messages are useless (by design)&lt;/h3&gt;
&lt;p&gt;There’s another reason server action observability matters. In production builds, Next.js intentionally strips error details from server-side failures before they reach the client. What the user sees: “An error occurred in a server component render. The specific message is omitted in production builds to avoid leaking sensitive details”&lt;/p&gt;
&lt;p&gt;This is the right security decision. It’s also completely useless for debugging. But because Sentry instruments the server side directly, you still get the full exception &lt;code&gt;&amp;quot;Database connection lost during authentication&amp;quot;&lt;/code&gt; instead of the sanitized nothing. This alone justifies the setup cost if you’re using server actions for anything important.&lt;/p&gt;
&lt;h2&gt;Logs and metrics: choosing the right signal&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/&quot;&gt;Errors, logs, and metrics&lt;/a&gt; serve different purposes, and the distinction matters for how you instrument a Next.js app.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Errors&lt;/strong&gt; (&lt;code&gt;Sentry.captureException&lt;/code&gt;) — something is broken and needs fixing. Creates an issue, triggers alerts, feeds into &lt;a href=&quot;https://blog.sentry.io/seer-debug-with-ai-at-every-stage-of-development/&quot;&gt;Seer&lt;/a&gt; for root cause analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs&lt;/strong&gt; (&lt;code&gt;Sentry.logger&lt;/code&gt;) — contextual breadcrumbs. What happened before, during, and after a failure. High-cardinality, queryable, trace-connected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metrics&lt;/strong&gt; (&lt;code&gt;Sentry.metrics&lt;/code&gt;) — counters, durations, gauges. Good for dashboards and alerts on aggregate patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To enable logs, add &lt;code&gt;enableLogs: true&lt;/code&gt; to each of your Sentry init files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// instrumentation-client.ts, sentry.server.config.ts, sentry.edge.config.ts
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  tracesSampleRate: 0.1,
  enableLogs: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once enabled, &lt;code&gt;Sentry.logger&lt;/code&gt; sends structured logs from anywhere in your application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

Sentry.logger.info(&amp;quot;User added talk to schedule&amp;quot;, {
  userId: session.user.id,
  talkId: talk.id,
  action: &amp;quot;add_to_schedule&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;a href=&quot;https://blog.sentry.io/not-everything-that-breaks-is-an-error-a-logs-and-next-js-story/&quot;&gt;logs are trace-connected&lt;/a&gt;, when you open an issue in Sentry, you see every log emitted during that trace. You can also navigate to the Log Explorer, filter by any attribute (like &lt;code&gt;talkId&lt;/code&gt; or &lt;code&gt;userId&lt;/code&gt;), and build alerts or dashboards from the results.&lt;/p&gt;
&lt;p&gt;One important distinction: logs and metrics aren’t sampled. If your &lt;code&gt;tracesSampleRate&lt;/code&gt; is 10%, you’ll still get 100% of your logs and metric data points. Traces use statistical sampling and Sentry extrapolates aggregate numbers, but logs and metrics give you exact counts.&lt;/p&gt;
&lt;h2&gt;Database queries disappear behind your ORM&lt;/h2&gt;
&lt;p&gt;If you’re using an ORM like Drizzle with a database like Turso, your traces will show server actions and API routes, but the actual SQL queries inside them are invisible by default. You’ll see that a request took 850ms but not &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Fixing this requires two things: wiring up the database client integration and adding it to your Sentry server config.&lt;/p&gt;
&lt;p&gt;For a Turso (libSQL) database, add the &lt;code&gt;libsqlIntegration&lt;/code&gt; to your server config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// sentry.server.config.ts




Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1,
  integrations: [
    libsqlIntegration({ client }),
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll also need to add &lt;code&gt;@libsql/client&lt;/code&gt; to the &lt;code&gt;serverExternalPackages&lt;/code&gt; in your &lt;code&gt;next.config.ts&lt;/code&gt; so it bundles correctly.&lt;/p&gt;
&lt;p&gt;Once configured, every Drizzle query surfaces as a span with the actual SQL, even though you wrote your queries using Drizzle’s TypeScript API. Sentry translates the ORM calls into their SQL equivalents in the trace waterfall. This means you can use the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/&quot;&gt;Query Insights&lt;/a&gt; view to see operations per minute, average duration, and get automatic alerts for N+1 queries or slow database calls.&lt;/p&gt;
&lt;p&gt;The same pattern applies to other databases. For Postgres (including Neon), the Sentry Node SDK includes Postgres instrumentation by default, so you might not need any custom configuration. For Supabase, there’s a dedicated Supabase integration.&lt;/p&gt;
&lt;h2&gt;AI agent monitoring: tracing token spend back to users&lt;/h2&gt;
&lt;p&gt;If your Next.js app includes AI features (chat interfaces, agent workflows, generated content, etc.), you probably have a decent-sized bill from your model provider. What you probably don’t have is a breakdown of which features, which users, or which agent paths are responsible for that cost.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;vercelAIIntegration&lt;/code&gt; adds instrumentation for the AI SDK by Vercel to capture spans using the AI SDK’s built-in telemetry. This integration is enabled by default in the Node runtime, but not in the Edge runtime.&lt;/p&gt;
&lt;p&gt;For each AI function call, you can enable detailed telemetry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;


const result = await streamText({
  model: anthropic(&amp;quot;claude-sonnet-4-20250514&amp;quot;),
  prompt: userMessage,
  experimental_telemetry: {
    isEnabled: true,
    functionId: &amp;quot;search-agent&amp;quot;, // Shows up in Sentry as the span name
    recordInputs: true,
    recordOutputs: true,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Setting &lt;code&gt;functionId&lt;/code&gt; in &lt;code&gt;experimental_telemetry&lt;/code&gt; makes it easier to correlate captured spans with function calls. If you have multiple agents, say a router that delegates to a search agent and an info agent, each using different models, each gets its own named span in the trace.&lt;/p&gt;
&lt;p&gt;In Sentry’s &lt;a href=&quot;https://blog.sentry.io/sentrys-updated-agent-monitoring/&quot;&gt;Agent Monitoring view&lt;/a&gt;, you get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model cost breakdown&lt;/strong&gt; — which models you’re using, how much, and what it costs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Token usage&lt;/strong&gt; — input and output tokens per model, per request&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool call visibility&lt;/strong&gt; — every tool invocation, including errors, linked back to the triggering trace&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full trace context&lt;/strong&gt; — AI calls shown alongside database queries, API calls, and everything else in the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is the one that matters most. If an AI response takes five seconds, is it because the model is slow, or because the tool call triggered a slow database query? The trace waterfall shows you both in the same view, rather than requiring you to cross-reference your Anthropic dashboard with your application logs.&lt;/p&gt;
&lt;p&gt;Both &lt;code&gt;recordInputs&lt;/code&gt; and &lt;code&gt;recordOutputs&lt;/code&gt; default to true. Set these to false if your prompts or responses contain sensitive data you don’t want sent to Sentry.&lt;/p&gt;
&lt;h2&gt;Closing the gaps&lt;/h2&gt;
&lt;p&gt;A lot of Next.js observability problems look like missing data until you know what to look for. Anonymous POST requests that are actually server actions. 850ms responses with no explanation. Hydration errors pointing at minified decoder URLs. Once you&amp;#39;ve seen each one, the fix can be straightforward. But the first time they show up in production, they&amp;#39;re easy to lose hours on.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Three runtimes, three configs.&lt;/strong&gt; Next.js splits across client, server, and edge. Instrument all of them, but configure each appropriately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hydration errors need visual diffs.&lt;/strong&gt; The browser error message is useless in production. Sentry’s diff tool shows you the actual DOM divergence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server actions need manual wrapping.&lt;/strong&gt; No OTel spans means no auto-instrumentation. Use &lt;code&gt;withServerActionInstrumentation&lt;/code&gt; and pass headers for distributed tracing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs and metrics aren’t sampled.&lt;/strong&gt; Unlike traces, you get every single one. Use them for the data that can’t afford gaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ORM queries are invisible by default.&lt;/strong&gt; Add a database integration to see actual SQL in your traces and catch N+1 queries automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI monitoring connects cost to context.&lt;/strong&gt; Token spend is meaningless without knowing which users, features, and code paths generated it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Get started with the &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/&quot;&gt;Next.js SDK docs&lt;/a&gt;, or check out the &lt;a href=&quot;https://www.youtube.com/playlist?list=PLOwEowqdeNMqf3dArAE-Mo4rVR7T2Vanr&quot;&gt;debugging Next.js series on YouTube&lt;/a&gt; for more stuff like this.&lt;/p&gt;
</content:encoded></item><item><title>Seer fixes Seer: How Seer pointed us toward a bug and helped fix an outage</title><link>https://blog.sentry.io/seer-fixes-seer-debugging-agent/</link><guid isPermaLink="true">https://blog.sentry.io/seer-fixes-seer-debugging-agent/</guid><description>How Sentry&apos;s AI debugging tool Seer helped identify and fix a cascading region blocklist bug that caused an outage in Seer&apos;s own EU deployment.</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;On February 21, 2026, Sentry&amp;#39;s &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;AI-powered issue summarization&lt;/a&gt; experienced an outage in the EU region. Approximately 80-90% of requests to Seer&amp;#39;s Issue Summary API endpoint failed, disabling AI Summary cards on new issues and generating over 40,000 error events.&lt;/p&gt;
&lt;p&gt;The root cause traced back to an upstream incident: Google Cloud Platform declared unavailability for &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; in several EU regions. However, Sentry had provisioned throughput capacity in &lt;code&gt;europe-west1&lt;/code&gt; with guaranteed resources. The outage should have been minor—Sentry was only using 12% of provisioned capacity.&lt;/p&gt;
&lt;p&gt;The actual problem stemmed from application code, not infrastructure. A latency optimization feature blocklisted &lt;strong&gt;every&lt;/strong&gt; Gemini region in the EU, including the one with guaranteed capacity.&lt;/p&gt;
&lt;h2&gt;How Seer Routes LLM Calls in the EU&lt;/h2&gt;
&lt;p&gt;Seer executes &lt;code&gt;gemini-2.5-flash-lite&lt;/code&gt; through GCP Vertex AI. The EU deployment maintains provisioned throughput in &lt;code&gt;europe-west1&lt;/code&gt;, providing reserved capacity during demand spikes. Several other EU regions use Standard pay-as-you-go capacity without guaranteed availability.&lt;/p&gt;
&lt;p&gt;The LLM client implements a region fallback mechanism with temporary blocklisting: regions accumulating 6 failures within a short window are temporarily removed from rotation. This optimization reduces latency during Autofix sessions, which trigger 50-100 LLM calls.&lt;/p&gt;
&lt;p&gt;A critical invariant should exist: &lt;strong&gt;never blocklist provisioned throughput regions&lt;/strong&gt;. That capacity represents paid-for, guaranteed resources. Sentry enforced this rule in the US deployment but omitted it from the EU configuration.&lt;/p&gt;
&lt;h2&gt;The Cascade&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;europe-west1&lt;/code&gt; returned &lt;code&gt;504 Deadline Exceeded&lt;/code&gt; errors during the GCP incident, six failures triggered blocklisting. All traffic shifted to Standard PayGo regions unprepared for full load. &lt;code&gt;europe-west4&lt;/code&gt; returned &lt;code&gt;429 RESOURCE_EXHAUSTED&lt;/code&gt; and was blocklisted. Then &lt;code&gt;europe-central2&lt;/code&gt;. Within minutes, every EU region was blocklisted, and calls returned &lt;code&gt;LlmNoRegionsToRunError&lt;/code&gt;—no allowed regions remained.&lt;/p&gt;
&lt;p&gt;Critically, most calls to &lt;code&gt;europe-west1&lt;/code&gt; succeeded because provisioned throughput absorbed the load. The blocklist triggered on raw failure count regardless of success rate, enabling a region handling the vast majority of traffic to be banned for having six clustered failures.&lt;/p&gt;
&lt;h2&gt;The Code Problem&lt;/h2&gt;
&lt;p&gt;The original blocklist logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def should_blocklist(region: str, model: str, error_count: int) -&amp;gt; bool:
    return error_count &amp;gt;= BLOCKLIST_THRESHOLD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The required fix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def should_blocklist(region: str, model: str, error_count: int) -&amp;gt; bool:
    if is_provisioned_throughput_region(region, model):
        return False  # Never blocklist PT regions

    return error_count &amp;gt;= BLOCKLIST_THRESHOLD
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The US deployment hardcoded an exception for its PT region. When EU provisioned throughput was added after a previous incident, the blocklist code wasn&amp;#39;t updated. Configuration relied on developers remembering to maintain a separate, manually-updated allowlist—a classic gap between infrastructure provisioning and application awareness.&lt;/p&gt;
&lt;p&gt;A secondary issue: the blocklist threshold of 6 errors was hardcoded based on months-old load patterns. Sentry is replacing it with an error-rate-based approach.&lt;/p&gt;
&lt;h2&gt;Seer Debugging Seer&lt;/h2&gt;
&lt;p&gt;Sentry&amp;#39;s &lt;a href=&quot;https://sentry.io/product/seer/&quot;&gt;AI debugging tool&lt;/a&gt; proved essential for understanding the blast radius of its own outage. Standard monitoring detected the alert, but Seer&amp;#39;s analysis of the &lt;code&gt;LlmNoRegionsToRunError&lt;/code&gt; issue determined the impact in seconds.&lt;/p&gt;
&lt;p&gt;Seer identified that failed issue summaries caused &lt;del&gt;42,000 errors, with spam detection (&lt;/del&gt;1,600) and autofix (~850) also affected. It confirmed &amp;gt;99% of events occurred in the EU deployment and traced the blocklisting cascade through breadcrumb trails.&lt;/p&gt;
&lt;p&gt;The analysis reached the region blocklisting mechanism autonomously. Engineers, applying knowledge of provisioned throughput architecture, recognized that the PT region shouldn&amp;#39;t have been blocklisted. Seer confirmed calls to the PT region mostly succeeded during the GCP incident—the precise combination of facts needed to identify the fix.&lt;/p&gt;
&lt;h2&gt;The Lesson&lt;/h2&gt;
&lt;p&gt;Latency optimizations can create failure modes worse than having no optimization at all. Circuit breakers opening too aggressively, blocklists ignoring reserved capacity, or fallback chains amplifying failures can transform upstream provider incidents into complete service outages.&lt;/p&gt;
&lt;p&gt;The bug exploited a mundane gap: the distance between &amp;quot;we provisioned GCP capacity&amp;quot; and &amp;quot;our code knows we provisioned GCP capacity.&amp;quot; Organizations routing LLM requests across multiple regions should audit circuit breakers to ensure reserved regions receive special protection. This fix required six lines of code.&lt;/p&gt;
&lt;p&gt;For Seer&amp;#39;s analytical capabilities, consult the &lt;a href=&quot;https://docs.sentry.io/product/ai-in-sentry/seer/&quot;&gt;Seer documentation&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>You&apos;re probably overdue for a Sentry SDK upgrade</title><link>https://blog.sentry.io/overdue-for-a-sentry-sdk-upgrade/</link><guid isPermaLink="true">https://blog.sentry.io/overdue-for-a-sentry-sdk-upgrade/</guid><description>Half of all Sentry JS SDK installs are on v8 or older. If you&apos;re one of them, here&apos;s what you&apos;re missing, and how to close the gap without the pain.</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Session Replay. Structured logs. AI monitoring. Automatic OpenTelemetry tracing. Feature flag tracking. If you haven&amp;#39;t seen these in your Sentry dashboard, your SDK version is probably the reason.&lt;/p&gt;
&lt;p&gt;Whether you&amp;#39;re on &lt;code&gt;@sentry/react&lt;/code&gt;, &lt;code&gt;@sentry/nextjs&lt;/code&gt;, &lt;code&gt;@sentry/vue&lt;/code&gt;, &lt;code&gt;@sentry/angular&lt;/code&gt;, &lt;code&gt;@sentry/sveltekit&lt;/code&gt;, or any other &lt;code&gt;@sentry/*&lt;/code&gt; package, they all version together. When we say v10, we mean all of them.&lt;/p&gt;
&lt;p&gt;Here&amp;#39;s the thing: based on npm download numbers, roughly half of all Sentry JavaScript SDK installs are still on v8 or older.&lt;/p&gt;
&lt;h2&gt;The numbers&lt;/h2&gt;
&lt;p&gt;We pulled npm download stats for the major &lt;code&gt;@sentry/*&lt;/code&gt; packages. Here&amp;#39;s where weekly installs land as of March 2026:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Package&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Weekly total&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Still on v7&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;v7 + v8 combined&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@sentry/node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14.9M&lt;/td&gt;
&lt;td&gt;4.8M (32%)&lt;/td&gt;
&lt;td&gt;7.3M (49%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@sentry/browser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14.5M&lt;/td&gt;
&lt;td&gt;3.2M (22%)&lt;/td&gt;
&lt;td&gt;7.3M (50%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@sentry/react&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9.9M&lt;/td&gt;
&lt;td&gt;2.0M (20%)&lt;/td&gt;
&lt;td&gt;4.9M (49%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@sentry/nextjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3.7M&lt;/td&gt;
&lt;td&gt;524K (14%)&lt;/td&gt;
&lt;td&gt;1.5M (41%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@sentry/vue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.1M&lt;/td&gt;
&lt;td&gt;307K (28%)&lt;/td&gt;
&lt;td&gt;642K (59%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The pattern holds across every package: roughly half of all installs are two or more major versions behind current.&lt;/p&gt;
&lt;p&gt;If that&amp;#39;s you, this post is a map of what you&amp;#39;re missing and how to close the gap.&lt;/p&gt;
&lt;h2&gt;The SDK isn&amp;#39;t just an error catcher anymore&lt;/h2&gt;
&lt;p&gt;The Sentry SDK started as a crash reporter. Today it&amp;#39;s a full observability client: errors, performance traces, session replays, structured logs, cron monitors, user feedback, and AI agent monitoring. Each capability feeds context into the others. A replay shows you what the user did before the error. A trace shows you which microservice was slow. Logs give you the application-level &amp;quot;why.&amp;quot;&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re on an old version, you have the error. On the current version, you have the story.&lt;/p&gt;
&lt;h2&gt;What you&amp;#39;re missing&lt;/h2&gt;
&lt;h3&gt;Session Replay (v8+)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://sentry.io/product/session-replay/&quot;&gt;Session Replay&lt;/a&gt; captures what happened in the browser before, during, and after an error. It reconstructs the DOM, user clicks, navigation, and console output into a video-like playback. It&amp;#39;s privacy-aware by default: text and inputs are masked, and you control what gets captured.&lt;/p&gt;
&lt;p&gt;The key part: replays link directly to errors and traces. When you&amp;#39;re looking at a bug report, you can watch the user reproduce it. No more having to ask users what happened. No more waiting for them to get back to you. No more guessing what &amp;quot;it doesn&amp;#39;t work&amp;quot; means.&lt;/p&gt;
&lt;p&gt;Available in &lt;code&gt;@sentry/browser&lt;/code&gt;, &lt;code&gt;@sentry/react&lt;/code&gt;, &lt;code&gt;@sentry/vue&lt;/code&gt;, &lt;code&gt;@sentry/angular&lt;/code&gt;, and &lt;code&gt;@sentry/svelte&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Structured Logs (v9+)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Sentry.logger.info()&lt;/code&gt;, &lt;code&gt;Sentry.logger.error()&lt;/code&gt;, and four more severity levels, with structured attributes that link to traces and errors.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.logger.error(&amp;#39;Payment processing failed&amp;#39;, {
  orderId: &amp;#39;order-123&amp;#39;,
  amount: 99.99,
  gateway: &amp;#39;stripe&amp;#39;,
  retryCount: 3,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These aren&amp;#39;t &lt;code&gt;console.log&lt;/code&gt; replacements floating in CloudWatch or Datadog. They&amp;#39;re &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;logs&lt;/a&gt; that show up in the same Sentry issue, linked to the trace that was active when they fired.&lt;/p&gt;
&lt;p&gt;The Logs API also supports template strings that Sentry can group and search:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.logger.info(Sentry.logger.fmt`User ${userId} completed checkout for order ${orderId}`, {
  amount: 99.99,
  paymentMethod: &amp;#39;credit_card&amp;#39;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;AI Monitoring (v9+/v10+)&lt;/h3&gt;
&lt;p&gt;If you&amp;#39;re calling LLMs from your backend, the SDK can instrument those calls automatically. OpenAI and LangChain support landed in v9. Anthropic and Vercel AI SDK support followed in v10. With &lt;a href=&quot;https://sentry.io/solutions/ai-observability/&quot;&gt;AI monitoring&lt;/a&gt;, you get token usage, latency, and error tracking for every call.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.init({
  dsn: &amp;#39;__DSN__&amp;#39;,
  integrations: [
    Sentry.openAIIntegration(),       // OpenAI
    Sentry.anthropicAIIntegration(),  // Anthropic/Claude
    Sentry.vercelAIIntegration(),     // Vercel AI SDK
    Sentry.langChainIntegration(),    // LangChain
  ],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;OpenTelemetry Tracing (v8+)&lt;/h3&gt;
&lt;p&gt;v8 rebuilt performance monitoring on OpenTelemetry. The old mental model treated &amp;quot;transactions&amp;quot; and &amp;quot;spans&amp;quot; as separate concepts with manual lifecycle management. The new model: everything is a span, and the lifecycle is automatic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// v7: manual transaction management
const transaction = Sentry.startTransaction({ name: &amp;#39;checkout&amp;#39; });
const span = transaction.startChild({ op: &amp;#39;db.query&amp;#39; });
// ... do work ...
span.finish();
transaction.finish();

// v8+: just wrap your code
const result = Sentry.startSpan({ name: &amp;#39;checkout&amp;#39;, op: &amp;#39;db.query&amp;#39; }, () =&amp;gt; {
  return db.query(&amp;#39;SELECT ...&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&amp;#39;s less code, but it&amp;#39;s also a different relationship with instrumentation. You don&amp;#39;t manage span lifecycles. You describe what you&amp;#39;re measuring, and the SDK handles the rest. Nested spans work automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.startSpan({ name: &amp;#39;checkout&amp;#39; }, () =&amp;gt; {
  Sentry.startSpan({ name: &amp;#39;validate-cart&amp;#39;, op: &amp;#39;function&amp;#39; }, () =&amp;gt; {
    // automatically a child of &amp;#39;checkout&amp;#39;
    validateCart();
  });
  Sentry.startSpan({ name: &amp;#39;charge-card&amp;#39;, op: &amp;#39;db.query&amp;#39; }, () =&amp;gt; {
    // also a child of &amp;#39;checkout&amp;#39;
    chargeCard();
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Node.js, Express, Fastify, Hapi, Postgres, MongoDB, Redis, Prisma, GraphQL, MySQL, and Mongoose are all auto-instrumented with zero manual setup. Just Sentry.init().&lt;/p&gt;
&lt;h2&gt;What changed under the hood&lt;/h2&gt;
&lt;p&gt;The features above are the headline reasons to upgrade. Here&amp;#39;s the compressed version of what changed structurally at each major version.&lt;/p&gt;
&lt;h4&gt;v8: Package consolidation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@sentry/tracing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/hub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/integrations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/replay&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These were all merged into the core SDKs. Integrations became functions (&lt;code&gt;new BrowserTracing()&lt;/code&gt; became &lt;code&gt;browserTracingIntegration()&lt;/code&gt;) for better tree-shaking. User Feedback widget, Cron Monitoring, and FID collection shipped. Angular v14+ became required. The new scope model (&lt;code&gt;getCurrentScope()&lt;/code&gt;, &lt;code&gt;getIsolationScope()&lt;/code&gt;, &lt;code&gt;getGlobalScope()&lt;/code&gt;) was introduced, deprecating &lt;code&gt;Hub&lt;/code&gt;, &lt;code&gt;getCurrentHub()&lt;/code&gt;, and &lt;code&gt;configureScope()&lt;/code&gt; with console warnings. This is the single biggest source of breaking-change noise when upgrading from v7.&lt;/p&gt;
&lt;h4&gt;v9: Deprecated API removal&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Hub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getCurrentHub()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configureScope()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The deprecated scope APIs were deleted. &lt;code&gt;@sentry/utils&lt;/code&gt; merged into &lt;code&gt;@sentry/core&lt;/code&gt;, &lt;code&gt;@sentry/types&lt;/code&gt; deprecated. ES2020 became the baseline. Feature flag tracking arrived with built-in LaunchDarkly and OpenFeature support. Node.js 18 became the minimum.&lt;/p&gt;
&lt;h4&gt;v10: OpenTelemetry v2&lt;/h4&gt;
&lt;p&gt;The underlying OpenTelemetry dependencies upgraded to v2.x. FID collection removed in favor of INP (Interaction to Next Paint), the metric Google actually uses for Core Web Vitals. &lt;code&gt;@sentry/node-core&lt;/code&gt; shipped a lightweight mode for teams that want error tracking, logs, and metrics without full OpenTelemetry instrumentation. Next.js Turbopack support landed.&lt;/p&gt;
&lt;h3&gt;Quick reference: features by version&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Minimum version&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Packages&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Docs&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Cron Monitoring&lt;/td&gt;
&lt;td&gt;v7+&lt;/td&gt;
&lt;td&gt;node, all server SDKs&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/node/crons/&quot;&gt;&lt;u&gt;Set Up Crons&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Replay&lt;/td&gt;
&lt;td&gt;v8+&lt;/td&gt;
&lt;td&gt;browser, react, vue, angular, svelte&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/session-replay/&quot;&gt;&lt;u&gt;Set Up Session Replay&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Feedback Widget&lt;/td&gt;
&lt;td&gt;v8+&lt;/td&gt;
&lt;td&gt;browser, all frontend SDKs&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/user-feedback/&quot;&gt;&lt;u&gt;Set Up User Feedback&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured Logs&lt;/td&gt;
&lt;td&gt;v9+&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/logs/&quot;&gt;&lt;u&gt;Set Up Logs&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feature Flag Tracking&lt;/td&gt;
&lt;td&gt;v9+&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/feature-flags/&quot;&gt;&lt;u&gt;Set Up Feature Flags&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Monitoring (OpenAI, LangChain)&lt;/td&gt;
&lt;td&gt;v9+&lt;/td&gt;
&lt;td&gt;node&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/ai-agent-monitoring/&quot;&gt;&lt;u&gt;Set Up AI Agent Monitoring&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Monitoring (Anthropic, Vercel AI)&lt;/td&gt;
&lt;td&gt;v10+&lt;/td&gt;
&lt;td&gt;node&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/ai-agent-monitoring/&quot;&gt;&lt;u&gt;Set Up AI Agent Monitoring&lt;/u&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INP (as sole Core Web Vital)&lt;/td&gt;
&lt;td&gt;v10+&lt;/td&gt;
&lt;td&gt;browser, all frontend SDKs&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Deprecated packages you might still have&lt;/h3&gt;
&lt;p&gt;If you see any of these in your lockfile, you&amp;#39;re at least two major versions behind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@sentry/tracing&lt;/code&gt; (merged into core SDKs in v8)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/hub&lt;/code&gt; (merged into &lt;code&gt;@sentry/core&lt;/code&gt; in v8)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/integrations&lt;/code&gt; (merged into core SDKs in v8)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/replay&lt;/code&gt; (merged into &lt;code&gt;@sentry/browser&lt;/code&gt; in v8)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/types&lt;/code&gt; (deprecated in v9, use &lt;code&gt;@sentry/core&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@sentry/utils&lt;/code&gt; (deprecated in v9, use &lt;code&gt;@sentry/core&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These packages still resolve on npm, so they won&amp;#39;t break your install. But they&amp;#39;re unmaintained and they add dead weight to your node_modules. If you see them, your Sentry setup needs attention.&lt;/p&gt;
&lt;h2&gt;Security and performance&lt;/h2&gt;
&lt;p&gt;Even if you don&amp;#39;t care about new features, staying current keeps you patched and lean.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CVE-2023-46729:&lt;/strong&gt; SSRF risk via insufficient validation of the Next.js tunnel route. Patched in v7.77.0, but only users on a current v7.x got the fix. If you were pinned to an older v7 release, you stayed vulnerable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP address inference removed by default.&lt;/strong&gt; Starting in v9 (fully enforced in v10.4.0), the SDK no longer instructs the Sentry backend to infer user IP addresses unless you set &lt;code&gt;sendDefaultPii: true&lt;/code&gt;. Privacy-by-default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fetchProxyScriptNonce&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;removed in v9.&lt;/strong&gt; The SvelteKit option was dropped due to security concerns around CSP bypass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transitive dependency CVE patches&lt;/strong&gt; (fast-xml-parser, rollup, tar, nuxt) only land on the current major version line. If you&amp;#39;re pinned to v7, you&amp;#39;re not getting these fixes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@sentry/browser&lt;/code&gt; &lt;strong&gt;base bundle:&lt;/strong&gt; 26 KB gzipped (v10). Tree-shaking flags can bring it down to ~24.5 KB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ES5 polyfills dropped in v8, ES2020 baseline in v9.&lt;/strong&gt; Smaller transpiled output for the vast majority of environments that support these natively.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;6 legacy packages removed in v8, 2 more deprecated in v9.&lt;/strong&gt; Simpler dependency graph, less duplication in your &lt;code&gt;node_modules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replay bundle reduced by ~20 KB&lt;/strong&gt; via tree-shaking improvements (v7.73.0+).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Let your AI assistant handle it&lt;/h2&gt;
&lt;p&gt;You don&amp;#39;t have to upgrade or configure Sentry by hand. Sentry publishes agent skills: instruction sets that teach AI coding assistants how to work with Sentry in your project. They work with Claude Code, Cursor, GitHub Copilot, OpenAI Codex, and more.&lt;/p&gt;
&lt;p&gt;The newest skill, &lt;code&gt;sentry-sdk-upgrade&lt;/code&gt;, can handle the entire migration for you. It runs a 4-phase workflow: &lt;strong&gt;Detect&lt;/strong&gt; (reads your &lt;code&gt;package.json&lt;/code&gt;, finds Sentry configs, greps for deprecated patterns) → &lt;strong&gt;Recommend&lt;/strong&gt; (categorizes changes as auto-fixable, AI-assisted, or manual-review) → &lt;strong&gt;Guide&lt;/strong&gt; (applies changes file by file with explanations) → &lt;strong&gt;Cross-Link&lt;/strong&gt; (verifies the build passes and suggests new features to enable). It covers v7→v8, v8→v9, and v9→v10.&lt;/p&gt;
&lt;p&gt;Install the skills with one command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx skills add getsentry/sentry-for-ai --skill sentry-sdk-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then ask your assistant to do the work:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;What to say&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;What happens&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&amp;quot;Upgrade my Sentry SDK to v10&amp;quot;&lt;/td&gt;
&lt;td&gt;Detects your version, scans for deprecated APIs, migrates code file by file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Add Sentry to my React app&amp;quot;&lt;/td&gt;
&lt;td&gt;Sets up &lt;code&gt;@sentry/react&lt;/code&gt; with error boundaries and routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Enable Sentry logging&amp;quot;&lt;/td&gt;
&lt;td&gt;Configures Structured Logs in your &lt;code&gt;Sentry.init()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Monitor my OpenAI calls&amp;quot;&lt;/td&gt;
&lt;td&gt;Adds &lt;code&gt;openAIIntegration()&lt;/code&gt; with token tracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Add performance monitoring&amp;quot;&lt;/td&gt;
&lt;td&gt;Configures tracing with the right integrations for your framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&amp;quot;Fix the recent Sentry errors&amp;quot;&lt;/td&gt;
&lt;td&gt;Pulls issues from Sentry and applies fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The upgrade skill is especially useful for the v7→v8 jump, where the number of API changes is large but most of them are mechanical renames. Your assistant can also wire up new features like Session Replay, Logs, or AI monitoring after the upgrade without you looking up the config. Skills are versioned and can be committed to your repo with dotagents so every team member gets the same setup.&lt;/p&gt;
&lt;h2&gt;&amp;quot;But upgrading is painful&amp;quot;&lt;/h2&gt;
&lt;p&gt;Let&amp;#39;s be honest about the effort, then talk about the tooling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;v7 to v8 is the biggest jump.&lt;/strong&gt; The performance monitoring API was rewritten on OpenTelemetry. Tracing concepts changed (transactions became spans), import order matters for Node.js auto-instrumentation, and 6 packages were consolidated. Expect a few hours for a typical app, more for complex setups with custom instrumentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;v8 to v9 is moderate.&lt;/strong&gt; Deprecated APIs were removed: &lt;code&gt;Hub&lt;/code&gt;, &lt;code&gt;getCurrentHub()&lt;/code&gt;, &lt;code&gt;configureScope()&lt;/code&gt;, and others. If you fixed the deprecation warnings that v8 printed, this is straightforward. The hard gate is Node.js 18 minimum.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;v9 to v10 is genuinely easy.&lt;/strong&gt; 8 breaking changes, mostly internal. The CHANGELOG itself says &amp;quot;minimal breaking changes.&amp;quot; OpenTelemetry v2 under the hood, FID removed (INP replaces it), and a handful of internal API cleanups.&lt;/p&gt;
&lt;p&gt;Each jump gives you warning first. APIs are deprecated with console warnings for a full major version before they&amp;#39;re removed. You don&amp;#39;t get surprised.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sentry-sdk-upgrade&lt;/code&gt; agent skill can automate much of this. It detects deprecated patterns with grep, applies mechanical renames automatically, and walks through complex changes with explanations. For the v7→v8 jump especially — where you&amp;#39;d otherwise be manually renaming dozens of integration constructors and scope APIs — the skill handles the tedious parts so you can focus on the genuinely tricky changes like custom instrumentation rewrites.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Migration guides with before/after code for every breaking change:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v7-to-v8/&quot;&gt;v7 to v8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v8-to-v9/&quot;&gt;v8 to v9&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v9-to-v10/&quot;&gt;v9 to v10&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The practical upgrade path&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check where you stand.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check your version now
npm ls @sentry/react @sentry/nextjs @sentry/vue @sentry/angular @sentry/sveltekit @sentry/node 2&amp;gt;/dev/null | grep @sentry
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Point your AI assistant at it, or read the migration guide.&lt;/strong&gt; If you have &lt;a href=&quot;https://docs.sentry.io/ai/agent-skills/&quot;&gt;Sentry agent skills&lt;/a&gt; installed, tell your assistant &amp;quot;upgrade my Sentry SDK to v10&amp;quot; — it&amp;#39;ll detect your version, scan for deprecated APIs, and walk through the migration file by file. Otherwise, read the migration guide for your jump on &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/&quot;&gt;docs.sentry.io&lt;/a&gt; (&lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v7-to-v8/&quot;&gt;v7 to v8&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v8-to-v9/&quot;&gt;v8 to v9&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration/v9-to-v10/&quot;&gt;v9 to v10&lt;/a&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you&amp;#39;re more than 2 major versions behind, upgrade one version at a time.&lt;/strong&gt; Going v7 to v10 in one PR is a recipe for confusing errors. Go v7 to v8, verify, then v8 to v9, and so on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with the core SDK, then enable new features incrementally.&lt;/strong&gt; Get the base upgrade working first. Then add &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/session-replay/&quot;&gt;Session Replay&lt;/a&gt;. Then &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/logs/&quot;&gt;Logs&lt;/a&gt;. Each one is an independent &lt;code&gt;init()&lt;/code&gt; option or integration, so you don&amp;#39;t have to adopt everything at once. Or install &lt;a href=&quot;https://docs.sentry.io/ai/agent-skills/&quot;&gt;agent skills&lt;/a&gt; and let your AI coding assistant configure them for you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use debug: true during migration.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.init({
  dsn: &amp;#39;__DSN__&amp;#39;,
  debug: true, // Logs SDK decisions to the console
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This surfaces configuration issues, dropped events, and integration problems immediately.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Close the gap&lt;/h2&gt;
&lt;p&gt;The SDK team ships weekly. Every release you skip adds to the distance between where you are and what&amp;#39;s available. With 4.8 million weekly &lt;code&gt;@sentry/node&lt;/code&gt; installs still on v7, we know this isn&amp;#39;t a small problem. That&amp;#39;s why we&amp;#39;ve invested heavily in migration guides and agent skills — including the &lt;code&gt;sentry-sdk-upgrade&lt;/code&gt; skill that can handle the migration for you — to make the path forward clear.&lt;/p&gt;
&lt;p&gt;Pick one version jump. Read the migration guide. Close the gap.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Check your version now
npm ls @sentry/react @sentry/nextjs @sentry/vue @sentry/angular @sentry/sveltekit @sentry/node 2&amp;gt;/dev/null | grep @sentry
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Migration guides: &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/migration&quot;&gt;docs.sentry.io/platforms/javascript/migration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Changelog: &lt;a href=&quot;https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md&quot;&gt;github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Agent skills: &lt;a href=&quot;https://docs.sentry.io/ai/agent-skills&quot;&gt;docs.sentry.io/ai/agent-skills&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Fair Source Software in the AI age</title><link>https://blog.sentry.io/fair-source-software-in-the-ai-age/</link><guid isPermaLink="true">https://blog.sentry.io/fair-source-software-in-the-ai-age/</guid><description>How generative AI is disrupting software licensing models and why Fair Source software remains viable in the AI era.</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you noticed AI recently? Yeah, us too. Generative AI is wreaking havoc on the software status quo, and that includes licensing, and that generates … opinions.&lt;/p&gt;
&lt;p&gt;Sentry has &lt;a href=&quot;https://blog.sentry.io/introducing-the-functional-source-license-freedom-without-free-riding/&quot;&gt;a long history of having opinions about software licensing&lt;/a&gt;. We started life as an &lt;a href=&quot;https://github.com/getsentry/sentry/commit/3c2e87573d3bd16f61cf08fece0638cc47a4fc22&quot;&gt;unlicensed side project&lt;/a&gt; in 2008, then went through &lt;a href=&quot;https://github.com/getsentry/sentry/commits/583bcd85e07eeb42d6767761afdc95ece78cc6e9/LICENSE&quot;&gt;BSD&lt;/a&gt;, to &lt;a href=&quot;https://blog.sentry.io/relicensing-sentry/&quot;&gt;BSL&lt;/a&gt;, to writing our own license, &lt;a href=&quot;https://blog.sentry.io/introducing-the-functional-source-license-freedom-without-free-riding/&quot;&gt;FSL&lt;/a&gt;. Most recently, in 2024, we &lt;a href=&quot;https://blog.sentry.io/sentry-is-now-fair-source/&quot;&gt;launched Fair Source&lt;/a&gt; to carve out an industry niche for the best of source-available licensing (including FSL): simple non-compete, eventually Open Source. &lt;a href=&quot;https://fss.cool/&quot;&gt;Fair Source adoption is growing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So what&amp;#39;s going on with AI? How does it impact software licensing? Specifically, does Fair Source still work as intended? Is it still a safe option for your company? Spoiler alert: yes. Let&amp;#39;s dive in.&lt;/p&gt;
&lt;h2&gt;The new AI moment&lt;/h2&gt;
&lt;p&gt;The software industry has taken a huge leap forward. As &lt;a href=&quot;https://x.com/karpathy/status/2026731645169185220&quot;&gt;Andrej Karpathy put it&lt;/a&gt;, this shift happened &amp;quot;not gradually and over time in the &amp;#39;progress as usual&amp;#39; way, but specifically this last December.&amp;quot; The last round of AI models in 2025 (Opus 4.5 on November 24, Codex 5.2 on December 11) were the first ones good enough to depend on as standalone agents in a harness such as Claude Code or OpenCode, rather than as a glorified autocomplete within traditional IDEs like VS Code or Cursor.&lt;/p&gt;
&lt;p&gt;On top of that, OpenClaw, the open-source AI personal assistant, &lt;a href=&quot;https://openpath.quest/2026/fueling-open-source-with-vibes-and-money/#vibe-coding-rocket-fuel&quot;&gt;exploded in popularity&lt;/a&gt;, demonstrating both the viability of vibe-coding (&amp;quot;&lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/the-creator-of-clawd-i-ship-code&quot;&gt;I ship code I don&amp;#39;t read&lt;/a&gt;&amp;quot;), and the overwhelming demand for agents to do more than just write code.&lt;/p&gt;
&lt;p&gt;This Cambrian explosion raises questions across the software industry and more broadly in society. As far as licensing goes, what is the status quo that AI is upending?&lt;/p&gt;
&lt;h2&gt;Standard model&lt;/h2&gt;
&lt;p&gt;Since the 1970s, the international community has considered software to be &amp;quot;literary works for copyright purposes&amp;quot; (&lt;a href=&quot;https://www.wipo.int/en/web/copyright/faq-copyright#accordion__collapse__10&quot;&gt;WIPO FAQ&lt;/a&gt;). This forms the basis for what we might call the &lt;em&gt;standard model&lt;/em&gt; of software licensing: a human writes software, and the law automatically recognizes their copyright. The author is then free to give permission to others to use the software, modify it, distribute it, and so forth. The legal instrument for this is a license agreement.&lt;/p&gt;
&lt;p&gt;A small subset of license agreements meet the criteria of the &lt;a href=&quot;https://opensource.org/osd&quot;&gt;Open Source Definition&lt;/a&gt; (OSD), a document maintained by the &lt;a href=&quot;https://opensource.org/&quot;&gt;Open Source Initiative&lt;/a&gt; (OSI) since 1998. (OSI does not have a legal trademark on the term &amp;quot;Open Source,&amp;quot; but they do have &lt;a href=&quot;https://openpath.quest/2025/osi-really-did-initiate-open-source/&quot;&gt;a clear socio-historical claim on it&lt;/a&gt;.) Software under these licenses is Open Source software (OSS).&lt;/p&gt;
&lt;p&gt;Another set of licenses meet the criteria of the &lt;a href=&quot;https://fair.io/about/&quot;&gt;Fair Source Definition&lt;/a&gt; (FSD), a document we wrote in 2023 to launch &lt;a href=&quot;https://fair.io/&quot;&gt;Fair Source&lt;/a&gt;, a movement complementary to Open Source that encourages companies to safely share their core software products. Software under these licenses is Fair Source software (FSS).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Open Source&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Fair Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;read, run, modify, distribute&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;read, run, modify, distribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;simple non-compete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;eventually Open Source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For completeness, &lt;a href=&quot;https://www.microsoft.com/en-us/servicesagreement#8_softwareLicense&quot;&gt;Microsoft&amp;#39;s Software License&lt;/a&gt; is an example of a license that fits neither OSD nor FSD, so the software they release under it is neither OSS nor FSS.&lt;/p&gt;
&lt;p&gt;In practice, most companies are careful to choose the right license for their goals, and to respect the licenses of others. For example, at Sentry, we have an extensive internal policy on software licensing. We use &lt;a href=&quot;https://fossa.com/&quot;&gt;FOSSA&lt;/a&gt; to help us manage our compliance with licenses of software we consume. Of course, we also go above and beyond the license terms of the OSS we consume, proactively funding its maintainers &lt;a href=&quot;https://opensourcepledge.com/members/sentry/&quot;&gt;as a member of the Open Source Pledge&lt;/a&gt; (which we also started btw).&lt;/p&gt;
&lt;h2&gt;How AI disrupts licensing&lt;/h2&gt;
&lt;p&gt;LLMs disrupt software licensing in at least three ways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;LLMs are trained on public source code without much effort to respect license terms. Is it &amp;quot;fair use&amp;quot;?&lt;/li&gt;
&lt;li&gt;LLMs make it very easy to rewrite libraries, potentially obviating copyleft licenses.&lt;/li&gt;
&lt;li&gt;The output of LLMs will seemingly not be subject to copyright protection.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The second and third are even more of an issue since the December Leap. Let&amp;#39;s look at each in turn, and then consider the implications for Fair Source software.&lt;/p&gt;
&lt;h3&gt;No putting the genie back in the bottle&lt;/h3&gt;
&lt;p&gt;LLMs are generating more and more of our code, but how were they trained? On publicly available sources, and we can say with near certainty that LLMs are not complying with license requirements, whether that&amp;#39;s the strong restrictions of copyleft licenses like GPL, or even the minimal attribution restrictions of permissive licenses like MIT and BSD. When was the last time your coding agent provided an attribution notice with its suggestions? But even with this imputed use by LLMs, what is to be done about it now?&lt;/p&gt;
&lt;p&gt;We&amp;#39;ve seen copyright holders in other industries like books, music and photography sue the LLM providers for copyright infringement. Although we have not seen court decisions come out of these suits yet, we have seen some of them result in &lt;a href=&quot;https://www.npr.org/2025/09/05/nx-s1-5529404/anthropic-settlement-authors-copyright-ai&quot;&gt;monetary settlements&lt;/a&gt;. However, there is an important distinction between those media and open source software. The former have no express license terms granting broad rights to infringe copyrights in a manner that &lt;a href=&quot;https://opensource.org/osd&quot;&gt;by their very definition&lt;/a&gt; is &amp;quot;technology-neutral&amp;quot;. It definitely makes arguments of copyright infringement and breach of contract much harder to defend.&lt;/p&gt;
&lt;p&gt;While we have seen an &lt;a href=&quot;https://moginlawllp.com/developers-sue-github-microsoft-and-openai-over-copyright-in-creating-ai-tool-copilot/&quot;&gt;attempt by developers to enforce their copyrights&lt;/a&gt; against the LLM providers, that is facing challenges due to the difficulty in providing specific examples of copied code. This supports the model companies&amp;#39; &amp;quot;fair use&amp;quot; position. What if the infringement is not even done by making a copy of the code, but creating a derivative work based on existing software that has been completely rewritten by an LLM?&lt;/p&gt;
&lt;h3&gt;No stopping permissive rewrites&lt;/h3&gt;
&lt;p&gt;There has been a longstanding tension in the industry around the &lt;a href=&quot;https://nextjs.org/&quot;&gt;Next.js&lt;/a&gt; web framework. It&amp;#39;s one of the most popular, and technically it is Open Source under the MIT license, but it can really only be used as a first-class citizen on one hosting platform. The &lt;a href=&quot;https://opennext.js.org/&quot;&gt;OpenNext&lt;/a&gt; project exists to support Next.js apps on other platforms, but it has challenges. Because of this, Steve Faulkner from Cloudflare &lt;a href=&quot;https://blog.cloudflare.com/vinext/&quot;&gt;announced a new project called vinext&lt;/a&gt; that reimplements the Next.js API surface in the Vite framework, offering much better compatibility than OpenNext. What&amp;#39;s notable is that Steve did it in a week using agentic coding.&lt;/p&gt;
&lt;p&gt;In the wake of this, Steve Ruiz &lt;a href=&quot;https://github.com/tldraw/tldraw/issues/8082#issuecomment-3964650501&quot;&gt;joked&lt;/a&gt; about taking tl;draw&amp;#39;s test suite private, since the test suite was a major baseline for vinext. People like &lt;a href=&quot;https://x.com/cramforce/status/2026782878609322317&quot;&gt;Malte Ube&lt;/a&gt; and &lt;a href=&quot;https://x.com/GergelyOrosz/status/2026906253377613985&quot;&gt;Gergely Orosz&lt;/a&gt; took him seriously, showing just how much uncertainty there is, now that AI agents have brought the cost of coding down so much. Next.js is MIT, so it&amp;#39;s fair game for Cloudflare to do a rewrite like this, so long as they provide attribution.&lt;/p&gt;
&lt;p&gt;Much more controversial was &lt;a href=&quot;https://simonwillison.net/2026/Mar/5/chardet/&quot;&gt;a rewrite of a venerable Python library, chardet&lt;/a&gt;. The long-time maintainer made &lt;a href=&quot;https://github.com/chardet/chardet/issues/327#issuecomment-4005195078&quot;&gt;a good-faith effort to do a &amp;quot;clean-room&amp;quot; reimplementation&lt;/a&gt;. The &lt;a href=&quot;https://github.com/chardet/chardet/issues/327&quot;&gt;controversy&lt;/a&gt; is that he then licensed it under MIT instead of the original author&amp;#39;s choice, LGPL. The maintainer argued that it does not trigger the terms of the LGPL because he did not &amp;quot;modify a copy of the Library&amp;quot; (as the LGPL says), but rather did a ground-up rewrite. &lt;a href=&quot;https://x.com/badlogicgames/status/2029706078603133383&quot;&gt;Chardet seems to be present&lt;/a&gt; in the training data of the LLM in question, but the maintainer presents &lt;a href=&quot;https://github.com/chardet/chardet/issues/327#issuecomment-4005195078&quot;&gt;a metrics-based case&lt;/a&gt; that the new code is not derived from the old codebase.&lt;/p&gt;
&lt;p&gt;What, though, is the legal status of the LLM-generated output?&lt;/p&gt;
&lt;h3&gt;No copyrights on electric sheep&lt;/h3&gt;
&lt;p&gt;For a decade, Stephen Thaler has tried to win a copyright assignment for his &amp;quot;Creativity Machine&amp;quot; on images it &lt;a href=&quot;https://www.theaioptimist.com/p/this-ai-paints-invents-dreams-growing&quot;&gt;hallucinated during a simulated near-death experience&lt;/a&gt; (yeah that&amp;#39;s &lt;a href=&quot;https://imagination-engines.com/founder.html&quot;&gt;a rabbit hole&lt;/a&gt;). Last week the U.S. Supreme Court &lt;a href=&quot;https://www.reuters.com/legal/government/us-supreme-court-declines-hear-dispute-over-copyrights-ai-generated-material-2026-03-02/&quot;&gt;declined to hear Thaler&amp;#39;s case&lt;/a&gt;, letting stand &lt;a href=&quot;https://media.cadc.uscourts.gov/opinions/docs/2025/03/23-5233.pdf&quot;&gt;a lower court ruling&lt;/a&gt; that &lt;a href=&quot;https://www.skadden.com/insights/publications/2025/03/appellate-court-affirms-human-authorship&quot;&gt;a significant human element is necessary&lt;/a&gt; to receive copyright protection (EU has a &lt;a href=&quot;https://github.com/getsentry/fsl.software/issues/63#issuecomment-4001939799&quot;&gt;similar requirement&lt;/a&gt;). The U.S. Copyright Office is backing this up with what they will grant registration for, in line with the &lt;a href=&quot;https://www.copyright.gov/ai/ai_policy_guidance.pdf&quot;&gt;ground rules&lt;/a&gt; for their &lt;a href=&quot;https://www.copyright.gov/newsnet/2023/1004.html&quot;&gt;ongoing AI initiative&lt;/a&gt; (p 2):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the Office&amp;#39;s view, it is well-established that copyright can protect only material that is the product of human creativity. Most fundamentally, the term &amp;quot;author,&amp;quot; which is used in both the Constitution and the Copyright Act, excludes non-humans.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;No surprise, then, that &lt;a href=&quot;https://www.copyright.gov/ai/Copyright-and-Artificial-Intelligence-Part-2-Copyrightability-Report.pdf&quot;&gt;their report last January on copyrightability&lt;/a&gt; states (p iii):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copyright does not extend to purely AI-generated material, or material where there is insufficient human control over the expressive elements.&lt;/li&gt;
&lt;li&gt;Whether human contributions to AI-generated outputs are sufficient to constitute authorship must be analyzed on a case-by-case basis.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The contest now shifts to the definition of &amp;quot;sufficient human control.&amp;quot; However, the January report already draws a significant line: &amp;quot;prompts alone do not provide sufficient human control to make users of an AI system the authors of the output. Prompts essentially function as instructions that convey unprotectible ideas.&amp;quot; (p. 18).&lt;/p&gt;
&lt;p&gt;If prompts don&amp;#39;t count, does human code review? Would review need to result in a significant human-authored change, or is reviewing the code enough? How is this demonstrated? If human maintainers look at some code but not other code, is the code they looked at under copyright, and the code they didn&amp;#39;t, isn&amp;#39;t? How much longer until there is &lt;a href=&quot;https://factory.strongdm.ai/&quot;&gt;no human code review at all&lt;/a&gt;? There seems to be precious little keeping AI-generated code within the bounds of copyright.&lt;/p&gt;
&lt;h2&gt;No worries with Fair Source&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://fair.io/&quot;&gt;Fair Source&lt;/a&gt; was designed to allow companies to share code for their core software products without compromising their business model. It still does, even if the company uses AI to generate code. The key is that Fair Source offers another enforcement mechanism besides copyright infringement for rightholders. Software licenses are considered contracts between parties, and &amp;quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Open_source_license_litigation#Contract_litigation&quot;&gt;breach of contract&lt;/a&gt;&amp;quot; is a separate violation of law that still applies, even if &amp;quot;&lt;a href=&quot;https://en.wikipedia.org/wiki/Open_source_license_litigation#Copyright_litigation&quot;&gt;copyright infringement&lt;/a&gt;&amp;quot; does not.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can&amp;#39;t use clones of Fair Source software to compete with the software you&amp;#39;re cloning.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since Sentry is leading the Fair Source movement, we want to make our position clear: you can&amp;#39;t use clones of Fair Source software to compete with the software you&amp;#39;re cloning. LLMs just make the process faster, they don&amp;#39;t fundamentally alter the equation. Just because technology makes it easier to copy or make a derivative work, that doesn&amp;#39;t make it permitted — and because FSL is a contract with its own terms that you agree to when you access the source code, the copyright status of the code doesn&amp;#39;t really matter.&lt;/p&gt;
&lt;p&gt;We are definitely in a shifting landscape regarding IP rights and artificially generated code. Cloud computing was a technology shift that highlighted some of the inherent limitations of Open Source licensing. AI is further turning OSS on its head, amplifying the distinction between OSS and FSS. It is more important than ever to make the right decision about how to license your project. Sentry is full steam ahead with &lt;a href=&quot;https://fair.io/&quot;&gt;Fair Source&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Choosing a JavaScript logging library: The 2026 definitive guide</title><link>https://blog.sentry.io/javascript-logging-library-definitive-guide/</link><guid isPermaLink="true">https://blog.sentry.io/javascript-logging-library-definitive-guide/</guid><description>Pino, Winston, Bunyan, or LogTape; here&apos;s how the top JavaScript logging libraries compare so you can pick the right one for your stack</description><pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With AI writing more and more of our code, properly monitoring and debugging that code has become an increasingly critical part of the development workflow that can&amp;#39;t be ignored. Luckily, we have more time than ever to implement the right tools to do so.&lt;/p&gt;
&lt;p&gt;Implementing a production-ready logging solution is easy to do, and provides you &lt;em&gt;and&lt;/em&gt; your LLM Agents with a wealth of debugging information from your app, across users and environments.&lt;/p&gt;
&lt;h2&gt;Why you need a logging library&lt;/h2&gt;
&lt;p&gt;If you&amp;#39;re still using &lt;code&gt;console.log&lt;/code&gt; for debugging, you might be wondering why you should bother with a logging library.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High performance&lt;/strong&gt; - Logging libraries are asynchronous, beating native console logging in performance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured Outputs&lt;/strong&gt; - Output structured objects rather than strings, and simplify managing additional context and child loggers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transports and Sinks&lt;/strong&gt; - Send logs to one or more destinations, including the console, files, streams, and observability platforms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filtering&lt;/strong&gt; - Filter logs by severity, category, or other criteria to reduce noise. Redact sensitive data before it leaves your application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integrations&lt;/strong&gt; - Integrate with web frameworks, ORMs, and other libraries to automatically log context and errors with a consistent API across all layers of your application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trace-connected logging&lt;/strong&gt; - With Sentry, logs are automatically trace-connected to errors and other events, making it easier to debug and correlate issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Picking a logging library&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s how the big four stack up at a glance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/posts/javascript-logging-library-definitive-guide/inline-0.png&quot; alt=&quot;Line chart showing GitHub star growth over time for four Node.js logging libraries. Winston (red) steadily increases to about 23k stars over roughly 13 years. Pino (pink) grows rapidly to around 17k stars in about 10 years. Node-bunyan (yellow) rises more slowly and levels off near 7k stars. Logtape (blue) has a short early timeline with under 2k stars. The x-axis shows timeline in years and the y-axis shows GitHub stars.&quot;&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Library&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Version&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Runtime&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Released&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Transports / Sinks&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Minified + gzip&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Tree-shakable&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/pinojs/pino&quot;&gt;&lt;strong&gt;Pino&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=pino&quot;&gt;10.2.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=pino&quot;&gt;3.3 KB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=pino&quot;&gt;11&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/winstonjs/winston&quot;&gt;&lt;strong&gt;Winston&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=winston&quot;&gt;3.17.0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=winston&quot;&gt;38.3 KB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=winston&quot;&gt;17&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/trentm/node-bunyan&quot;&gt;&lt;strong&gt;Bunyan&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=bunyan&quot;&gt;1.8.15&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Node&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=bunyan&quot;&gt;5.6 KB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=bunyan&quot;&gt;0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://github.com/dahlia/logtape&quot;&gt;&lt;strong&gt;LogTape&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=%40logtape%2Flogtape&quot;&gt;2.0.2&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Universal&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=%40logtape%2Flogtape&quot;&gt;8.3 KB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://bundlephobia.com/api/size?package=%40logtape%2Flogtape&quot;&gt;0&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Source:&lt;/em&gt; &lt;a href=&quot;https://bundlephobia.com/&quot;&gt;&lt;em&gt;Bundlephobia API&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Quick selection guide&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pick Pino&lt;/strong&gt; when you&amp;#39;re Node-only and care most about speed and a small bundle.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick Winston&lt;/strong&gt; when you want the most transports and configuration options and bundle size isn&amp;#39;t a concern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick Bunyan&lt;/strong&gt; only if you&amp;#39;re maintaining an existing codebase that already uses it (not recommended for new projects).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick LogTape&lt;/strong&gt; when you need one logger for Node + browser/edge, or when writing a library that must work everywhere without forcing a choice on the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of the libraries above support custom transports or sinks, so you can pipe logs to whatever backend you use. If you use &lt;a href=&quot;https://sentry.io/&quot;&gt;Sentry&lt;/a&gt; for errors and performance, Sentry’s &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/logs/&quot;&gt;logging&lt;/a&gt; capabilities and integrations for &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/pino/&quot;&gt;Pino&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/#winston&quot;&gt;Winston&lt;/a&gt;, &lt;a href=&quot;https://docs.sentry.io/platforms/javascript/guides/nextjs/logs/#upcoming-integrations&quot;&gt;Bunyan&lt;/a&gt;, and &lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/#configuring-logtape-with-sentry-3&quot;&gt;LogTape&lt;/a&gt; let you send logs into the same place as your issues and traces, so you can search and correlate without juggling multiple tools.&lt;/p&gt;
&lt;h3&gt;Pino&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Node backends where speed and small bundle size matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/pinojs/pino&quot;&gt;pinojs/pino&lt;/a&gt; · &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://getpino.io/&quot;&gt;getpino.io&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/pino&quot;&gt;pino&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Setup
const pino = require(&amp;#39;pino&amp;#39;);
const logger = pino({ name: &amp;#39;user-service&amp;#39; });
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Usage
logger.info(&amp;#39;Request received&amp;#39;);

const child = logger.child({ userId: &amp;#39;u-123&amp;#39;, action: &amp;#39;login&amp;#39; });
child.info(&amp;#39;User action&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pino was created in 2016 by &lt;a href=&quot;https://github.com/mcollina&quot;&gt;Matteo Collina&lt;/a&gt;, creator of &lt;a href=&quot;https://www.fastify.io/&quot;&gt;Fastify&lt;/a&gt; and member of the Node.js Technical Steering Committee. It’s one of the most popular and fastest JSON loggers for Node.js; it can run in the browser via a polyfill, but you lose most of the speed benefits there.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://getpino.io/#/docs/benchmarks&quot;&gt;Reports to be&lt;/a&gt; ~2.5x faster than Winston&lt;/li&gt;
&lt;li&gt;Smallest bundle here (3.3 KB gzipped);&lt;/li&gt;
&lt;li&gt;Node.js only; browser via polyfill&lt;/li&gt;
&lt;li&gt;Pluggable transports and a wide ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pino&amp;#39;s popularity grew quickly as it provided a huge leap in performance at a smaller size than the competition at the time, and it provides sensible defaults out of the box. Every log will automatically include a timestamp, pid, and level, along with any structured data you provide.&lt;/p&gt;
&lt;h3&gt;Winston&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Node apps that need a rich ecosystem of transports and familiar, flexible configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/winstonjs/winston&quot;&gt;winstonjs/winston&lt;/a&gt; · &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://github.com/winstonjs/winston#readme&quot;&gt;GitHub README&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/winston&quot;&gt;winston&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Setup
const winston = require(&amp;#39;winston&amp;#39;);
const logger = winston.createLogger({
  level: &amp;#39;info&amp;#39;,
  format: winston.format.json(),
  defaultMeta: { service: &amp;#39;user-service&amp;#39; },
  transports: [new winston.transports.Console()],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Usage
logger.info(&amp;#39;Request received&amp;#39;);
logger.info({ userId: &amp;#39;u-123&amp;#39;, action: &amp;#39;login&amp;#39; }, &amp;#39;User action&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Winston was released in 2010 by &lt;a href=&quot;https://github.com/indexzero&quot;&gt;Charlie Robbins&lt;/a&gt;, a former Node.js Foundation board member (now OpenJS Foundation). It&amp;#39;s the most popular and one of the oldest logging libraries for Node.js, with a large ecosystem and many built-in transports. The trade-off is it&amp;#39;s the largest bundle size (38.3 KB) and 17 dependencies in this comparison.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Many built-in transports (console, file, HTTP, and many community options) with flexible formatting&lt;/li&gt;
&lt;li&gt;Mature, well-documented, and widely used in production&lt;/li&gt;
&lt;li&gt;Not tree-shakeable; you pay for the full feature set in bundle size&lt;/li&gt;
&lt;li&gt;Node.js only&lt;/li&gt;
&lt;li&gt;No data redaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To call Winston &amp;quot;legacy&amp;quot; would be a disservice, but it does follow an older design pattern that leads to a larger bundle size and more dependencies. Without question, Winston is your choice for mature, well-established Node.js applications that need a wide range of transports and flexible configuration right out of the box.&lt;/p&gt;
&lt;p&gt;While all of the libraries mentioned in this list offer custom filtering capabilities, Winston does not explicitly support &lt;strong&gt;data redaction&lt;/strong&gt;. Most loggers offer some form of redaction function that uses regex to replace private or sensitive data before it leaves your application.&lt;/p&gt;
&lt;h3&gt;Bunyan&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Node services that want a simple, JSON-first API and minimal dependencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/trentm/node-bunyan&quot;&gt;trentm/node-bunyan&lt;/a&gt; · &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://github.com/trentm/node-bunyan#readme&quot;&gt;GitHub README&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/bunyan&quot;&gt;bunyan&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Setup
const bunyan = require(&amp;#39;bunyan&amp;#39;);
const logger = bunyan.createLogger({ name: &amp;#39;user-service&amp;#39; });
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Usage
logger.info(&amp;#39;Request received&amp;#39;);
logger.info({ userId: &amp;#39;u-123&amp;#39;, action: &amp;#39;login&amp;#39; }, &amp;#39;User action&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bunyan was created by &lt;a href=&quot;https://github.com/trentm&quot;&gt;Trent Mick&lt;/a&gt; in 2012, making it one of the oldest libraries in this list. It&amp;#39;s a simple JSON-first logger with zero dependencies and a small bundle size.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero dependencies; small bundle (5.6 KB gzipped)&lt;/li&gt;
&lt;li&gt;Node.js only&lt;/li&gt;
&lt;li&gt;No data redaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some libraries are small and simple, and so don&amp;#39;t require updating often. That said, it&amp;#39;s been 5 years since the last release, and it doesn&amp;#39;t appear there has been much activity in the GitHub repository. At this time, I am not recommending Bunyan for new projects, though it remains one of the most popular libraries for Node.js.&lt;/p&gt;
&lt;p&gt;Like Winston, Bunyan does not support data redaction.&lt;/p&gt;
&lt;h3&gt;LogTape&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Modern TypeScript applications and libraries designed to run on Node, Deno, Bun, browsers, and edge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/dahlia/logtape&quot;&gt;dahlia/logtape&lt;/a&gt; · &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://logtape.org/&quot;&gt;logtape.org&lt;/a&gt; · &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href=&quot;https://www.npmjs.com/package/@logtape/logtape&quot;&gt;@logtape/logtape&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Setup

await configure({
  sinks: { console: getConsoleSink() },
  loggers: [{ category: [&amp;quot;user-service&amp;quot;], lowestLevel: &amp;quot;info&amp;quot;, sinks: [&amp;quot;console&amp;quot;] }],
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Usage

const logger = getLogger([&amp;quot;user-service&amp;quot;]);
logger.info(&amp;quot;Request received&amp;quot;);
logger.info(&amp;quot;User action&amp;quot;, { userId: &amp;quot;u-123&amp;quot;, action: &amp;quot;login&amp;quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LogTape is the newest library here (2023) and the only one in this list that’s fully tree-shakable and runs natively on every major JavaScript runtime: Node, Deno, Bun, browsers, and edge. Their &lt;a href=&quot;https://logtape.org/comparison&quot;&gt;comparison page&lt;/a&gt; goes deep on how it stacks up against Pino, Winston, Bunyan, and others.&lt;/p&gt;
&lt;p&gt;LogTape &lt;a href=&quot;https://logtape.org/comparison#performance-comparison&quot;&gt;reports to be&lt;/a&gt; ~2x faster than Pino, and over 10x faster than Winston. Its cross-runtime compatibility comes with a slight increase in bundle size, making it the second smallest library in the list, only beaten by Pino.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Universal runtime: Perfect for full stack and serverless applications&lt;/li&gt;
&lt;li&gt;Zero dependencies and tree-shakable&lt;/li&gt;
&lt;li&gt;Hierarchical categories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LogTape is our &amp;quot;Editor&amp;#39;s Favorite&amp;quot;. New to the scene, LogTape is the only option in our list that runs natively on Bun, Deno, browsers, and edge platforms like Cloudflare Workers and Vercel Edge Functions.&lt;/p&gt;
&lt;h2&gt;Special mention&lt;/h2&gt;
&lt;h3&gt;Sentry Logger&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Sentry users, capturing with existing console logs, compatible with all runtimes.&lt;/p&gt;
&lt;p&gt;G&lt;strong&gt;itHub:&lt;/strong&gt; &lt;a href=&quot;https://github.com/getsentry/sentry&quot;&gt;getsentry/sentry&lt;/a&gt; · &lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href=&quot;https://docs.sentry.io/product/explore/logs/&quot;&gt;docs.sentry.io&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Setup

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Usage

Sentry.logger.info(&amp;quot;User signed up&amp;quot;, {
  userId: user.id,
  plan: &amp;quot;pro&amp;quot;,
  referrer: &amp;quot;google&amp;quot;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we are including libraries that aren’t dedicated to logging, Sentry actually takes the award for the newest &lt;a href=&quot;https://blog.sentry.io/logs-generally-available/&quot;&gt;logging library.&lt;/a&gt; The Sentry monitoring platform added logging in late 2025, which can be accessed via the same SDKs you may already be using if you are monitoring your application with Sentry.&lt;/p&gt;
&lt;p&gt;Sentry’s logger is the only other logger besides LogTape in this list that is runtime agnostic. You can use Sentry in the browser, or anywhere you deploy your JavaScript code.&lt;/p&gt;
&lt;p&gt;Sentry offers a wide selection of SDKs, meaning you can use Sentry’s standard logging across languages. Have a Go backend? Sentry supports logging there too.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pino:&lt;/strong&gt; Small and fast for Node; best when performance and bundle size are your top priority.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Winston:&lt;/strong&gt; Most options and transports; best when you want one mature, configurable logger and aren&amp;#39;t constrained by bundle size.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bunyan:&lt;/strong&gt; Tiny and simple JSON logger; Currently not recommended for new projects as it may no longer be receiving updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LogTape:&lt;/strong&gt; Universal runtimes, zero deps, tree-shakable, library-friendly; best when you need one logger for Node and browser/edge, or when libraries need to log without forcing a choice on the app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sentry:&lt;/strong&gt; Easily integrates into existing projects using the Sentry SDK; universal runtime; multiple SDKs for other languages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What to do next&lt;/h2&gt;
&lt;p&gt;After picking a logging library, you&amp;#39;ll want to start collecting structured logs, and send them to a monitoring platform like Sentry. All of the libraries mentioned above support sending logs to external sources via &lt;em&gt;transports&lt;/em&gt; or &lt;em&gt;sinks&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to query and aggregate &lt;a href=&quot;https://docs.sentry.io/product/explore/logs/&quot;&gt;logs on Sentry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.sentry.io/trace-connected-structured-logging-with-logtape-and-sentry/&quot;&gt;Trace-connected structured logging with LogTape and Sentry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;[Video] &lt;a href=&quot;https://www.youtube.com/watch?v=k_qhTXAyiUs&quot;&gt;Production Logging for JS with LogTape + Sentry&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Routing OpenTelemetry logs to Sentry using OTLP</title><link>https://blog.sentry.io/structured-logging-opentelemetry/</link><guid isPermaLink="true">https://blog.sentry.io/structured-logging-opentelemetry/</guid><description>OpenTelemetry already set up? Two environment variables and your logs are in Sentry. This guide covers setup, how it works, and when to use the native SDK instead.</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&amp;#39;ve already instrumented your app with OpenTelemetry, you don&amp;#39;t have to rip it out to use Sentry. Two environment variables and your logs start flowing into Sentry, no SDK changes, no re-instrumentation. Here&amp;#39;s how to set it up in a sample app, and when the native Sentry SDK might be the better call.&lt;/p&gt;
&lt;h2&gt;Why you&amp;#39;d use OTLP instead of the native SDK&lt;/h2&gt;
&lt;p&gt;The main advantage of OTLP is that your logging code stays decoupled from any specific observability backend. You can switch where logs go by changing a few config lines. That&amp;#39;s useful if you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Already have OpenTelemetry logging in place&lt;/li&gt;
&lt;li&gt;Want to send logs to multiple backends&lt;/li&gt;
&lt;li&gt;Need vendor-neutral instrumentation&lt;/li&gt;
&lt;li&gt;Work with AI or LLM frameworks that use OpenTelemetry by default&lt;/li&gt;
&lt;li&gt;Want to use the broader OpenTelemetry ecosystem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you&amp;#39;re starting from scratch and only need Sentry, the &lt;a href=&quot;https://docs.sentry.io/platforms/node/logs/&quot;&gt;native Sentry SDK&lt;/a&gt; is probably the better call. With the native SDK, you get issue creation from &lt;a href=&quot;https://sentry.io/product/logs/&quot;&gt;logs&lt;/a&gt;, &lt;a href=&quot;https://sentry.io/product/session-replay/&quot;&gt;session replay&lt;/a&gt; integration, automatic breadcrumbs, and built-in error correlation. &lt;a href=&quot;https://docs.sentry.io/concepts/otlp/&quot;&gt;Ingesting OpenTelemetry traces and logs&lt;/a&gt; with Sentry via OTLP endpoints is still in beta and currently lacks these integrated features.&lt;/p&gt;
&lt;h2&gt;Guide prerequisites&lt;/h2&gt;
&lt;p&gt;Before we start, you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;a href=&quot;https://sentry.io/signup/&quot;&gt;Sentry account&lt;/a&gt; (the free tier works)&lt;/li&gt;
&lt;li&gt;Node.js 18 or later installed&lt;/li&gt;
&lt;li&gt;Basic familiarity with Express.js&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you don&amp;#39;t have a Sentry project yet, create one now. Select &lt;strong&gt;Express&lt;/strong&gt; as the platform. You can skip the DSN setup instructions because you&amp;#39;ll use the OTLP endpoint instead.&lt;/p&gt;
&lt;h2&gt;Get your Sentry OTLP credentials&lt;/h2&gt;
&lt;p&gt;Sentry exposes separate OTLP endpoints for logs and traces. In this guide, we&amp;#39;re focusing on the &lt;strong&gt;Logs endpoint&lt;/strong&gt;. To find your OTLP credentials:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Settings&lt;/strong&gt; in the left sidebar.&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;Organization&lt;/strong&gt; section in the &lt;strong&gt;Settings&lt;/strong&gt; sidebar, click &lt;strong&gt;Projects&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Find your project in the list and click on it to open the project settings.&lt;/li&gt;
&lt;li&gt;In the project settings sidebar, click &lt;strong&gt;Client Keys (DSN)&lt;/strong&gt; under the &lt;strong&gt;SDK Setup&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;OpenTelemetry&lt;/strong&gt; tab. Click the &lt;strong&gt;Expand&lt;/strong&gt; button to see all OTLP endpoint values.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep this tab open. We&amp;#39;ll use the following values in the next step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OTLP Logs Endpoint:&lt;/strong&gt; The URL where Sentry receives logs (which looks like &lt;code&gt;https://o{ORG_ID}.ingest.us.sentry.io/api/{PROJECT_ID}/integration/otlp/v1/logs&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OTLP Logs Endpoint Headers:&lt;/strong&gt; The authentication header (which looks like &lt;code&gt;x-sentry-auth=sentry sentry_key={YOUR_PUBLIC_KEY}&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing worth knowing: most OTLP exporters expect headers as raw key/value pairs, not full header strings. You&amp;#39;ll need to parse the header in your app. We&amp;#39;ll handle this in the setup below.&lt;/p&gt;
&lt;h2&gt;Connect your OpenTelemetry app to Sentry&lt;/h2&gt;
&lt;p&gt;We&amp;#39;ll use a sample payment processing service that already has OpenTelemetry logging instrumentation. &lt;strong&gt;You don&amp;#39;t need to touch the logging code itself.&lt;/strong&gt; Just point it at Sentry&amp;#39;s OTLP endpoint.&lt;/p&gt;
&lt;h3&gt;Clone the starter app&lt;/h3&gt;
&lt;p&gt;Run the following commands to clone the payment processing app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/getsentry/otlp-logging-sentry.git
cd otlp-logging-sentry
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This app includes the OpenTelemetry SDK already configured, structured logging throughout, multiple log severity levels (&lt;code&gt;INFO&lt;/code&gt;, &lt;code&gt;DEBUG&lt;/code&gt;, &lt;code&gt;WARN&lt;/code&gt;, and &lt;code&gt;ERROR&lt;/code&gt;), and rich log attributes for every entry.&lt;/p&gt;
&lt;h3&gt;Configure Sentry as the OTLP destination&lt;/h3&gt;
&lt;p&gt;Create a .&lt;code&gt;env&lt;/code&gt; file in the project root:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now edit &lt;code&gt;.env&lt;/code&gt; and add your Sentry OTLP credentials from the previous step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://o{YOUR_ORG_ID}.ingest.us.sentry.io/api/{YOUR_PROJECT_ID}/integration/otlp/v1/logs
OTEL_EXPORTER_OTLP_LOGS_HEADERS=x-sentry-auth=sentry sentry_key={YOUR_PUBLIC_KEY}
OTEL_SERVICE_NAME=payment-processing-service
PORT=3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the placeholders with your actual Sentry credentials. The &lt;code&gt;OTEL_SERVICE_NAME&lt;/code&gt; will let you filter logs by service in Sentry later.&lt;/p&gt;
&lt;p&gt;That&amp;#39;s it. Two config lines and OpenTelemetry logs are flowing to Sentry.&lt;/p&gt;
&lt;h2&gt;Test the integration&lt;/h2&gt;
&lt;p&gt;Start the app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OpenTelemetry logging initialized
Service: payment-processing-service
Payment Processing Service running on http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate some logs&lt;/h3&gt;
&lt;p&gt;In a new terminal window, send a request to process a payment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X POST http://localhost:3000/process-payment \
  -H &amp;quot;Content-Type: application/json&amp;quot; \
  -d &amp;#39;{&amp;quot;userId&amp;quot;: &amp;quot;user123&amp;quot;, &amp;quot;amount&amp;quot;: 99.99, &amp;quot;paymentMethod&amp;quot;: &amp;quot;credit_card&amp;quot;}&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ll get a JSON response confirming the payment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{
  &amp;quot;success&amp;quot;: true,
  &amp;quot;transactionId&amp;quot;: &amp;quot;txn_1730123456789_abc123def&amp;quot;,
  &amp;quot;amount&amp;quot;: 99.99,
  &amp;quot;currency&amp;quot;: &amp;quot;USD&amp;quot;,
  &amp;quot;status&amp;quot;: &amp;quot;completed&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;View the logs in Sentry&lt;/h3&gt;
&lt;p&gt;Now let&amp;#39;s see what this looks like in &lt;a href=&quot;https://docs.sentry.io/product/explore/logs/&quot;&gt;Sentry&amp;#39;s Logs view&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to your Sentry project.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Explore&lt;/strong&gt; in the left sidebar, then click &lt;strong&gt;Logs&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;#39;ll see a list of log entries from your payment processing workflow. Each log shows a timestamp, severity indicator (colored dot), and message.&lt;/p&gt;
&lt;h3&gt;Explore log attributes&lt;/h3&gt;
&lt;p&gt;Click on any log entry to expand it and see all its attributes.&lt;/p&gt;
&lt;p&gt;For example, the &lt;strong&gt;High-risk transaction detected&lt;/strong&gt; log includes attributes like the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fraud_check.score&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;97.98&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fraud_check.threshold&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;70&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fraud_check.reason&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;unusual_amount_pattern&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;user.id&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;user123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;transaction.id&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;txn_1762164637756_0hscczobm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;severity&lt;/code&gt;:&lt;/strong&gt; &lt;code&gt;warn&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these are searchable. To add any attribute as a filter, hover over it, click the overflow menu (three dots), and select &lt;strong&gt;Add to filter&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;How OpenTelemetry logging works&lt;/h2&gt;
&lt;p&gt;Here&amp;#39;s what&amp;#39;s happening under the hood, in case you&amp;#39;re applying these patterns to your own app.&lt;/p&gt;
&lt;h3&gt;OpenTelemetry SDK initialization&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;instrument.js&lt;/code&gt; file configures the OTLP exporter and wires up the logger provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;




// Configure the OTLP log exporter
const logExporter = new OTLPLogExporter({
  url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
  headers: {
    &amp;#39;x-sentry-auth&amp;#39;: process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS?.replace(&amp;#39;x-sentry-auth=&amp;#39;, &amp;#39;&amp;#39;) || &amp;#39;&amp;#39;,
  },
});

// Create logger provider
const loggerProvider = new LoggerProvider({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: &amp;#39;payment-processing-service&amp;#39;,
  }),
});

loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));

// Make logger provider available globally
global.loggerProvider = loggerProvider;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are the key parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/protocol/exporter/&quot;&gt;OTLPLogExporter&lt;/a&gt; sends logs to the OTLP endpoint.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/logs/sdk/#loggerprovider&quot;&gt;LoggerProvider&lt;/a&gt; manages the logging system.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/logs/sdk/#batching-processor&quot;&gt;BatchLogRecordProcessor&lt;/a&gt; groups log records before export, which reduces network overhead at scale.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Emitting structured logs&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;index.js&lt;/code&gt; file imports &lt;code&gt;instrument.js&lt;/code&gt; first, then creates a logger and emits records:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;

const logger = logs.getLogger(&amp;#39;payment-processing-service&amp;#39;, &amp;#39;1.0.0&amp;#39;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here&amp;#39;s how we emit a structured log:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function log(severity, severityNumber, message, attributes = {}) {
  logger.emit({
    severityNumber,
    severityText: severity,
    body: message,
    attributes,
  });
}

// Example usage
log(&amp;#39;INFO&amp;#39;, SeverityNumber.INFO, &amp;#39;Payment request received&amp;#39;, {
  &amp;#39;user.id&amp;#39;: userId,
  &amp;#39;payment.amount&amp;#39;: amount,
  &amp;#39;payment.method&amp;#39;: paymentMethod,
  &amp;#39;transaction.id&amp;#39;: transactionId,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each call to &lt;code&gt;logger.emit()&lt;/code&gt; takes a severity level, a message body, and a set of attributes. The attributes are what make logs searchable — the more context you add here, the easier it is to find specific events later.&lt;/p&gt;
&lt;h3&gt;Log severity levels&lt;/h3&gt;
&lt;p&gt;OpenTelemetry supports &lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber&quot;&gt;six severity levels&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
// TRACE (most detailed)
log(&amp;#39;TRACE&amp;#39;, SeverityNumber.TRACE, &amp;#39;Function entry&amp;#39;, {...});
// DEBUG (debugging info)
log(&amp;#39;DEBUG&amp;#39;, SeverityNumber.DEBUG, &amp;#39;Validating payment&amp;#39;, {...});
// INFO (informational)
log(&amp;#39;INFO&amp;#39;, SeverityNumber.INFO, &amp;#39;Payment received&amp;#39;, {...});
// WARN (warnings)
log(&amp;#39;WARN&amp;#39;, SeverityNumber.WARN, &amp;#39;High-risk transaction&amp;#39;, {...});
// ERROR (errors)
log(&amp;#39;ERROR&amp;#39;, SeverityNumber.ERROR, &amp;#39;Payment failed&amp;#39;, {...});
// FATAL (critical)
log(&amp;#39;FATAL&amp;#39;, SeverityNumber.FATAL, &amp;#39;System failure&amp;#39;, {...});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Adding rich attributes&lt;/h3&gt;
&lt;p&gt;The more attributes you add, the easier it is to debug issues. Here&amp;#39;s an example from the fraud detection path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;log(&amp;#39;WARN&amp;#39;, SeverityNumber.WARN, &amp;#39;High-risk transaction detected&amp;#39;, {
  &amp;#39;user.id&amp;#39;: userId,
  &amp;#39;transaction.id&amp;#39;: transactionId,
  &amp;#39;fraud_check.score&amp;#39;: 85.2,
  &amp;#39;fraud_check.threshold&amp;#39;: 70,
  &amp;#39;fraud_check.reason&amp;#39;: &amp;#39;unusual_amount_pattern&amp;#39;,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All these attributes are searchable in Sentry, so you can find specific transactions quickly without scanning log text.&lt;/p&gt;
&lt;h2&gt;OTLP vs native Sentry SDK&lt;/h2&gt;
&lt;p&gt;Both approaches send logs to Sentry. The difference is in what you get automatically.&lt;/p&gt;
&lt;h3&gt;Setup and configuration&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OTLP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// instrument.js


const logExporter = new OTLPLogExporter({
  url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
  headers: {
    &amp;#39;x-sentry-auth&amp;#39;: process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS
  },
});
const loggerProvider = new LoggerProvider({...});
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Native Sentry SDK&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// instrument.js

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  enableLogs: true, // Required for structured logging
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that &lt;code&gt;Sentry.logger&lt;/code&gt; requires Sentry JavaScript SDK v9.41.0 or above.&lt;/p&gt;
&lt;h3&gt;Emitting logs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OTLP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
const logger = logs.getLogger(&amp;#39;my-service&amp;#39;, &amp;#39;1.0.0&amp;#39;);
logger.emit({
  severityNumber: SeverityNumber.INFO,
  severityText: &amp;#39;INFO&amp;#39;,
  body: &amp;#39;Payment request received&amp;#39;,
  attributes: {
    &amp;#39;user.id&amp;#39;: userId,
    &amp;#39;payment.amount&amp;#39;: amount,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Native Sentry SDK&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
Sentry.logger.info(&amp;#39;Payment request received&amp;#39;, {
  &amp;#39;user.id&amp;#39;: userId,
  &amp;#39;payment.amount&amp;#39;: amount,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With OpenTelemetry, you specify both &lt;code&gt;severityNumber&lt;/code&gt; and &lt;code&gt;severityText&lt;/code&gt; manually. The Sentry SDK infers both from the method you call (&lt;code&gt;info()&lt;/code&gt;, &lt;code&gt;warn()&lt;/code&gt;, and so on). The SDK also associates logs with errors, transactions, and user sessions automatically, without any extra setup.&lt;/p&gt;
&lt;h3&gt;Log levels&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;OTLP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;
logger.emit({ severityNumber: SeverityNumber.DEBUG, ... });
logger.emit({ severityNumber: SeverityNumber.INFO, ... });
logger.emit({ severityNumber: SeverityNumber.WARN, ... });
logger.emit({ severityNumber: SeverityNumber.ERROR, ... });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Native Sentry SDK&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;Sentry.logger.debug(&amp;#39;message&amp;#39;, {...});
Sentry.logger.info(&amp;#39;message&amp;#39;, {...});
Sentry.logger.warn(&amp;#39;message&amp;#39;, {...});
Sentry.logger.error(&amp;#39;message&amp;#39;, {...});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What&amp;#39;s next&lt;/h2&gt;
&lt;p&gt;You now have OpenTelemetry logs flowing into Sentry. A few ways to get more value from here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Add context to your logs.&lt;/strong&gt; The more attributes you add, the easier it is to debug issues. Add user IDs, request IDs, transaction IDs, feature flags, or any relevant business context to every log entry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use consistent attribute naming.&lt;/strong&gt; Follow &lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/&quot;&gt;OpenTelemetry Semantic Conventions&lt;/a&gt; for standardized attribute names. This keeps your logs consistent and easier to search across services.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up alerts.&lt;/strong&gt; Configure &lt;a href=&quot;https://docs.sentry.io/product/alerts/&quot;&gt;Sentry alerts&lt;/a&gt; to notify you when certain log patterns appear — &lt;code&gt;ERROR&lt;/code&gt; logs exceeding a threshold, or high-risk transactions crossing a fraud score cutoff.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Combine logs with traces.&lt;/strong&gt; If you&amp;#39;re also sending &lt;a href=&quot;https://sentry.io/product/tracing/&quot;&gt;traces&lt;/a&gt; to Sentry, you can correlate them with logs to get a complete picture of your application&amp;#39;s behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OTLP logging support is still in open beta. If you run into a limitation not listed here, &lt;a href=&quot;https://github.com/getsentry/sentry&quot;&gt;open an issue on GitHub&lt;/a&gt;. That&amp;#39;s the fastest way to get it on our radar.&lt;/p&gt;
</content:encoded></item></channel></rss>