<?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: Anurag Sati</title>
    <description>The latest articles on DEV Community by Anurag Sati (@anurag_sati_5b77a6578f780).</description>
    <link>https://dev.to/anurag_sati_5b77a6578f780</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%2F3602291%2F786e1233-773e-4f3c-8c2a-d87444bc9656.jpg</url>
      <title>DEV Community: Anurag Sati</title>
      <link>https://dev.to/anurag_sati_5b77a6578f780</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anurag_sati_5b77a6578f780"/>
    <language>en</language>
    <item>
      <title>Test each Convex PR preview with TestSprite before it can merge</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Tue, 20 Oct 2026 18:02:36 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/test-each-convex-pr-preview-with-testsprite-before-it-can-merge-4foc</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/test-each-convex-pr-preview-with-testsprite-before-it-can-merge-4foc</guid>
      <description>&lt;p&gt;Disclosure: This project and article were developed with AI assistance, including Devin and Codex. The results below are linked to the repository and TestSprite evidence.&lt;/p&gt;

&lt;p&gt;Handoff is a shared workspace for families looking after an aging parent. Here is one thing it has to get right. Maya asks her brother Leo to pick up a prescription. Until the request is answered, nobody else should quietly take it over, and the record should say that Leo accepted.&lt;/p&gt;

&lt;p&gt;TestSprite found that we got this wrong. Leo saw a "Take this" button next to the request. Pressing it recorded "Responsibility taken" and silently dropped Maya's request. It was the fourth app bug the suite caught. Each same-repository pull request to Handoff gets its own live preview, and the browser suite has to pass on that preview before GitHub allows the merge.&lt;/p&gt;

&lt;p&gt;This is the setup, with the parts that cost us time. All code is in &lt;a href="https://github.com/Satianurag/handoff" rel="noopener noreferrer"&gt;Satianurag/handoff&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;17 browser tests in the TestSprite 3.0 portal: handovers with receipts, responsibility transfer with attribution, cross-household denial, care-circle permission limits, used one-time links, and required fields.&lt;/li&gt;
&lt;li&gt;4 app bugs caught, each fixed in its own PR. The first production run passed 12 of 16.&lt;/li&gt;
&lt;li&gt;Each same-repository PR deploys a Convex preview. TestSprite tests that preview, not production, and posts a &lt;code&gt;TestSprite V3&lt;/code&gt; status that a branch ruleset requires.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Satianurag/handoff/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt; shows the full loop: red on its own preview, merge blocked, green after the fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull_request ─▶ deploy-preview job ─▶ Convex preview  pr-&amp;lt;N&amp;gt;   (random hostname)
                     │
                     ▼
               preview job ──OIDC──▶ alias Worker:  handoff-pr-&amp;lt;N&amp;gt;.satimon.com ─302─▶ preview
                     │
          `preview` check completes
                     ▼
        TestSprite PR trigger ─▶ runs the suite on handoff-pr-{pr}.satimon.com
                     ▼
        `TestSprite V3` status ─▶ `main-gate` ruleset allows or blocks the merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: a Convex preview for each PR
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Satianurag/handoff/blob/main/.github/workflows/preview.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/preview.yml&lt;/code&gt;&lt;/a&gt; runs on &lt;code&gt;pull_request&lt;/code&gt; (&lt;code&gt;opened&lt;/code&gt;, &lt;code&gt;synchronize&lt;/code&gt;, &lt;code&gt;reopened&lt;/code&gt;) and skips fork PRs, so preview secrets never reach a fork. It has two jobs, so the job that runs &lt;code&gt;npm ci&lt;/code&gt; and PR code never holds an OIDC token:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&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;deploy-preview&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.pull_request.head.repo.full_name == github.repository&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;preview_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deploy.outputs.preview_url }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# checkout (persist-credentials: false), setup-node, npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node scripts/deploy-preview.mjs "pr-${{ github.event.pull_request.number }}"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CONVEX_DEPLOY_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CONVEX_PREVIEW_DEPLOY_KEY }}&lt;/span&gt;

  &lt;span class="na"&gt;preview&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;deployments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# publish a GitHub Deployment, then register and verify the alias (step 2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script calls the Convex CLI with a preview deploy key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx convex deploy &lt;span class="nt"&gt;--preview-name&lt;/span&gt; pr-&amp;lt;N&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cmd&lt;/span&gt; &lt;span class="s2"&gt;"node scripts/deploy-preview.mjs pr-&amp;lt;N&amp;gt; --seed-preview-env"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convex runs &lt;code&gt;--cmd&lt;/code&gt; after the preview exists and before it pushes functions, so that is where we set the environment variables the functions need at push time. URL-dependent variables such as &lt;code&gt;SITE_URL&lt;/code&gt; are set after &lt;code&gt;deploy&lt;/code&gt; returns.&lt;/p&gt;

&lt;p&gt;Gotchas we hit with Convex CLI 1.46.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running &lt;code&gt;convex env set --preview-name pr-&amp;lt;N&amp;gt;&lt;/code&gt; before the first deploy returned &lt;code&gt;PreviewNotFound&lt;/code&gt;, and a preview deploy key could not set project defaults. Seeding inside &lt;code&gt;--cmd&lt;/code&gt; avoids both.&lt;/li&gt;
&lt;li&gt;Preview hostnames are random, for example &lt;code&gt;fortunate-moose-341.convex.site&lt;/code&gt;. Read the URL from the deploy output instead of deriving it from the PR number. We publish it as the GitHub Deployment &lt;code&gt;environment_url&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@convex-dev/static-hosting&lt;/code&gt; 0.2.1 has no preview target. We run its upload with a 12-line &lt;code&gt;--require&lt;/code&gt; shim (&lt;a href="https://github.com/Satianurag/handoff/blob/main/scripts/convex-preview-name.cjs" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/convex-preview-name.cjs&lt;/code&gt;&lt;/a&gt;) that adds &lt;code&gt;--preview-name&lt;/code&gt; to its &lt;code&gt;convex run&lt;/code&gt; calls.&lt;/li&gt;
&lt;li&gt;On the Free plan, previews expire after 5 days. Push to a stale PR to redeploy it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: a stable URL TestSprite can predict
&lt;/h2&gt;

&lt;p&gt;TestSprite's Pull Request trigger builds the target URL from a pattern. The placeholders are &lt;code&gt;{pr}&lt;/code&gt;, &lt;code&gt;{branch}&lt;/code&gt;, &lt;code&gt;{branch-slug}&lt;/code&gt;, &lt;code&gt;{sha}&lt;/code&gt;, and &lt;code&gt;{short-sha}&lt;/code&gt;. A random Convex hostname can't be written that way, so a small Cloudflare Worker sits in front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://handoff-pr-{pr}.satimon.com&lt;/code&gt; returns a 302 to that PR's preview.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pr → {url, sha}&lt;/code&gt; mapping lives in a SQLite-backed Durable Object.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;preview&lt;/code&gt; job registers the mapping with a GitHub Actions OIDC token. There is no shared secret:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getIDToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://handoff-previews.satimon.com&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://handoff-previews.satimon.com/register&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nx"&gt;pr&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="nx"&gt;previewUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sha&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;Before it stores anything, the Worker verifies the token:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the RS256 signature, against GitHub's JWKS (a forced refresh for an unknown key ID at most once a minute);&lt;/li&gt;
&lt;li&gt;the issuer, the audience, expiry, and &lt;code&gt;nbf&lt;/code&gt;/&lt;code&gt;iat&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repository_id&lt;/code&gt; and &lt;code&gt;repository_owner_id&lt;/code&gt;, because a repository name can be reused after a rename;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event_name&lt;/code&gt; is &lt;code&gt;pull_request&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt; is &lt;code&gt;refs/pull/&amp;lt;pr&amp;gt;/merge&lt;/code&gt;, and &lt;code&gt;workflow_ref&lt;/code&gt; is &lt;code&gt;preview.yml&lt;/code&gt; at that ref.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A token for PR 12 can only register PR 12's alias, and only from the preview workflow. The Worker also accepts only &lt;code&gt;https://&amp;lt;name&amp;gt;.convex.site&lt;/code&gt; targets. &lt;a href="https://github.com/Satianurag/handoff/blob/main/infra/preview-alias/test/worker.test.mjs" rel="noopener noreferrer"&gt;Its tests&lt;/a&gt; cover a wrong audience, repository, owner, PR ref or workflow, push events, expired tokens, tampered signatures, unknown key IDs, &lt;code&gt;alg: none&lt;/code&gt;, and the refresh throttle. They run in CI.&lt;/p&gt;

&lt;p&gt;After registering, the job fetches the alias and fails unless it gets the expected 302. A broken alias fails the &lt;code&gt;preview&lt;/code&gt; check before TestSprite ever starts.&lt;/p&gt;

&lt;p&gt;Each alias hostname is a Workers custom domain, and Cloudflare allows 100 per zone. We created custom domains for PRs 1–60 ahead of time. Our PR #8 got no TestSprite run until its hostname existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the TestSprite trigger
&lt;/h2&gt;

&lt;p&gt;In the TestSprite portal, open the project, then &lt;strong&gt;GitHub Action → Add Trigger → Pull Request&lt;/strong&gt;. Paste a recent PR and click &lt;strong&gt;Detect Events&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Event&lt;/td&gt;
&lt;td&gt;The workflow-level &lt;code&gt;preview&lt;/code&gt; check&lt;/td&gt;
&lt;td&gt;It completes only after the alias works. Don't pick a per-PR deployment environment such as &lt;code&gt;preview-pr-9&lt;/code&gt;; later PRs won't match it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target URL pattern&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://handoff-pr-{pr}.satimon.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The stable alias from step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block PR until tests pass&lt;/td&gt;
&lt;td&gt;Off for the first few PRs, then on&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://docs.testsprite.com/web-portal/integrations/github-integration" rel="noopener noreferrer"&gt;TestSprite's docs&lt;/a&gt; recommend starting non-blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On PR #10, the &lt;code&gt;preview&lt;/code&gt; check completed at 14:49:52 UTC, and the TestSprite run started at 14:49:55. It ran all 16 tests against that PR's own preview, and all passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: make the gate real
&lt;/h2&gt;

&lt;p&gt;"Block PR until tests pass" makes TestSprite report a failed run through the &lt;code&gt;TestSprite V3&lt;/code&gt; status. GitHub only blocks a merge on a status that a rule requires. We added a branch ruleset on the default branch: &lt;strong&gt;Require status checks to pass → &lt;code&gt;TestSprite V3&lt;/code&gt;&lt;/strong&gt;, plus blocked force pushes and deletion. We left "Require a pull request" off because our merges are fast-forward pushes of the tested PR head; the required &lt;code&gt;TestSprite V3&lt;/code&gt; status still has to be green on that commit.&lt;/p&gt;

&lt;p&gt;For the pending-request bug, we first added a TestSprite test, then opened &lt;a href="https://github.com/Satianurag/handoff/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt; with a commit that changed no app code. TestSprite failed on the PR's preview, and GitHub kept the merge blocked. The fix commit turned the run green, and the merge unblocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong, and what we didn't fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A run that didn't reproduce.&lt;/strong&gt; Our first PR-triggered run stalled on &lt;code&gt;/demo&lt;/code&gt; (2 passed, 1 failed, 13 blocked). Single-test reruns in fresh browser contexts on production, the direct preview, and the alias all passed, and so did a full CLI run and the next PR run. We logged it as unexplained and changed no code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A red test that was our fault.&lt;/strong&gt; One plan used a role that correctly had no medical access. The app was right; we replaced the plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push runs don't gate.&lt;/strong&gt; The Push trigger on &lt;code&gt;main&lt;/code&gt; posts &lt;code&gt;TestSprite V3&lt;/code&gt; as success even when a test fails, because only the PR trigger blocks. It still caught bug 4, so we treat it as a production monitor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated plans were shallow.&lt;/strong&gt; TestSprite proposed 53 tests from the live app. We kept 4 and wrote 13 by hand for handovers, permissions, and negative paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Copy this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] A Convex preview deploy key stored as &lt;code&gt;CONVEX_PREVIEW_DEPLOY_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;preview.yml&lt;/code&gt; with a &lt;code&gt;deploy-preview&lt;/code&gt; job and a &lt;code&gt;preview&lt;/code&gt; job, with permissions scoped per job&lt;/li&gt;
&lt;li&gt;[ ] The alias Worker, with custom domains created ahead of time&lt;/li&gt;
&lt;li&gt;[ ] A TestSprite Pull Request trigger on the &lt;code&gt;preview&lt;/code&gt; check, with the alias as the Target URL pattern&lt;/li&gt;
&lt;li&gt;[ ] "Block PR until tests pass" on, once the suite is stable&lt;/li&gt;
&lt;li&gt;[ ] A branch ruleset that requires &lt;code&gt;TestSprite V3&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No TestSprite comment on a PR&lt;/td&gt;
&lt;td&gt;The alias hostname doesn't exist, so the &lt;code&gt;preview&lt;/code&gt; check failed&lt;/td&gt;
&lt;td&gt;Add the custom domain and redeploy the Worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The trigger never fires on new PRs&lt;/td&gt;
&lt;td&gt;It is bound to a per-PR event name&lt;/td&gt;
&lt;td&gt;Choose the workflow-level &lt;code&gt;preview&lt;/code&gt; check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A red &lt;code&gt;TestSprite V3&lt;/code&gt; doesn't block the merge&lt;/td&gt;
&lt;td&gt;No rule requires the status&lt;/td&gt;
&lt;td&gt;Add the ruleset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PreviewNotFound&lt;/code&gt; from &lt;code&gt;convex env set&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The preview doesn't exist yet&lt;/td&gt;
&lt;td&gt;Seed variables inside &lt;code&gt;convex deploy --cmd&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Over to you
&lt;/h2&gt;

&lt;p&gt;If your preview platform also hands out random URLs, how do you point PR tests at them? We would rather not run an alias service. A TestSprite option to use the GitHub Deployment's &lt;code&gt;environment_url&lt;/code&gt; as the target would remove step 2 entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Satianurag/handoff" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; · &lt;a href="https://github.com/Satianurag/handoff/blob/main/LOOP.md" rel="noopener noreferrer"&gt;LOOP.md with every run ID&lt;/a&gt; · &lt;a href="https://admired-fish-176.convex.site/demo" rel="noopener noreferrer"&gt;Try the sample family, no sign-up&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
    </item>
    <item>
      <title>My k6 run said 70% failed. SigNoz told me *why* — then my agent wrote the fix</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:40:58 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/my-k6-run-said-70-failed-signoz-told-me-why-then-my-agent-wrote-the-fix-18b9</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/my-k6-run-said-70-failed-signoz-told-me-why-then-my-agent-wrote-the-fix-18b9</guid>
      <description>&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%2F6vr84de1y3gm14ujfue5.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%2F6vr84de1y3gm14ujfue5.png" alt="omen cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was staring at a load-test number that looked decisive: &lt;strong&gt;~70% of requests failed&lt;/strong&gt;. Cool. Except that number alone is almost useless at 2am.&lt;/p&gt;

&lt;p&gt;Was it my new endpoint? Rate limiting? A bad client script? A flaky network? Or a real server bug?&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz&lt;/a&gt;, I built &lt;strong&gt;omen&lt;/strong&gt; — an agent that load-tests a code change with k6, correlates the same window against live SigNoz traces, names the root cause, and hands back a remediation diff. This post is what I actually learned wiring that loop on Windows with Gemini, Foundry, and the official SigNoz MCP server.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Satianurag/omen" rel="noopener noreferrer"&gt;github.com/Satianurag/omen&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem I kept hitting
&lt;/h2&gt;

&lt;p&gt;k6 is great at client-side truth: request count, p95, failure rate. It is terrible at answering “what broke &lt;em&gt;inside&lt;/em&gt; the service?”&lt;/p&gt;

&lt;p&gt;I planted a small FastAPI demo (&lt;code&gt;petclinic&lt;/code&gt;) with a bad write path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /api/visits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SQLite with &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;tiny timeout&lt;/li&gt;
&lt;li&gt;a fake sleep under lock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under concurrent load it fails hard. Client-side you see 5xx. Server-side, SigNoz sees the real error string: &lt;strong&gt;&lt;code&gt;database is locked&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That gap — client metrics vs server spans — is what omen exists for.&lt;/p&gt;




&lt;h2&gt;
  
  
  What omen is (short version)
&lt;/h2&gt;

&lt;p&gt;omen is a governed &lt;a href="https://github.com/apache/burr" rel="noopener noreferrer"&gt;Burr&lt;/a&gt; state machine served over MCP by &lt;a href="https://msradam.github.io/theodosia/" rel="noopener noreferrer"&gt;Theodosia&lt;/a&gt;. The driver sees one tool: &lt;code&gt;step&lt;/code&gt;. Illegal transitions get refused. Every step lands on a hash-chained ledger you can verify later.&lt;/p&gt;

&lt;p&gt;Rough walk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read a git diff (or an intent)&lt;/li&gt;
&lt;li&gt;Scaffold + generate a k6 script&lt;/li&gt;
&lt;li&gt;Validate and run it through Grafana’s k6 MCP&lt;/li&gt;
&lt;li&gt;Preflight the SigNoz service&lt;/li&gt;
&lt;li&gt;Correlate traces (&lt;code&gt;signoz_aggregate_traces&lt;/code&gt;, &lt;code&gt;signoz_search_traces&lt;/code&gt;, &lt;code&gt;signoz_get_trace_details&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Detect latency anomalies over the test window&lt;/li&gt;
&lt;li&gt;Analyze → groundedness screen → report + remediation&lt;/li&gt;
&lt;/ol&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%2Ftppi25gsbsbti6epc8r6.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%2Ftppi25gsbsbti6epc8r6.png" alt="omen state machine" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: &lt;code&gt;omen render&lt;/code&gt; — 16 actions, legal edges only. Note &lt;code&gt;signoz_preflight&lt;/code&gt; after &lt;code&gt;run_test&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How I used SigNoz (not “I used observability”)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Self-hosted via Foundry
&lt;/h3&gt;

&lt;p&gt;I deployed SigNoz with Foundry from the repo’s &lt;code&gt;casting.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;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;That gave me:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:8080&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OTLP gRPC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:4317&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SigNoz MCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:8000/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;API key went into &lt;code&gt;.env&lt;/code&gt; as &lt;code&gt;OMEN_SIGNOZ_API_KEY&lt;/code&gt;. No inline comments on that line — that bit me once and silently broke auth.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Official OpenTelemetry on the target app
&lt;/h3&gt;

&lt;p&gt;Demo apps run under stock &lt;code&gt;opentelemetry-instrument&lt;/code&gt;. No custom shipper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'petclinic'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:4317'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'grpc'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_TRACES_EXPORTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'otlp'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opentelemetry-instrument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples/petclinic/app.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a few omen runs, the SigNoz home page stopped looking empty:&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%2Fkgjyb3twdla2l1la9yjp.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%2Fkgjyb3twdla2l1la9yjp.png" alt="SigNoz home — petclinic ingestion" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: Traces + metrics ingestion active. Services widget shows &lt;code&gt;petclinic&lt;/code&gt; with ~70% error rate and ~596ms p99 over the last 30 minutes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That services row is the “something is on fire” view:&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%2Fxk5zotk593wbxqfkr3pk.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%2Fxk5zotk593wbxqfkr3pk.png" alt="SigNoz services table" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the service overview shows the load spikes from the k6 runs as clear humps in rate and latency:&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%2Fq6hg3zft289zs6dpemqx.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%2Fq6hg3zft289zs6dpemqx.png" alt="petclinic latency and rate" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: Two load bursts on &lt;code&gt;petclinic&lt;/code&gt; — same windows omen used for correlation.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Correlation through the SigNoz MCP (not hand-clicked UI)
&lt;/h3&gt;

&lt;p&gt;This was the interesting part for Track 01. omen does not scrape the UI. It talks to the official SigNoz MCP server during the run.&lt;/p&gt;

&lt;p&gt;On a real petclinic run (Gemini backend, local k6 MCP, SigNoz correlate), the phase stream looked like this:&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%2Fzez9rjlbf7uf07gw1i4x.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%2Fzez9rjlbf7uf07gw1i4x.png" alt="omen pilot correlate run" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: Terminal output from a full omen run — cards for each phase, including SigNoz tools.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The numbers from that run (not invented for this blog):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;k6: &lt;strong&gt;3897 requests&lt;/strong&gt;, p95 &lt;strong&gt;547.59ms&lt;/strong&gt;, &lt;strong&gt;~72% failed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz preflight: &lt;code&gt;service petclinic, exists=True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Correlate: &lt;code&gt;aggregate_traces&lt;/code&gt; ×9 + &lt;code&gt;search_traces&lt;/code&gt; + &lt;code&gt;get_trace_details&lt;/code&gt; ×3&lt;/li&gt;
&lt;li&gt;Worst path: &lt;code&gt;/api/visits&lt;/code&gt; &lt;strong&gt;72.1% 5xx&lt;/strong&gt;, top error &lt;strong&gt;&lt;code&gt;database is locked&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Guardian screen: &lt;strong&gt;grounded&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Verdict: server-side regression on &lt;code&gt;/api/visits&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Client p95 (~548ms) and server-side p95 (~543ms) lined up. That consistency mattered to me — if those numbers diverged wildly, I’d distrust the join.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The agent publishes itself back into SigNoz
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;report&lt;/code&gt;, omen emits &lt;code&gt;omen.run&lt;/code&gt; and &lt;code&gt;omen.step.*&lt;/code&gt; spans over OTLP (when &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt; is set). So the &lt;em&gt;agent walk&lt;/em&gt; is visible in the same backend that observed the app. That self-observability loop is half the point of this track.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix it proposed (and why it wasn’t hand-wavy)
&lt;/h2&gt;

&lt;p&gt;omen didn’t stop at “SQLite is sad.” It returned a validated unified diff that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raised the connect timeout (&lt;code&gt;0.25&lt;/code&gt; → &lt;code&gt;5.0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;enabled &lt;code&gt;PRAGMA journal_mode=WAL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;removed &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;removed the artificial &lt;code&gt;time.sleep(0.015)&lt;/code&gt; under the lock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s exactly the planted fault class. The Guardian check marked the analysis grounded against the cited telemetry before the verdict was sealed.&lt;/p&gt;

&lt;p&gt;Also: every stepped phase went through Theodosia’s mounted MCP &lt;code&gt;step&lt;/code&gt; tool, so the session wrote a real &lt;strong&gt;&lt;code&gt;ledger.jsonl&lt;/code&gt;&lt;/strong&gt;. &lt;code&gt;omen verify&lt;/code&gt; came back HMAC-keyed and intact — important if you’re going to claim “audited by construction” in a demo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotchas I only learned by breaking things
&lt;/h2&gt;

&lt;p&gt;These are the details that bit me on a fresh clone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows + Docker k6 is a trap.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;OMEN_K6_DOCKER=1&lt;/code&gt; with &lt;code&gt;--network host&lt;/code&gt; cannot reach &lt;code&gt;127.0.0.1&lt;/code&gt; demo apps on Windows. Local &lt;code&gt;k6 x mcp&lt;/code&gt; worked. Put &lt;code&gt;OMEN_K6_CMD=k6 x mcp&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; and leave Docker k6 off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;uv run omen&lt;/code&gt;, not bare &lt;code&gt;omen&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
After &lt;code&gt;uv sync&lt;/code&gt;, the &lt;code&gt;omen&lt;/code&gt; entrypoint lives in &lt;code&gt;.venv&lt;/code&gt;. Use &lt;code&gt;uv run omen …&lt;/code&gt; (or activate the venv). Same for pytest and scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;opentelemetry-bootstrap&lt;/code&gt; needs pip in a uv venv.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;uv run opentelemetry-bootstrap --action=install&lt;/code&gt; failed here with &lt;code&gt;No module named pip&lt;/code&gt;. Fix with &lt;code&gt;uv run python -m ensurepip --upgrade&lt;/code&gt;, or skip it — &lt;code&gt;uv sync&lt;/code&gt; already installs FastAPI + OTLP packages the demos need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runnable demos are on 8400–8404, not &lt;code&gt;:8000&lt;/code&gt; / &lt;code&gt;petstore&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;examples/petclinic&lt;/code&gt; serves &lt;code&gt;http://localhost:8400&lt;/code&gt;. &lt;code&gt;examples/petstore&lt;/code&gt; is OpenAPI-only (unit tests). SigNoz MCP itself is on &lt;code&gt;:8000/mcp&lt;/code&gt; — don’t confuse that with the app port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;omen pilot&lt;/code&gt; ≠ Gemini by default (until I fixed it).&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Phase workers already respected &lt;code&gt;OMEN_LLM=gemini&lt;/code&gt;. The pilot &lt;em&gt;driver&lt;/em&gt; still assumed Ollama/Granite. I fixed the non-Ollama path to walk the same mounted MCP server deterministically so Gemini runs still seal the ledger. Banner now says &lt;code&gt;Gemini is reading.&lt;/code&gt; (honest: Burr/MCP walks the graph; Gemini authors the work inside phases).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bypassing &lt;code&gt;mount()&lt;/code&gt; drops your ledger.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Calling &lt;code&gt;build_application().arun()&lt;/code&gt; (what &lt;code&gt;verify_scenario.py&lt;/code&gt; uses for a fast live check) can leave you with a thin or empty multi-step ledger listing. Driving through MCP &lt;code&gt;step&lt;/code&gt; via &lt;code&gt;omen pilot&lt;/code&gt; writes a real &lt;code&gt;ledger.jsonl&lt;/code&gt; that &lt;code&gt;omen verify&lt;/code&gt; can HMAC-check. Use verify_scenario to prove k6+SigNoz; use pilot when you need the audited session story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SigNoz API keys hate trailing comments.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;OMEN_SIGNOZ_API_KEY=.... # note&lt;/code&gt; broke auth. Key alone on the line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filename must be &lt;code&gt;.env&lt;/code&gt;, not &lt;code&gt;.env.txt&lt;/code&gt;.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Windows “Save as text” bit me once. omen only loads &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I’d tell myself on day one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Instrument the &lt;em&gt;target&lt;/em&gt; first. If SigNoz Services doesn’t show your app with a nasty error rate after a load test, stop debugging the agent.&lt;/li&gt;
&lt;li&gt;Correlate on a wall-clock window from the k6 run — not “last 1 hour of everything.”&lt;/li&gt;
&lt;li&gt;Prefer MCP tools that return structured aggregates (&lt;code&gt;signoz_aggregate_traces&lt;/code&gt;) before you drown in individual spans.&lt;/li&gt;
&lt;li&gt;Keep the agent’s own walk on OTLP. Judges (and future-you) can see whether the FSM actually ran.&lt;/li&gt;
&lt;li&gt;Demo a failure k6 alone can’t explain cleanly. &lt;code&gt;database is locked&lt;/code&gt; from traces beats “high error rate” from a client report.&lt;/li&gt;
&lt;li&gt;On Windows, never start from &lt;code&gt;OMEN_K6_DOCKER=1&lt;/code&gt; — local k6 first.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try the same path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;omen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;foundryctl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cast&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;casting.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;example&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Edit .env: OMEN_SIGNOZ_API_KEY, GEMINI_API_KEY, OMEN_LLM=gemini, OMEN_K6_CMD=k6 x mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Do NOT set OMEN_K6_DOCKER=1 on Windows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;omen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;warm-k6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# optional (needs pip in the venv):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# uv run python -m ensurepip --upgrade&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# uv run opentelemetry-bootstrap --action=install&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Fastest live proof (starts petclinic :8400 for you):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;scripts/verify_scenario.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;petclinic&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Audited pilot path (full ledger) — terminal 1:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'petclinic'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'http://localhost:4317'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'grpc'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OTEL_TRACES_EXPORTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'otlp'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opentelemetry-instrument&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples/petclinic/app.py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# terminal 2 — intent mode (or --ref HEAD~1 against a two-commit demo repo)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;omen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pilot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--intent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"load test recording a new visit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--repo-path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;examples/petclinic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--target-base-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http://127.0.0.1:8400&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--signoz-service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;petclinic&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;omen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sessions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ls&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;omen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;verify&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;app-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; → Services → &lt;code&gt;petclinic&lt;/code&gt;. If you see a fat error rate after the run, the loop is alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I didn’t learn that “observability is important.” I learned that a 70% k6 failure rate is a &lt;em&gt;question&lt;/em&gt;, and SigNoz traces are where the answer lives — especially when an agent can query those traces through MCP in the same window as the load test and come back with a grounded fix.&lt;/p&gt;

&lt;p&gt;That’s omen. Divines disaster, crafts the cure — but only because SigNoz made the server side legible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project: &lt;a href="https://github.com/Satianurag/omen" rel="noopener noreferrer"&gt;github.com/Satianurag/omen&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Hackathon: &lt;a href="https://www.wemakedevs.org/hackathons/signoz" rel="noopener noreferrer"&gt;Agents of SigNoz&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz MCP docs: &lt;a href="https://signoz.io/docs/ai/signoz-mcp-server" rel="noopener noreferrer"&gt;signoz.io/docs/ai/signoz-mcp-server&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Blog guide I followed: &lt;a href="https://www.wemakedevs.org/hackathons/signoz/blog-guide" rel="noopener noreferrer"&gt;WeMakeDevs Blog Guide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>python</category>
    </item>
    <item>
      <title>Immich promised OpenTelemetry traces. SigNoz showed me metrics instead — and that's what found the 750ms spike</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:22:03 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/gbgfbgf-5c8j</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/gbgfbgf-5c8j</guid>
      <description>&lt;p&gt;I wanted one screenshot for a blog: Immich showing up under SigNoz &lt;strong&gt;Services&lt;/strong&gt;, with a nice flamegraph. I never got that screenshot.&lt;/p&gt;

&lt;p&gt;What I got instead was more useful — Immich's &lt;em&gt;real&lt;/em&gt; Prometheus histograms in SigNoz, a dashboard that showed album list p99 drop from ~25ms to ~7ms after warmup, and a thumbnail-generation spike at &lt;strong&gt;~750ms p95&lt;/strong&gt;. This is the path that actually works today, and the one people keep getting wrong.&lt;/p&gt;

&lt;p&gt;If you're trying to connect Immich → SigNoz right now, start here. Don't start with the NestJS auto-instrumentation guide.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Immich + SigNoz?
&lt;/h2&gt;

&lt;p&gt;Immich is the self-hosted Google Photos alternative a lot of us already run. It already uses OpenTelemetry (&lt;code&gt;nestjs-otel&lt;/code&gt;). SigNoz is the place I wanted those signals.&lt;/p&gt;

&lt;p&gt;Immich's own docs say Prometheus metrics are first-class, and that &lt;em&gt;exporting traces is also possible&lt;/em&gt; because of OpenTelemetry — then they document only Prometheus. That gap is exactly why &lt;a href="https://github.com/immich-app/immich/issues/14045" rel="noopener noreferrer"&gt;people open issues&lt;/a&gt; asking how to point Immich at SigNoz.&lt;/p&gt;

&lt;p&gt;I decided to stop reading and just run both stacks on one machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My lab:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Version / URL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SigNoz&lt;/td&gt;
&lt;td&gt;v0.133.0 (self-hosted, Foundry) → &lt;code&gt;http://localhost:3301&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immich&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;release&lt;/code&gt; image (v3.0.3) → &lt;code&gt;http://localhost:2283&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bridge&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;otel/opentelemetry-collector-contrib:0.120.0&lt;/code&gt; scraping Immich &lt;code&gt;/metrics&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Mistake #1: treating Immich like a blank NestJS app
&lt;/h2&gt;

&lt;p&gt;SigNoz's NestJS docs (and half of Reddit) say: set &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--require @opentelemetry/auto-instrumentations-node/register"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Immich that is a trap. Immich &lt;strong&gt;already&lt;/strong&gt; boots its own OpenTelemetry SDK for Prometheus. Adding auto-instrumentation gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: @opentelemetry/api: Attempted duplicate registration of API: trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the same crash people hit in &lt;a href="https://github.com/immich-app/immich/issues/14045" rel="noopener noreferrer"&gt;immich#14045&lt;/a&gt;. I reproduced the shape of the problem by inspecting the running container: Immich ships &lt;code&gt;@opentelemetry/exporter-prometheus&lt;/code&gt;. It does &lt;strong&gt;not&lt;/strong&gt; ship an OTLP trace exporter. So env vars like &lt;code&gt;OTEL_TRACES_EXPORTER=otlp&lt;/code&gt; look correct and do almost nothing for traces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule I wish I'd known on day one:&lt;/strong&gt; if the app initializes OTel itself, don't also &lt;code&gt;--require&lt;/code&gt; the Node auto-instrumentation pack.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually works: scrape Immich, export OTLP to SigNoz
&lt;/h2&gt;

&lt;p&gt;Immich exposes Prometheus on port &lt;code&gt;8081&lt;/code&gt; when you enable telemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMMICH_TELEMETRY_INCLUDE=all
IMMICH_API_METRICS_PORT=8081
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I put Immich and SigNoz on the same Docker network (&lt;code&gt;signoz-network&lt;/code&gt;), then ran a tiny collector that scrapes Immich and forwards OTLP to SigNoz's ingester:&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="c1"&gt;# configs/otel-collector.yaml (trimmed)&lt;/span&gt;
&lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;immich-api-metrics&lt;/span&gt;
          &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
          &lt;span class="na"&gt;metrics_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/metrics&lt;/span&gt;
          &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;immich-server:8081"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# service name, NOT container name&lt;/span&gt;

&lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz-ingester-1:4317&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gotcha that wasted an evening
&lt;/h3&gt;

&lt;p&gt;My first scrape target was &lt;code&gt;immich_server:8081&lt;/code&gt; (container name). Collector logs said &lt;code&gt;Failed to scrape Prometheus endpoint&lt;/code&gt; forever. Docker DNS resolves the &lt;strong&gt;Compose service name&lt;/strong&gt; (&lt;code&gt;immich-server&lt;/code&gt;), not the &lt;code&gt;container_name&lt;/code&gt;. One hyphen later, the collector started printing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metrics ... resource metrics: 1, metrics: 77, data points: 122
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then SigNoz Metrics Explorer lit up when I searched &lt;code&gt;immich&lt;/code&gt;.&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%2Fyv30stwvt9r5aq6f5ib9.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%2Fyv30stwvt9r5aq6f5ib9.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1 — SigNoz Metrics Explorer autocomplete for `immich_&lt;/em&gt;` histograms.*&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that still doesn't work (be honest)
&lt;/h2&gt;

&lt;p&gt;I opened &lt;strong&gt;Services&lt;/strong&gt;. Empty. &lt;strong&gt;Traces&lt;/strong&gt;. Empty. Service map? Nothing.&lt;/p&gt;

&lt;p&gt;That isn't SigNoz being broken. Services/APM in SigNoz are built from &lt;strong&gt;spans&lt;/strong&gt;. Immich's stock image only exports Prometheus metrics from its NodeSDK. No spans → no APM row.&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%2Fj3pw1qy9u62jjrld7q0r.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%2Fj3pw1qy9u62jjrld7q0r.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2 — SigNoz Services with no Immich traces. This is expected today.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So if your blog (or your weekend) depends on a pretty Immich flamegraph from the official image alone: change the goal. Instrument a sidecar, fork Immich's telemetry bootstrap, or wait for native OTLP traces. Don't pretend env vars fixed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature-rich SigNoz without traces: dashboard + Metrics Explorer
&lt;/h2&gt;

&lt;p&gt;I built a dashboard called &lt;strong&gt;Immich Observability&lt;/strong&gt; with six panels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Album list p99
&lt;/li&gt;
&lt;li&gt;Thumbnail generation p95
&lt;/li&gt;
&lt;li&gt;CLIP encode p95
&lt;/li&gt;
&lt;li&gt;Face detection p95
&lt;/li&gt;
&lt;li&gt;Event bus emit p99
&lt;/li&gt;
&lt;li&gt;Thumbnail jobs / second
&lt;/li&gt;
&lt;/ol&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%2Ftkg2l865nm72lolr42h9.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%2Ftkg2l865nm72lolr42h9.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3 — Immich Observability dashboard in SigNoz.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The album panel was the first “okay, this is worth it” moment. Fresh after restart, album list p99 sat around &lt;strong&gt;~25ms&lt;/strong&gt;. After a few dozen API hits it settled near &lt;strong&gt;~7ms&lt;/strong&gt;. Same endpoint, same machine — warmup is not a myth when you can see it.&lt;/p&gt;

&lt;p&gt;Then thumbnail generation showed a &lt;strong&gt;p95 near 750ms&lt;/strong&gt; on a real upload path. Client-side curl for a cached thumbnail was ~6ms. Server-side &lt;em&gt;generation&lt;/em&gt; is a different animal. That distinction is exactly why repo-level histograms beat “the API felt slow.”&lt;/p&gt;




&lt;h2&gt;
  
  
  Numbers from the load I actually ran
&lt;/h2&gt;

&lt;p&gt;I hammered the Immich API with a script (warmup + repeated reads + photo upload). Client-side timings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/server/ping&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;2.1ms&lt;/td&gt;
&lt;td&gt;3.2ms&lt;/td&gt;
&lt;td&gt;4.6ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/users/me&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;4.6ms&lt;/td&gt;
&lt;td&gt;6.6ms&lt;/td&gt;
&lt;td&gt;9.8ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/albums&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;6.1ms&lt;/td&gt;
&lt;td&gt;12.6ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28.3ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/assets/{id}/thumbnail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;6.2ms&lt;/td&gt;
&lt;td&gt;9.0ms&lt;/td&gt;
&lt;td&gt;15.3ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /api/assets/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;16.2ms&lt;/td&gt;
&lt;td&gt;31.2ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.6ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POST /api/assets&lt;/code&gt; (upload)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;44.6ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Server-side histograms after a second load batch (from &lt;code&gt;/metrics&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;avg&lt;/th&gt;
&lt;th&gt;count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;immich_event_repository_emit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38.8ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;immich_asset_repository_create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21.9ms&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;immich_asset_repository_upsert_exif&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;22.8ms&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;immich_machine_learning_repository_check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27.2ms&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Early in the session, before more traffic diluted the average, &lt;code&gt;event_repository_emit&lt;/code&gt; showed &lt;strong&gt;~252ms&lt;/strong&gt; on a tiny sample — the kind of spike you'd never notice from &lt;code&gt;curl&lt;/code&gt; alone if you only look at the happy path.&lt;/p&gt;

&lt;p&gt;None of these numbers are “production SLOs.” They're what my laptop + Docker setup produced on 2026-07-19. Your SSD and CPU will disagree. The &lt;em&gt;method&lt;/em&gt; is what transfers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Minimal Immich override (copy-paste)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.override.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;immich-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;signoz-network&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2283:2283"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8081:8081"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;IMMICH_TELEMETRY_INCLUDE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
      &lt;span class="na"&gt;IMMICH_API_METRICS_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;

  &lt;span class="na"&gt;immich-prometheus-scraper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;otel/opentelemetry-collector-contrib:0.120.0&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config=/etc/otelcol/config.yaml"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./configs/otel-collector.yaml:/etc/otelcol/config.yaml:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;signoz-network&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;immich-server&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;signoz-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bring SigNoz up first so &lt;code&gt;signoz-network&lt;/code&gt; and &lt;code&gt;signoz-ingester-1:4317&lt;/code&gt; exist. Then &lt;code&gt;docker compose up -d&lt;/code&gt; in the Immich folder.&lt;/p&gt;

&lt;p&gt;Working configs from this write-up live under &lt;code&gt;configs/&lt;/code&gt; in the companion repo (dashboard JSON export included).&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell myself last week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't chase the Services page first.&lt;/strong&gt; For stock Immich, Metrics Explorer + a custom dashboard is the win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never stack two OTel SDKs.&lt;/strong&gt; Skip &lt;code&gt;NODE_OPTIONS=--require @opentelemetry/...&lt;/code&gt; on Immich.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name DNS correctly.&lt;/strong&gt; &lt;code&gt;immich-server&lt;/code&gt;, not &lt;code&gt;immich_server&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at histograms, not vibes.&lt;/strong&gt; Album list warmup (~25ms → ~7ms) and thumbnail generation (~750ms p95) are the kind of findings that change how you size a homelab box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the failure.&lt;/strong&gt; The missing traces story is half the value. Someone else will burn a weekend on the same env vars.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Immich's docs weren't lying when they said OpenTelemetry is in the tree. They just didn't tell you the exporter story. SigNoz didn't need me to invent a new photo app — it needed an honest scrape pipeline and a dashboard that charts the metrics Immich already emits.&lt;/p&gt;

&lt;p&gt;I still want native Immich → OTLP traces. Until then, this is the setup I'll leave running: Immich for photos, SigNoz for the slow bits that aren't the thumbnails I already cached.&lt;/p&gt;




&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Immich monitoring docs: &lt;a href="https://docs.immich.app/features/monitoring/" rel="noopener noreferrer"&gt;https://docs.immich.app/features/monitoring/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Immich OTel / SigNoz discussion: &lt;a href="https://github.com/immich-app/immich/discussions/14062" rel="noopener noreferrer"&gt;https://github.com/immich-app/immich/discussions/14062&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz: &lt;a href="https://signoz.io" rel="noopener noreferrer"&gt;https://signoz.io&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>signoz</category>
      <category>opentelemetry</category>
      <category>immich</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>f</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sun, 15 Feb 2026 07:18:31 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/f-4ibn</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/f-4ibn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>t</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sun, 08 Feb 2026 13:46:12 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/t-2ah</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/t-2ah</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/algolia"&gt;Algolia Agent Studio Challenge&lt;/a&gt;: Consumer-Facing Conversational Experiences&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Used Algolia Agent Studio
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why Fast Retrieval Matters
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>algoliachallenge</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Fg</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sat, 07 Feb 2026 17:21:26 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/fg-n0d</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/fg-n0d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/algolia"&gt;Algolia Agent Studio Challenge&lt;/a&gt;: Consumer-Facing Conversational Experiences&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Used Algolia Agent Studio
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why Fast Retrieval Matters
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>algoliachallenge</category>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>h</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sun, 01 Feb 2026 13:02:15 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/h-3dc9</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/h-3dc9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge Presented by Google AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Portfolio
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What I'm Most Proud Of
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>portfolio</category>
      <category>gemini</category>
    </item>
    <item>
      <title>a</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Fri, 02 Jan 2026 07:41:18 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/a-904</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/a-904</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mux-2025-12-03"&gt;DEV's Worldwide Show and Tell Challenge Presented by Mux&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  My Pitch Video
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Story Behind It
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Technical Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use of Mux (Additional Prize Category Participants Only)
&lt;/h3&gt;

</description>
      <category>devchallenge</category>
      <category>muxchallenge</category>
      <category>showandtell</category>
      <category>video</category>
    </item>
    <item>
      <title>g</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Mon, 15 Dec 2025 06:50:56 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/g-1g89</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/g-1g89</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/xano-2025-11-20"&gt;Xano AI-Powered Backend Challenge&lt;/a&gt;: Production-Ready Public API&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  API Documentation
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The AI Prompt I Used
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Refined the AI-Generated Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  My Experience with Xano
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>xanochallenge</category>
      <category>api</category>
      <category>backend</category>
    </item>
    <item>
      <title>f</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Sun, 14 Dec 2025 08:27:55 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/f-2n2n</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/f-2n2n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/xano-2025-11-20"&gt;Xano AI-Powered Backend Challenge&lt;/a&gt;: Full-Stack, AI-First Application&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The AI Prompt I Used
&lt;/h2&gt;

&lt;h2&gt;
  
  
  How I Refined the AI-Generated Code
&lt;/h2&gt;

&lt;h2&gt;
  
  
  My Experience with Xano
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>xanochallenge</category>
      <category>ai</category>
      <category>backend</category>
    </item>
    <item>
      <title>an</title>
      <dc:creator>Anurag Sati</dc:creator>
      <pubDate>Mon, 08 Dec 2025 07:11:15 +0000</pubDate>
      <link>https://dev.to/anurag_sati_5b77a6578f780/a-5fef</link>
      <guid>https://dev.to/anurag_sati_5b77a6578f780/a-5fef</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/unoplatform"&gt;AI Challenge for Cross-Platform Apps&lt;/a&gt; - AI Acceleration&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h2&gt;
  
  
  AI Tooling in Action
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Using App MCP and Uno Platform MCP
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Cross-Platform Reach
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Development Experience
&lt;/h2&gt;

</description>
      <category>devchallenge</category>
      <category>unoplatformchallenge</category>
      <category>dotnet</category>
      <category>crossplatform</category>
    </item>
  </channel>
</rss>
