← Sentry Init / API
Your token

Driving Sentry Init from your own code

Everything the web page does is one HTTP API. The base URL is https://api.skillsafe.ai/v1/app-api, every call carries Authorization: Bearer <token>, and every response is the same envelope. The app is identified by the token, not by a path segment — the app slug never appears in a request path, and a route built by inserting it returns 404.

The task field comes first

Sentry Init is one app with four lanes over the same work object: the init call your project already ships. Every request must carry a task, and the reply is that lane's contract and no other. A request with no task is answered by the closest lane, with the chosen lane named in lane and explained in the first sentence of summary — useful as a fallback, not as a way of asking.

tasklaneverdict valuesartifact_json.kindwhat you get
auditConfiguration auditproduction-ready | minor-gaps | incomplete | misconfiguredconfig_auditthe option-by-option verdict per init block, plus the corrected init call
privacyPrivacy reviewcontained | needs-scrubbing | exposedprivacy_planthe data categories that can reach Sentry, and a complete beforeSend scrubber
budgetSampling planwithin-quota | tight | over-quotasampling_planthe sample rates that fit the quota, and what each change costs in visibility
signalsSignal coveragecovered | partial | blindsignal_plansignal-by-signal coverage, the alert rules worth creating, and OTel exporter wiring

The input fields

Taken from readForm() in app.js, which is what the page actually sends. Only task and config are required; everything else improves the answer and several fields make a whole section of it possible.

fieldtypewhat it is
taskstring, requiredOne of audit, privacy, budget, signals. Routes the run to a lane.
configstring, requiredThe init call itself. Several files in one string is normal - separate them with a // file: path marker line. The page masks credential-shaped values before this field is built; if you are calling the API directly, mask them yourself.
platformsstringComma-joined platform labels the browser detected, e.g. Next.js, Python. Advisory - the model re-reads the config.
environmentstringproduction, staging, mixed or unknown.
strictnessstringbalanced, strict or pragmatic.
masking_onbooleanWhether the placeholders in config replaced real values.
volumesobjecterrors, pageViews, sessions, requests, spansPerTransaction - all numbers, all per month. Zero means not supplied.
quotasobjecterrors, spans, replays, profiles - the plan allowance. Zero means not supplied.
budgetobjectThe arithmetic the browser already did: rows[] with the monthly figure, the allowance, whether it is over and the rate that would fit, plus assumptions[]. The prompt treats this as authoritative and does not recompute it.
prescanobjectThe parse: blocks[] (platform, file, syntax, options, DSN structure) and flags[], each with a unique flag_id, its rule, a severity and the block it belongs to.
notesstringFree text the config cannot carry - the plan you are on, why a rate was changed.
upstreamstringThe digest of a previous lane's result, when one lane hands over to the next.

The traffic fields are load-bearing for one lane. With volumes left empty, budget.computed is false and the budget lane will say the arithmetic could not be done rather than inventing an average. That is deliberate.

The envelope

Every response is {"ok":true,"data":{...}} or {"ok":false,"error":{"code":"...","message":"..."}}, with a meta.request_id on both. Job output arrives as a string inside data.output.output — parse that string as JSON to get the app's reply object.

statuserror.codewhat it means
401UNAUTHORIZEDNo token, or a token that is not for this app. Mint one with POST /guest.
402INSUFFICIENT_CREDITSThe balance is under min_credits. /estimate is free and tells you the number in advance.
404NOT_FOUNDUsually a path with the app slug in it. The slug never appears in a request path - the token identifies the app.
409CONFLICTAn Idempotency-Key reused with a different body.
422VALIDATION_ERRORThe input object was not accepted - most often task missing or not one of the four lane ids.
429RATE_LIMITEDBack off and retry. Never tight-loop.
503UNAVAILABLEThe model tier is briefly unavailable. Retry with the same Idempotency-Key.

The client used below

# A token first (see step 1). Everything below reuses it.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"

Step 1 — get a token

A guest token is enough for /me and /estimate. Running a lane is metered, so it needs a personal token — the easy way to get one is the token page, which signs in and shows you the token this browser holds. Reuse one token: every POST /guest mints a new identity, and run history is scoped to the identity that wrote it.

curl -s -X POST "$BASE/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"sentry-init"}'
# -> {"ok":true,"data":{"token":"aut_...","subject_type":"guest", ...}}

Step 2 — check the token with /me

subject_type is user or guest; credits is the balance a metered run draws on. Compare it against min_credits from the estimate before you submit, rather than discovering a 402 afterwards.

curl -s "$BASE/me" -H "Authorization: Bearer $TOKEN"

Step 3 — price the run with /estimate

/estimate is free and creates no job. It returns the model binding as well as the price, which makes it the cheapest way to assert an app is wired to the model you think it is: model reads gpt-5.6-terra, model_alias reads gpt-terra, and markup_bps is 1000. Estimate the exact payload you are about to run — the hold differs per lane.

curl -s -X POST "$BASE/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d @input.json
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#                       "markup_bps":1000,"hold_credits":1627,"min_credits":198,
#                       "sponsor_enabled":false}}

The request body

Trimmed for the page; prescan.blocks and budget.rows are full objects.

{
  "task": "audit",
  "config": "// file: instrumentation-client.ts\nimport * as Sentry from \"@sentry/nextjs\";\n\nSentry.init({\n  dsn: \"https://[DSN-KEY-1]@o4508912.ingest.us.sentry.io/4508913\",\n  environment: \"production\",\n  tracesSampleRate: 1.0,\n});",
  "platforms": "Next.js",
  "environment": "production",
  "strictness": "balanced",
  "masking_on": true,
  "volumes": {
    "errors": 90000,
    "pageViews": 1800000,
    "sessions": 350000,
    "requests": 4200000,
    "spansPerTransaction": 10
  },
  "quotas": {
    "errors": 50000,
    "spans": 10000000,
    "replays": 50000,
    "profiles": 100000
  },
  "budget": {
    "computed": true,
    "verdict": "over-quota",
    "rows": [
      "... see below ..."
    ]
  },
  "prescan": {
    "readable": true,
    "platforms": [
      "Next.js"
    ],
    "blocks": [
      "... one entry per init block ..."
    ],
    "flags": [
      {
        "flag_id": "traces-full-b1-tracessamplerate",
        "rule": "traces-full",
        "severity": "high",
        "block": 1,
        "option": "tracesSampleRate",
        "title": "tracesSampleRate is 1.0",
        "detail": "Every transaction is sent."
      }
    ]
  },
  "notes": "",
  "upstream": ""
}

Step 4 — run a lane and poll the job

POST /run returns a job_id immediately; poll GET /jobs/{job_id} until status is succeeded or failed. Always send an Idempotency-Key derived from the task, the input and an attempt counter — a retried network blip must not bill twice, and the key must include the task because two lanes over one config are two distinct runs.

# The idempotency key must be stable for one (task, input, attempt).
KEY="sentry-init:audit:$(shasum input.json | cut -c1-16):1"

JOB=$(curl -s -X POST "$BASE/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d @input.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')

# poll until terminal
until curl -s "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN" \
  | tee /dev/stderr | grep -q '"status":"\(succeeded\|failed\)"'; do sleep 2; done

Step 5 — stream it instead

POST /run-stream is the same call over SSE. Three event types arrive: job once at the start, delta repeatedly, and done at the end with charged_credits and truncated. Each delta carries the whole text so far, not an increment — assign it, do not append it.

curl -N -X POST "$BASE/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d @input.json

# event: job    {"job_id":"job_..."}
# event: delta  {"text":"{\"lane\":\"audit\",\"title\":\"..."}
# event: done   {"output":"...","charged_credits":412,"truncated":false}

The output contract

One JSON object, the same envelope in every lane. The page parses it with JSON.parse after stripping an optional code fence, so prose around the object is a defect rather than a decoration.

{
  "lane": "audit" | "privacy" | "budget" | "signals",
  "title": "short name for this run",
  "verdict": "one of the lane's values",
  "headline": "one sentence under 160 characters",
  "summary": "two to four sentences",
  "checks":   [ { "name", "value", "verdict": "good|weak|missing|risky|not-applicable", "note" } ],
  "findings": [ { "id", "severity": "critical|high|medium|low", "option", "block": 1,
                  "quote", "why", "fix" } ],
  "rates":    [ { "signal": "errors|spans|replays|profiles|logs|crons",
                  "current": "1.0", "recommended": "0.1", "events_per_month": 0, "note" } ],
  "artifact": "the lane's document, in Markdown",
  "artifact_json": { "kind": "config_audit|privacy_plan|sampling_plan|signal_plan", ... },
  "coverage_check": [ { "flag_id", "status": "confirmed|cleared|not-applicable", "note" } ],
  "questions": [ "..." ],
  "confidence": "high|medium|low"
}

coverage_check is the accountability mechanism. Every flag the browser raised carries a unique flag_id, and the reply must answer each one exactly once. The page shows any flag left unanswered and any id that was invented, so a reply that ignores the prescan is visible rather than plausible.

The same rule fires once per init block, so ids are suffixed with the block: before-send-missing-b1 and before-send-missing-b2 are two separate answers. Read prescan.flags[].rule when you want to group them.

Worked example: task: "audit"

Required in this lane: title, verdict, headline, summary, checks, findings, artifact, artifact_json, coverage_check, confidence. Verdict is one of production-ready | minor-gaps | incomplete | misconfigured, and artifact_json.kind must be config_audit.

Reply (trimmed)

{
  "lane": "audit",
  "title": "Next.js Sentry Init \u2014 Audit (browser + server)",
  "verdict": "minor-gaps",
  "headline": "Errors, tracing and replay all reach Sentry, but no release on either side and a 100% browser trace rate need fixing before this scales.",
  "summary": "This audits the two Next.js Sentry.init calls you pasted \u2014 instrumentation-client.ts (browser) and sentry.server.config.ts (server) \u2014 both pointed at the same project, 4508913. The essentials are present: a valid DSN, environment set to production, tracing enabled on both sides, and Session Replay on in the browser. The one gap present on both blocks that actually costs a named feature is release (no source maps, no release health); a missing beforeSend and unstated replay-masking options are real, but for this lane \u2014 is the init correct and complete \u2014 they are notes rather than correctness gaps, since the SDK already behaves safely by default. The browser's tracesSampleRate of 1.0 is the one setting worth changing this week.",
  "checks": [
    {
      "name": "dsn (browser, instrumentation-client.ts)",
      "value": "https://[DSN-KEY-1]@o4508912.ingest.us.sentry.io/4508913",
      "verdict": "good",
      "note": "Identifies the project events are sent to; DSN parses as valid with no legacy secret."
    },
    {
      "name": "environment (browser)",
      "value": "production",
      "verdict": "good",
      "note": "Separates production events from other stages in issue lists, alerts and release health."
    },
    "... 18 more ..."
  ],
  "findings": [
    {
      "id": "traces-full-b1-tracessamplerate",
      "severity": "high",
      "option": "tracesSampleRate",
      "block": 1,
      "quote": "tracesSampleRate: 1.0",
      "why": "Every browser transaction is sent at 100%. That is fine for a first day in production and expensive for a month of real traffic \u2014 it is the single most common way a Sentry span bill grows without anything actually breaking.",
      "fix": "tracesSampleRate: 0.2, // was 1.0 (100%); matches the server rate below and cuts browser span volume roughly 5x \u2014 refine the exact number against your quota in the budget lane"
    },
    "... 11 more ..."
  ],
  "rates": [],
  "artifact": "# SENTRY-AUDIT.md\n\n## Block 1 \u2014 `instrumentation-client.ts` (Next.js, browser)\n\n**Verdict: minor-gaps**\n\n| # | Option | Set to | Verdict | What turns on it |\n|---|--------|--------|---------|-------------------|\n| 1 | dsn | `https://[DSN-KEY-1]@o4508912.ingest.us.sentry.io/4508913` | good | Sends ev\n... (full Markdown document) ...",
  "artifact_json": {
    "kind": "config_audit",
    "score": 72,
    "blocks": [
      {
        "index": 1,
        "platform": "Next.js",
        "file": "instrumentation-client.ts",
        "verdict": "minor-gaps",
        "missing": [
          "release",
          "beforeSend",
          "tracePropagationTargets",
          "tunnel",
          "denyUrls",
          "ignoreErrors"
        ]
      },
      {
        "index": 2,
        "platform": "Next.js",
        "file": "sentry.server.config.ts",
        "verdict": "minor-gaps",
        "missing": [
          "release",
          "beforeSend"
        ]
      }
    ]
  },
  "coverage_check": [
    {
      "flag_id": "traces-full-b1-tracessamplerate",
      "status": "confirmed",
      "note": "Agreed with the prescan's high severity \u2014 captures every browser transaction; addressed with a lower recommended rate in findings."
    },
    {
      "flag_id": "release-missing-b1-release",
      "status": "confirmed",
      "note": "Agreed \u2014 no release key in the browser init; kept at the prescan's medium, since release health has no release to measure without it."
    },
    "... 10 more, one per prescan flag_id ..."
  ],
  "questions": [
    "Does the installed @sentry/nextjs version auto-register a profiling integration when profilesSampleRate is set on the server, or does @sentry/profiling-node's nodeProfilingIntegration() need to be added to sentry.server.config.ts by hand? The pasted server block sets profilesSampleRate: 1.0 with no integrations array at all, and I can't tell from the pasted text alone which behavior applies.",
    "What is the @sentry/nextjs package version? Option names and defaults for tracePropagationTargets and browserTracingIntegration have changed across major versions."
  ],
  "confidence": "medium"
}

Worked example: task: "privacy"

Required in this lane: title, verdict, headline, summary, checks, findings, artifact, artifact_json, coverage_check, confidence. Verdict is one of contained | needs-scrubbing | exposed, and artifact_json.kind must be privacy_plan.

Reply (trimmed)

{
  "lane": "privacy",
  "title": "Sentry init ships PII, unmasked replay and a legacy DSN secret",
  "verdict": "exposed",
  "headline": "sendDefaultPii, unmasked Session Replay and a legacy-secret DSN are sending real user data to Sentry right now -- fix those three before anything else.",
  "summary": "This is the privacy lane: what this pasted init sends about your users, not whether it is correctly configured. Both the browser block (src/monitoring.js) and the Node block (server/instrument.js) ship sendDefaultPii: true with no beforeSend, so IP addresses, cookies and the Authorization header are attached to every event unfiltered today. The browser block also records every session with Session Replay's masking switched off (maskAllText, maskAllInputs and blockAllMedia are all false), the server block ships every stack frame's local variables, and the browser DSN still carries a legacy key:secret pair. Given this is a healthcare product, verdict is exposed.",
  "checks": [
    {
      "name": "Request data \u2014 IP address, cookies & Authorization header",
      "value": "sendDefaultPii: true in both blocks; no beforeSend in either",
      "verdict": "risky",
      "note": "Set sendDefaultPii: false, or keep it true and strip request.headers.authorization/.cookie and user.ip_address in beforeSend."
    },
    {
      "name": "Session replay DOM \u2014 on-screen text, form inputs, media",
      "value": "replayIntegration({ maskAllText: false, maskAllInputs: false, blockAllMedia: false }), replaysSessionSampleRate: 1.0",
      "verdict": "risky",
      "note": "Remove the three false overrides so the SDK's masking defaults (all true) apply, or replace them with the sentry-mask / sentry-unmask CSS-class allowlist."
    },
    "... 3 more ..."
  ],
  "findings": [
    {
      "id": "dsn-legacy-secret-b1-dsn",
      "severity": "critical",
      "option": "dsn",
      "block": 1,
      "quote": "dsn: \"https://[DSN-KEY-1]:[DSN-SECRET-1]@o4501234.ingest.us.sentry.io/4501235\"",
      "why": "The DSN carries a key:secret@ pair. The secret half has not been required since 2016; anyone who has this string can write events into project 4501235 as if they were you. That is a live credential shipped in a browser bundle, which is exactly the kind of thing legal asked about when they asked what Sentry can see.",
      "fix": "Rotate the client key in Sentry (Settings > Client Keys), then ship only the key half: dsn: \"https://[DSN-KEY-1]@o4501234.ingest.us.sentry.io/4501235\", -- replace [DSN-KEY-1] with the newly issued key."
    },
    "... 11 more ..."
  ],
  "rates": [],
  "artifact": "# SENTRY-PRIVACY.md\n\n## What this init sends about your users today\n\nTwo init blocks were pasted: a browser bundle (`src/monitoring.js`, `@sentry/browser`) and a\nserver process (`server/instrument.js`, `@sentry/node`). Read together, this configuration is\n**exposed**: personal data is leaving Sentry\n... (full Markdown document) ...",
  "artifact_json": {
    "kind": "privacy_plan",
    "exposed_categories": [
      "request-headers",
      "replay-dom",
      "local-variables",
      "breadcrumb-urls",
      "trace-headers",
      "dsn-credential"
    ],
    "controls": [
      {
        "option": "sendDefaultPii",
        "set_to": "false",
        "block": 1,
        "why": "Stops the browser SDK from attaching the visitor's IP address to every event by default."
      },
      {
        "option": "sendDefaultPii",
        "set_to": "false",
        "block": 2,
        "why": "Stops the Node SDK from attaching the request's IP address, cookies and Authorization header to every event by default."
      },
      {
        "option": "maskAllText",
        "set_to": "true",
        "block": 1,
        "why": "Restores DOM text masking in Session Replay recordings."
      },
      {
        "option": "maskAllInputs",
        "set_to": "true",
        "block": 1,
        "why": "Restores form-input masking in Session Replay recordings."
      },
      {
        "option": "blockAllMedia",
        "set_to": "true",
        "block": 1,
        "why": "Stops images and video elements from being captured into Session Replay recordings."
      },
      {
        "option": "includeLocalVariables",
        "set_to": "false",
        "block": 2,
        "why": "Stops server stack frames from shipping local variable values, where request bodies and records tend to end up."
      },
      {
        "option": "beforeSend",
        "set_to": "scrubber shown in the artifact",
        "block": 1,
        "why": "Gives the browser SDK a last chance to strip anything sendDefaultPii or a breadcrumb still carries before the event leaves the page."
      },
      {
        "option": "beforeSend",
        "set_to": "scrubber shown in the artifact",
        "block": 2,
        "why": "Gives the Node SDK a last chance to strip request headers and cookies before the event leaves the process."
      },
      {
        "option": "tracePropagationTargets",
        "set_to": "[/^https:\\/\\/api\\.your-app\\.com/]",
        "block": 1,
        "why": "Stops trace headers from attaching to third-party requests the wildcard currently matches."
      },
      {
        "option": "dsn",
        "set_to": "rotate the key, drop the secret half",
        "block": 1,
        "why": "The current DSN is a live credential; the secret half lets anything holding it write to the project."
      }
    ]
  },
  "coverage_check": [
    {
      "flag_id": "dsn-legacy-secret-b1-dsn",
      "status": "confirmed",
      "note": "A key:secret@ credential is present in the pasted DSN; treated as an active exposure, see findings."
    },
    {
      "flag_id": "replay-unmasked-b1-maskalltext",
      "status": "confirmed",
      "note": "maskAllText: false is set in the replay integration and is captured as-is above."
    },
    "... 22 more, one per prescan flag_id ..."
  ],
  "questions": [
    "Does the Sentry organization or project have server-side data-scrubbing rules configured that already redact any of this before storage?",
    "Is there a reverse proxy or API gateway in front of server/instrument.js that already strips Authorization/Cookie headers before the request reaches this process?"
  ],
  "confidence": "high"
}

Worked example: task: "budget"

Required in this lane: title, verdict, headline, summary, checks, rates, artifact, artifact_json, coverage_check, confidence. Verdict is one of within-quota | tight | over-quota, and artifact_json.kind must be sampling_plan.

Reply (trimmed)

{
  "lane": "budget",
  "title": "Sentry budget check \u2014 Python + Go, over quota on errors and spans",
  "verdict": "over-quota",
  "headline": "Spans run 9x over quota and errors 5.2x over \u2014 cut TracesSampleRate/SampleRate from 1.0 before the next bill.",
  "summary": "Both blocks run tracesSampleRate/traces_sample_rate at 1.0 with no tracesSampler to override it, and the Go worker also holds its error SampleRate at 1.0 -- against the quotas you typed, that's spans at 9x over (90,000,000 vs 10,000,000/mo) and errors at 5.2x over (260,000 vs 50,000/mo). Replays sit unused at 0 since neither block ships a browser SDK, and profiles run 7,500,000/mo against a quota that reads as 0, which looks like an unpurchased add-on rather than a genuine allowance. Nothing about the app needed to change for the bill to triple -- these rates were already at 1.0; a smaller included quota on the new Team plan is the more likely cause.",
  "checks": [
    {
      "name": "Spans per transaction",
      "value": "12",
      "verdict": "weak",
      "note": "Typed by the team, not measured from a live trace -- Django ORM N+1 patterns or added Go worker spans could push this well above 12, which would lower every 'rate that fits' figure further."
    },
    {
      "name": "Sessions that saw an error",
      "value": "0",
      "verdict": "good",
      "note": "Both blocks run server-only Python and Go processes with no browser SDK (runs_in_browser: false) -- there are no end-user sessions to replay, so 0 is exact, not an estimate, and the 500-replay quota is simply unused rather than at risk."
    },
    "... 0 more ..."
  ],
  "findings": [
    {
      "id": "traces-full-b1-tracessamplerate",
      "severity": "high",
      "option": "traces_sample_rate",
      "block": 1,
      "quote": "traces_sample_rate=1.0",
      "why": "This is the Python side of a flat 1.0 trace rate that puts spans at 9x the 10,000,000/mo quota (90,000,000 vs 10,000,000).",
      "fix": "traces_sampler=traces_sampler,  # was traces_sample_rate=1.0 -- replace the flat rate with a sampler that zeroes health checks and bases the rest at 0.08"
    },
    "... 3 more ..."
  ],
  "rates": [
    {
      "signal": "errors",
      "current": "1.0",
      "recommended": "0.15",
      "events_per_month": 260000,
      "note": "SampleRate is 1.0 in the Go worker (Python leaves sample_rate unset, same 1.0 default) against a 50,000/mo quota -- 260,000 events is 5.2x over. Dropping to 0.15 projects to about 39,000/mo (78% of quota, some headroom), but for a 3.47% error rate on 7,500,000 requests the more durable fix is filtering the noisy/duplicate error at the source rather than sampling it away blind."
    },
    "... 3 more ..."
  ],
  "artifact": "# SENTRY-SAMPLING.md\n\n## Where the bill is going\n\nBoth init blocks trace at a flat `1.0` with no `tracesSampler` to override it (`sampler_present: false`),\nand the Go worker also holds its error `SampleRate` at `1.0`. Against the quotas you typed:\n\n| Signal   | Current rate | Monthly at that rate | \n... (full Markdown document) ...",
  "artifact_json": {
    "kind": "sampling_plan",
    "recommended": {
      "sampleRate": 0.15,
      "tracesSampleRate": 0.08,
      "replaysSessionSampleRate": 0,
      "profilesSampleRate": 0
    },
    "estimated_monthly": {
      "errors": 39000,
      "spans": 7200000,
      "replays": 0,
      "profiles": 0
    }
  },
  "coverage_check": [
    {
      "flag_id": "traces-full-b1-tracessamplerate",
      "status": "confirmed",
      "note": "Confirmed -- this is the block-1 (Python) tracesSampleRate driving the 90,000,000/mo span volume; addressed in rates and the artifact plan."
    },
    {
      "flag_id": "traces-full-b2-tracessamplerate",
      "status": "confirmed",
      "note": "Confirmed -- the same setting in block 2 (Go); both blocks feed the same quota and need to move together."
    },
    "... 4 more, one per prescan flag_id ..."
  ],
  "questions": [
    "Is Continuous Profiling actually included on the current plan, or is the 0 profiles quota literal?",
    "Does this version of sentry-go expose a TracesSampler function option, the way the Python SDK does, so the worker can get the same health-check exclusion as the Python side instead of a flat rate?"
  ],
  "confidence": "medium"
}

Worked example: task: "signals"

Required in this lane: title, verdict, headline, summary, checks, findings, artifact, artifact_json, coverage_check, confidence. Verdict is one of covered | partial | blind, and artifact_json.kind must be signal_plan.

Reply (trimmed)

{
  "lane": "signals",
  "title": "Next.js Sentry Signals Review: Core Monitoring On, Key Surfaces Missing",
  "verdict": "partial",
  "headline": "Errors and performance both reach Sentry today, but no release means no release health, and logs and user feedback aren't wired up either.",
  "summary": "This is a signals-lane review of two Next.js Sentry init blocks - instrumentation-client.ts (browser) and sentry.server.config.ts (server) - that share one project (4508913). Errors and performance/tracing are both reachable end to end, and Session Replay is active in the browser, but no release is set in either block, so release health, regression marking and automatic source maps are unavailable. Logs and user feedback have no corresponding option or integration anywhere in this paste, and tracePropagationTargets is unset on both sides, so a new external host would silently be included or excluded from the trace by default rather than by a stated list. Cron/check-ins and AI/LLM monitoring can't be judged from these two bootstrap files either way, since application code that would call a model provider or run a scheduled job would live elsewhere.",
  "checks": [
    {
      "name": "Errors",
      "value": "dsn set in both instrumentation-client.ts and sentry.server.config.ts; default error capture is on in both.",
      "verdict": "good",
      "note": "Reachable today. A tunnel would stop the share of browser errors that ad blockers drop before they reach ingest.sentry.io - see the tunnel-missing-b1 finding."
    },
    {
      "name": "Performance/tracing",
      "value": "tracesSampleRate: 1.0 in instrumentation-client.ts, tracesSampleRate: 0.2 in sentry.server.config.ts, browserTracingIntegration() enabled.",
      "verdict": "good",
      "note": "Whether 1.0/0.2 are the right rates is a budget-lane question, not a signals one - the signal itself is reachable on both sides of the request."
    },
    "... 8 more ..."
  ],
  "findings": [
    {
      "id": "release-missing-b1",
      "severity": "medium",
      "option": "release",
      "block": 1,
      "quote": "",
      "why": "instrumentation-client.ts has no release option. Without it, this block's errors can't be tied to a deploy, source maps won't apply, and release health has nothing to measure.",
      "fix": "Sentry.init({\n  dsn: \"https://[DSN-KEY-1]@o4508912.ingest.us.sentry.io/4508913\",\n  release: env.NEXT_PUBLIC_SENTRY_RELEASE, // set by the Sentry Next.js build plugin, or your own CI commit SHA\n  // ...\n});"
    },
    "... 5 more ..."
  ],
  "rates": [],
  "artifact": "## Coverage today\n\nTwo Next.js Sentry init blocks share one project (`4508913`): `instrumentation-client.ts` runs in\nthe browser, `sentry.server.config.ts` runs on the server. Between them:\n\n| Signal | What you have today | Verdict |\n| --- | --- | --- |\n| Errors | dsn set in both blocks; default err\n... (full Markdown document) ...",
  "artifact_json": {
    "kind": "signal_plan",
    "coverage": {
      "errors": true,
      "performance": true,
      "release_health": false,
      "replay": true,
      "logs": false,
      "crons": "unknown",
      "ai": "unknown"
    },
    "alerts": [
      {
        "name": "Production error volume spike",
        "condition": "Number of events > 100 in 1 hour, filtered to level:error, project-wide",
        "environment": "production",
        "notify": "Whatever Slack/email/PagerDuty integration is connected under Settings -> Integrations for this project (connect one first if none exists)."
      },
      {
        "name": "New issue first seen in production",
        "condition": "A new issue is created",
        "environment": "production",
        "notify": "On-call/triage channel for this project."
      },
      {
        "name": "Performance regression on a key transaction",
        "condition": "p95(transaction.duration) increases by more than 20% week over week",
        "environment": "production",
        "notify": "Backend on-call channel."
      },
      {
        "name": "Crash-free session rate drop",
        "condition": "Crash-free sessions % drops below 99% over 1 hour (requires release to be set - see the release-missing findings)",
        "environment": "production",
        "notify": "Release owner / on-call channel."
      },
      {
        "name": "Error-triggered replay volume spike",
        "condition": "Count of replays with an error exceeds a threshold in 1 hour (replaysOnErrorSampleRate is 1.0, so this tracks 1:1 with client error volume)",
        "environment": "production",
        "notify": "Frontend on-call channel."
      }
    ]
  },
  "coverage_check": [
    {
      "flag_id": "traces-full-b1-tracessamplerate",
      "status": "not-applicable",
      "note": "A sampling-rate/cost question for the budget lane; the performance signal is reachable regardless of what the rate is."
    },
    {
      "flag_id": "release-missing-b1-release",
      "status": "confirmed",
      "note": "This is exactly why Release health is marked missing below."
    },
    "... 10 more, one per prescan flag_id ..."
  ],
  "questions": [
    "Does this app call an LLM or AI provider (e.g. via the Vercel AI SDK)? Neither init file would show that even if true, and it decides whether the AI/LLM monitoring row is worth acting on.",
    "Does this app run any scheduled jobs (cron routes, queue workers)? Same visibility limit as above."
  ],
  "confidence": "medium"
}

Chaining the lanes

The page's handoff buttons are just a second call: take the result of lane A, build a short digest of it, and send that as upstream on lane B with the same config. The order the app follows is auditprivacybudgetsignals, and the digest keeps the later lane from repeating what the earlier one established.

const audit   = await runLane({ ...base, task: "audit" });
const privacy = await runLane({ ...base, task: "privacy", upstream: digest(audit) });
const budget  = await runLane({ ...base, task: "budget",  upstream: digest(privacy) });
const signals = await runLane({ ...base, task: "signals", upstream: digest(budget) });

Rate limits and cost

Reading the app's own contract

/llms.txt states what this app takes, what the free browser-side prescan computes, the four lanes and the output envelope, in a form an answer engine or an agent can read without executing anything.