<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jesutofunmi</title>
    <description>The latest articles on DEV Community by Jesutofunmi (@codebreak_er).</description>
    <link>https://dev.to/codebreak_er</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029363%2F8dc7ecd3-9787-401f-aa0a-4504eed41b0c.jpg</url>
      <title>DEV Community: Jesutofunmi</title>
      <link>https://dev.to/codebreak_er</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codebreak_er"/>
    <language>en</language>
    <item>
      <title>We Gave Our AI Code Review Agent a Dashboard — Here's What SigNoz Showed Us</title>
      <dc:creator>Jesutofunmi</dc:creator>
      <pubDate>Sun, 26 Jul 2026 00:23:34 +0000</pubDate>
      <link>https://dev.to/codebreak_er/we-gave-our-ai-code-review-agent-a-dashboard-heres-what-signoz-showed-us-4m2j</link>
      <guid>https://dev.to/codebreak_er/we-gave-our-ai-code-review-agent-a-dashboard-heres-what-signoz-showed-us-4m2j</guid>
      <description>&lt;h2&gt;
  
  
  Every AI agent shouldn't be a black box
&lt;/h2&gt;

&lt;p&gt;While building Jargons, our AI-powered GitHub code review agent, we noticed something strange. We could see the comments it posted on pull requests, but We couldn't answer simple questions about what happened behind the scenes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How long did a review actually take?&lt;/li&gt;
&lt;li&gt;How many tokens did it consume?&lt;/li&gt;
&lt;li&gt;How much did each review cost?&lt;/li&gt;
&lt;li&gt;Which step was slowing everything down?&lt;/li&gt;
&lt;li&gt;Did the agent quietly fail halfway through?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For software that runs automatically on every pull request, that's a surprising amount of trust to place in something you can't actually observe. So instead of treating observability as an afterthought, We decided to build it into the agent from day one. That decision ended up uncovering bugs We probably never would have found by reading the code alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Jargons
&lt;/h2&gt;

&lt;p&gt;Jargons is an AI-powered GitHub code review and security scanning agent. Given a pull request or an entire repository, it can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Review code changes&lt;/li&gt;
&lt;li&gt;Detect bugs and security issues&lt;/li&gt;
&lt;li&gt;Highlight architectural problems&lt;/li&gt;
&lt;li&gt;Suggest improvements&lt;/li&gt;
&lt;li&gt;Automatically open a companion pull request containing fixes whenever possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the Agents of SigNoz Hackathon (Track 1), We wanted to go one step further. Instead of simply letting the agent review code, We wanted to observe everything it was doing in real time. That meant instrumenting the entire review pipeline with OpenTelemetry and sending every trace to SigNoz, allowing us to visualize every review from start to finish.&lt;/p&gt;

&lt;p&gt;The stack looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TanStack Start (React 19) running on Nitro&lt;/li&gt;
&lt;li&gt;Drizzle ORM + PostgreSQL&lt;/li&gt;
&lt;li&gt;GitHub App for webhook events and repository access&lt;/li&gt;
&lt;li&gt;Google Gemini as the LLM&lt;/li&gt;
&lt;li&gt;OpenTelemetry&lt;/li&gt;
&lt;li&gt;Self-hosted SigNoz&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foell9hhpd55leatwk8di.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foell9hhpd55leatwk8di.png" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting SigNoz running
&lt;/h2&gt;

&lt;p&gt;One thing We appreciated was how quick it was to get SigNoz running locally. The hackathon uses SigNoz Foundry, so the deployment is defined in a small declarative configuration file committed directly into the repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;flavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting everything only required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh | bash
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foundry also generates a lock file, making the deployment reproducible for anyone cloning the project — including the hackathon judges. On our setup, running Docker Compose through Windows + WSL, everything worked smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every review becomes a trace
&lt;/h2&gt;

&lt;p&gt;Instead of viewing a code review as one giant operation, We treated every major step as its own span. A typical review looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;review.run
├── github.fetch_diff
├── llm.review
├── db.write_findings
├── github.open_fix_pr
│   └── llm.autofix
└── github.post_review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means every pull request becomes one distributed trace inside SigNoz. Instead of wondering where time was spent, We could immediately see which step was responsible. The LLM spans also follow OpenTelemetry's GenAI semantic conventions, allowing SigNoz to automatically surface information such as model usage, token consumption, and estimated cost. Rather than manually piecing together logs, We could simply open a trace and understand the entire lifecycle of a review.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frd461ric880mc57ppjur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frd461ric880mc57ppjur.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an observability dashboard
&lt;/h2&gt;

&lt;p&gt;Tracing was only part of the story. We also wanted a high-level view of how the agent was behaving over time. Using custom OpenTelemetry metrics, Jargons records information such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviews processed&lt;/li&gt;
&lt;li&gt;Review duration&lt;/li&gt;
&lt;li&gt;LLM token usage&lt;/li&gt;
&lt;li&gt;Estimated model cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those metrics power a dedicated dashboard showing the overall health of the agent. Instead of watching logs scroll by, We could immediately answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the agent healthy?&lt;/li&gt;
&lt;li&gt;Are reviews getting slower?&lt;/li&gt;
&lt;li&gt;Is token usage increasing?&lt;/li&gt;
&lt;li&gt;Is one workspace consuming significantly more resources?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzziu7bsl3ddjt0uyvfmk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzziu7bsl3ddjt0uyvfmk.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing observability into the product
&lt;/h2&gt;

&lt;p&gt;One feature we're particularly happy with is that observability isn't limited to the SigNoz dashboard. Jargons includes its own Agent Health page. Some information — such as review counts and historical statistics — comes directly from PostgreSQL because those numbers need to be exact. Live observability data, including token usage and model cost, comes directly from SigNoz through its query API.&lt;/p&gt;

&lt;p&gt;The page also displays whether the application can currently communicate with SigNoz. If SigNoz becomes unavailable, the page degrades gracefully instead of failing entirely. That separation keeps operational monitoring independent from application data while still presenting everything in one place for users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mbu8uu7qt201kfx1n3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8mbu8uu7qt201kfx1n3v.png" alt=" " width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability without exposing user code
&lt;/h2&gt;

&lt;p&gt;One design decision we were careful about involved privacy. The spans only contain operational metadata such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository name&lt;/li&gt;
&lt;li&gt;Pull request number&lt;/li&gt;
&lt;li&gt;Model used&lt;/li&gt;
&lt;li&gt;Token counts&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Cost&lt;/li&gt;
&lt;li&gt;Number of findings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They never include source code, pull request diffs, or generated review content. Observability shouldn't become another way to leak sensitive application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs tracing helped us find
&lt;/h2&gt;

&lt;p&gt;This ended up being our favorite part of the project.&lt;/p&gt;

&lt;p&gt;The first issue involved throughput metrics. Initially, the dashboard appeared broken because throughput stayed at zero immediately after startup. The traces clearly showed reviews completing successfully. The problem wasn't the agent — it was the metric. Since cumulative counters need multiple samples before functions like &lt;code&gt;increase()&lt;/code&gt; become meaningful, a brand-new workspace naturally reported zero throughput. The fix was straightforward: use PostgreSQL for exact counts while letting SigNoz focus on telemetry such as latency, token usage, and cost.&lt;/p&gt;

&lt;p&gt;The second bug was even more interesting. After watching the traces for a while, we noticed something unusual. The same repository kept producing nearly identical &lt;code&gt;review.run&lt;/code&gt; traces. Looking closer, we realized Jargons had started reviewing the pull requests it created itself. Nothing crashed. No exceptions were thrown. The agent had quietly entered a review loop. A single guard preventing reviews on &lt;code&gt;jargons/fix-*&lt;/code&gt; branches solved the problem immediately. Without distributed tracing, that bug could have taken much longer to discover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;If we could give our past selves one piece of advice, it would be this: instrument your AI agent before you think you need to.&lt;/p&gt;

&lt;p&gt;Observability isn't just about debugging production issues. It changes how you build. Instead of guessing where time goes, you can measure it. Instead of wondering why costs increase, you can see exactly when they happen. Instead of assuming your workflows behave correctly, you can watch every step execute. That visibility changes the way you design software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Building Jargons was an exciting engineering challenge. Making it observable made it trustworthy. Today, every review generates a trace. Every AI call has measurable latency. Every token consumed can be monitored. Every workflow can be inspected. And the health of the agent lives on a dashboard instead of disappearing into log files.&lt;/p&gt;

&lt;p&gt;For us, that's what observability adds to AI systems: confidence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with AI assistance (Anthropic's Claude); all architecture, integration, and testing decisions were directed and reviewed by our team, in accordance with the hackathon rules.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/devtofunmi/jargons" rel="noopener noreferrer"&gt;https://github.com/devtofunmi/jargons&lt;/a&gt;&lt;br&gt;
Live Demo: &lt;a href="https://jargons.vercel.app" rel="noopener noreferrer"&gt;https://jargons.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The 500 That Told Me Nothing — Until I Self-Hosted SigNoz</title>
      <dc:creator>Jesutofunmi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 20:08:04 +0000</pubDate>
      <link>https://dev.to/codebreak_er/the-500-that-told-me-nothing-until-i-self-hosted-signoz-3mk9</link>
      <guid>https://dev.to/codebreak_er/the-500-that-told-me-nothing-until-i-self-hosted-signoz-3mk9</guid>
      <description>&lt;p&gt;A few days ago, a route in one of my side projects started returning this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"unhandled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"HTTPError"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That's the whole error. No stack trace in the response, nothing useful in the handler — my own &lt;code&gt;try/catch&lt;/code&gt; never even fired. The browser got a 500, my code swore it did nothing wrong, and I spent longer than I'd like to admit staring at both.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev8f18i7h0748soapkcn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev8f18i7h0748soapkcn.png" alt="The opaque 500 response in the browser" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I eventually found the cause (more on that later — it's a genuinely sneaky one), but the experience exposed a bigger problem: I could only see what happened inside my handler. Once the request left my code and passed back through the framework, I was blind. &lt;strong&gt;If an error can happen outside my own code, &lt;code&gt;console.log&lt;/code&gt; will never be enough.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I needed was a way to follow the entire request — through my handler, the framework, and every operation in between — and see exactly where it failed. That is what distributed tracing is built for. To try it properly, I chose SigNoz: an open-source, OpenTelemetry-native observability platform I could self-host for free. Then I built a tiny reproduction app, instrumented it, and pointed it at my local SigNoz instance.&lt;/p&gt;

&lt;p&gt;This post is what actually happened — including the parts where things didn't work, because those turned out to be the most instructive bits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug, revisited — what tracing could and couldn't see
&lt;/h2&gt;

&lt;p&gt;Once the reproduction app was sending traces, I went straight back to the problem that started all of this: what &lt;em&gt;was&lt;/em&gt; my opaque 500? In TanStack Start, this innocent-looking handler is broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setCookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo_session&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Response.redirect()&lt;/code&gt; returns a Response whose headers are &lt;strong&gt;immutable&lt;/strong&gt; — that's the Fetch spec, not a framework quirk. The framework then tries to append the &lt;code&gt;Set-Cookie&lt;/code&gt; header to the returned response &lt;em&gt;after the handler has returned&lt;/em&gt;. Appending to immutable headers throws &lt;code&gt;TypeError: immutable&lt;/code&gt;, the framework converts it to a generic 500, and your &lt;code&gt;try/catch&lt;/code&gt; never sees a thing, because the explosion happens outside your code. (The fix: build the redirect by hand with &lt;code&gt;new Response(null, { status: 302, headers: { location } })&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;Here's the part that taught me something about observability. In SigNoz, the &lt;code&gt;/bug&lt;/code&gt; requests show up as &lt;strong&gt;error spans&lt;/strong&gt; — they're why the error rate chart works. But the &lt;strong&gt;Exceptions tab stayed empty&lt;/strong&gt;. Why? Because nothing ever called &lt;code&gt;recordException&lt;/code&gt;. The framework swallowed the &lt;code&gt;TypeError&lt;/code&gt; and returned a 500; the span was marked as an error by its status code, but no exception &lt;em&gt;event&lt;/em&gt; was ever attached to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status-code errors and recorded exceptions are different signals.&lt;/strong&gt; If your code never sees the error, neither does your exception tracking — only the trace does.&lt;/p&gt;

&lt;p&gt;To prove the contrast, I added &lt;code&gt;/crash&lt;/code&gt;, a failure that is instrumented the way failures should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cannot read properties of undefined (reading 'amount')&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recordException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SpanStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment processing failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace view shows it as a red, failed span:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l0sskvywtmgqmaa6fpo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5l0sskvywtmgqmaa6fpo.png" alt="Crash trace with error span" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And &lt;em&gt;this&lt;/em&gt; one shows up in the Exceptions tab, grouped by type, with a count and timeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqoise1p334l1zaarpnxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqoise1p334l1zaarpnxi.png" alt="Exceptions tab showing the recorded TypeError" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same status code as &lt;code&gt;/bug&lt;/code&gt;. Completely different visibility. That contrast — an invisible failure next to an instrumented one, in the same service, on the same screen — is the clearest argument for tracing I've come across.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else SigNoz showed me
&lt;/h2&gt;

&lt;p&gt;Once I understood the original bug, I explored what the same instrumentation revealed across the rest of the app. A few minutes of clicking around, and the Services page already had opinions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv6t1nbrm8ag9x4kc5xr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv6t1nbrm8ag9x4kc5xr.png" alt="Services overview with error rate" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;P99 latency, throughput, and an &lt;strong&gt;error rate&lt;/strong&gt; — computed entirely from auto-instrumented spans. I never wrote separate metrics code. The 500s from &lt;code&gt;/bug&lt;/code&gt; were already there as a percentage on a chart, which made the service-level impact of the failure visible before I opened an individual trace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsvfywruvspudx22hfsd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsvfywruvspudx22hfsd.png" alt="Service detail charts" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The service detail page also has a Key Operations table that ranks every span by latency — including my hand-written &lt;code&gt;query-database&lt;/code&gt; and &lt;code&gt;load-user-session&lt;/code&gt; spans, sitting alongside the auto-captured HTTP and TLS spans, with P50/P95/P99 for each. Zero dashboard configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following a slow request
&lt;/h3&gt;

&lt;p&gt;Opening a single &lt;code&gt;/slow&lt;/code&gt; request shows the anatomy of the whole 3.56 seconds:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vss6qhxhrzku7u360qb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vss6qhxhrzku7u360qb.png" alt="Trace waterfall for the slow route" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The GET request contains &lt;code&gt;load-user-session&lt;/code&gt; (120ms), then &lt;code&gt;query-database&lt;/code&gt; (340ms), then the TCP + TLS handshake and the outbound GitHub API call. Every "why is this page slow?" conversation I've ever had would have been shorter with this view.&lt;/p&gt;

&lt;p&gt;A detail I didn't expect: each trace contained &lt;strong&gt;two&lt;/strong&gt; GET server spans. Vite's dev mode runs application code in a worker process behind a small internal proxy — and OpenTelemetry's context propagation stitched the proxy process and the worker process into a single coherent trace, automatically, across process boundaries. That's the machinery working exactly as designed, visible on a laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I like most
&lt;/h2&gt;

&lt;p&gt;After a few hours of poking at dashboards, alerts, logs, and explorers, the feature I keep coming back to is the &lt;strong&gt;trace detail view&lt;/strong&gt; — the flamegraph plus span waterfall.&lt;/p&gt;

&lt;p&gt;Everything else in SigNoz is aggregate: charts, percentiles, rates. All useful, all things other tools also do. But the trace view answers the question aggregates can't: &lt;em&gt;what happened to this one request?&lt;/em&gt; It turned my invisible framework bug into a red span with a timestamp and a duration. It showed me my dev server was secretly two processes. It put my hand-written spans, the framework's HTTP handling, and a third-party API call on one timeline with no effort beyond starting the SDK.&lt;/p&gt;

&lt;p&gt;And because SigNoz is OpenTelemetry-native, getting there required nothing proprietary — one exporter URL. If I ever want to point the same app somewhere else, it's a one-line change. That's the right kind of dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you if you're trying this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the SigNoz account before debugging "broken" OTLP ports.&lt;/strong&gt; The collector's receivers don't start until an org exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter dev-server noise&lt;/strong&gt; with &lt;code&gt;ignoreIncomingRequestHook&lt;/code&gt;, or your traces are unreadable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;recordException&lt;/code&gt; is not optional&lt;/strong&gt; if you want the Exceptions view to mean anything. Span status alone won't do it.&lt;/li&gt;
&lt;li&gt;Windows works fine via the Docker Compose files, even though the shiny new installer says otherwise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole demo app is a handful of files — an &lt;code&gt;instrumentation.mjs&lt;/code&gt;, four routes, and a docker-compose you didn't have to write. For a free, self-hosted stack, the distance between "nothing" and "I can see every request my app makes" turned out to be about an afternoon — most of which was me learning these pitfalls so you don't have to.&lt;/p&gt;

&lt;p&gt;Those are the lessons I would carry into the next project. If you want to reproduce the experiment yourself, here is the exact setup that produced them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Windows 11, Docker Desktop (WSL2 backend)&lt;/li&gt;
&lt;li&gt;A minimal &lt;a href="https://tanstack.com/start" rel="noopener noreferrer"&gt;TanStack Start&lt;/a&gt; app (React + file-based routing with server routes), Node 24&lt;/li&gt;
&lt;li&gt;SigNoz v0.99.0, self-hosted with Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo app has four routes, each designed to produce a different kind of telemetry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/slow&lt;/code&gt; — a deliberately multi-step request: two fake I/O waits wrapped in manual spans, plus a real outbound call to the GitHub API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/bug&lt;/code&gt; — a faithful reproduction of my opaque 500&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/fixed&lt;/code&gt; — the same logic as &lt;code&gt;/bug&lt;/code&gt;, done correctly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/crash&lt;/code&gt; — a failure that is &lt;em&gt;properly&lt;/em&gt; instrumented&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-hosting SigNoz (the Windows detour)
&lt;/h2&gt;

&lt;p&gt;SigNoz's current docs push a new installer called Foundry — which plainly states that &lt;strong&gt;Windows is not officially supported&lt;/strong&gt;. The older Docker Compose files still ship in the repo though, and the last compose-based release is the &lt;em&gt;current&lt;/em&gt; release, so on Windows this works fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; https://github.com/SigNoz/signoz.git
&lt;span class="nb"&gt;cd &lt;/span&gt;signoz
git sparse-checkout &lt;span class="nb"&gt;set &lt;/span&gt;deploy
git checkout v0.99.0
&lt;span class="nb"&gt;cd &lt;/span&gt;deploy/docker
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run pulls a few gigabytes (ClickHouse, ZooKeeper, the SigNoz server, and the OTel collector), then the stack boots in order: ClickHouse → schema migrator → SigNoz server → collector. A few minutes later the UI is up at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One honest note from the logs: the server warns loudly if &lt;code&gt;SIGNOZ_JWT_SECRET&lt;/code&gt; isn't set. Fine for a laptop demo, but set it if you deploy this anywhere real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup pitfall: the OTLP ports are dead until you create an account
&lt;/h3&gt;

&lt;p&gt;This one cost me actual debugging time, so I'm writing it down for the next person.&lt;/p&gt;

&lt;p&gt;After the stack came up, the collector logged &lt;code&gt;Everything is ready. Begin running and processing data.&lt;/code&gt; — and yet nothing could connect to the OTLP endpoints on ports 4317/4318. Connection refused, even container-to-container. The ports were mapped, the config said &lt;code&gt;0.0.0.0:4318&lt;/code&gt;, the collector claimed to be alive.&lt;/p&gt;

&lt;p&gt;The answer was in the &lt;em&gt;SigNoz server's&lt;/em&gt; logs, not the collector's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to find or create agent ... cannot create agent without orgId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigNoz's collector runs in &lt;strong&gt;managed mode&lt;/strong&gt;: it fetches its live configuration from the SigNoz server over OpAMP, and it won't start its receivers until it registers. Registration requires an organization to exist — and the organization is only created &lt;strong&gt;when you set up the first admin account in the UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the fix for "my OTLP ports won't open" was: open &lt;code&gt;localhost:8080&lt;/code&gt;, create the login. Thirty seconds later the collector registered and ports 4317/4318 came alive. Every span my app had sent before that moment was silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting a TanStack Start app
&lt;/h2&gt;

&lt;p&gt;With SigNoz running, the last step is connecting the application to it. The Node OpenTelemetry SDK gets preloaded into the dev server via &lt;code&gt;NODE_OPTIONS&lt;/code&gt;. The whole setup is one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// instrumentation.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodeSDK&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/sdk-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OTLPTraceExporter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/exporter-trace-otlp-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getNodeAutoInstrumentations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/auto-instrumentations-node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NOISY_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;@/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/node_modules/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;vite/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/favicon/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;css/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;js&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NodeSDK&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;serviceName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;otel-playground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;traceExporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OTLPTraceExporter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4318/v1/traces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;instrumentations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;getNodeAutoInstrumentations&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/instrumentation-fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opentelemetry/instrumentation-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;ignoreIncomingRequestHook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;NOISY_PATHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cross-env NODE_OPTIONS=--import=./instrumentation.mjs vite dev --port 3001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;ignoreIncomingRequestHook&lt;/code&gt; matters in dev. Vite serves a storm of module and asset requests; without the filter, your traces drown in &lt;code&gt;/@vite/client&lt;/code&gt; noise.&lt;/li&gt;
&lt;li&gt;Custom spans are just a few lines inside a server function:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;otel-playground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startActiveSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query-database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;simulateDbCall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire integration. No agent daemon, no vendor SDK — the OTLP endpoint is the only SigNoz-specific thing in the app, which means none of this is lock-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo code&lt;/strong&gt; (the app, all four routes, and the instrumentation file): &lt;a href="https://github.com/devtofunmi/otel-playground" rel="noopener noreferrer"&gt;github.com/devtofunmi/otel-playground&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz self-hosting docs&lt;/strong&gt;: &lt;a href="https://signoz.io/docs/install/self-host/" rel="noopener noreferrer"&gt;signoz.io/docs/install/self-host&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry JS getting started&lt;/strong&gt;: &lt;a href="https://opentelemetry.io/docs/languages/js/" rel="noopener noreferrer"&gt;opentelemetry.io/docs/languages/js&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Written for the &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz hackathon&lt;/a&gt; early-blog challenge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Transparency note: I used an AI assistant (Claude) to help draft and edit this post. Every install step, error message, trace, and screenshot is from my own machine — including the hours those pitfalls cost me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>signoz</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
