> ## Documentation Index
> Fetch the complete documentation index at: https://subtext.fullstory.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sentry and Subtext: From an Error to the Session

> Attach the Subtext session URL to Sentry as a tag, then jump from any error or performance issue straight into the user's real session.

Sentry tells you *that* an exception was thrown: an issue spikes, a regression appears, a transaction slows. It can't tell you *whether* the person was actually blocked or what they saw when it fired. Subtext can. Attach the Subtext session URL to your Sentry events as a tag and every issue becomes a jumping-off point — your agent reads the `subtext_url`, opens that person's real session, and walks the decisive moments with screenshots, the component tree, network, and console. Sentry detects; Subtext diagnoses.

<Note>
  This guide assumes the [Subtext capture snippet](/docs/install/overview) is installed and the Sentry JavaScript SDK (`@sentry/browser` or a framework-specific package) is initialized.
</Note>

## Attach the session URL

Store the current Subtext session URL on your Sentry events as a tag so it travels with every error. Tags are indexable and searchable in the Sentry UI, and there are two ways to set one.

### As a global tag

Set the URL as a Sentry tag once, after identification. It rides on every subsequent error event and shows up wherever you filter issues. Session URL values fit within Sentry's 200-character tag limit.

<CodeGroup>
  ```js analytics.js theme={null}
  const subtextUrl = FS('getSession', { format: 'url.now' })
  if (subtextUrl) {
    Sentry.setTag('subtext_url', subtextUrl)
  }
  ```

  ```tsx React theme={null}
  useEffect(() => {
    if (!user) return
    FS('setIdentity', { uid: user.id, properties: { email: user.email } })
    Sentry.setUser({ id: user.id, email: user.email })
    const subtextUrl = FS('getSession', { format: 'url.now' })
    if (subtextUrl) {
      Sentry.setTag('subtext_url', subtextUrl)
    }
  }, [user])
  ```
</CodeGroup>

### Per error, with beforeSend

`beforeSend` runs on every captured event and reads the URL fresh, so each error gets a link to the exact moment it occurred rather than a frozen session-start link. `beforeSendTransaction` does the same for performance events.

```js theme={null}
Sentry.init({
  dsn: '<YOUR_DSN>',
  beforeSend(event) {
    const url = FS('getSession', { format: 'url.now' })
    if (url) event.tags = { ...event.tags, subtext_url: url }
    return event
  },
  beforeSendTransaction(event) {
    const url = FS('getSession', { format: 'url.now' })
    if (url) event.tags = { ...event.tags, subtext_url: url }
    return event
  },
})
```

Reach for `beforeSend` when moment-level precision on errors matters more than simplicity. The two patterns can coexist — `beforeSend` overwrites the global tag value for each specific event.

<Warning>
  Don't read `FS('getSession')` at module top level — it returns `null` until the session has started. Don't put the URL in `event.extra`; use `event.tags` so it stays searchable in the Sentry UI. The session URL changes on a new session and when `FS('setIdentity')` is called with a different `uid`, so re-call `Sentry.setTag` whenever you re-identify. `beforeSend` reads the URL fresh on every event, so it never needs re-attaching.
</Warning>

## From signal to session

However you first hear about a problem in Sentry — a paged alert, a "New Issue" email, an assigned ticket — the path is the same: get to the `subtext_url` on the event, then hand it to your agent. Sentry tells you an exception was *thrown*; the session tells you whether the user was actually *blocked*, what they saw, and the exact sequence that triggered it. That gap is the Detect-vs-Diagnose read, and the output is a structured summary you can paste back into the Sentry issue.

<AccordionGroup>
  <Accordion title="A new-issue or regression email">
    A Sentry "New Issue" or "Regression" email lands in your inbox: *`TypeError: cannot read properties of undefined (reading 'total')` — CheckoutForm.tsx*.

    Click through to the issue from the email, open the latest event, go to the **Tags** section, and copy the `subtext_url` value. Then hand it to your agent:

    ```text theme={null}
    Sentry just emailed me a regression: TypeError in CheckoutForm. Here's the session from the
    moment it fired: <paste subtext_url>

    Open it and give me a Detect-vs-Diagnose read. Sentry says the exception was thrown — did it
    actually break the user, or did the page recover? Walk the 30s before the error, tell me what
    the user was doing, what the UI looked like when it fired, and whether this reproduces from that
    flow. End with a one-paragraph summary I can drop into the Sentry issue.
    ```
  </Accordion>

  <Accordion title="A Slack alert on a post-deploy spike">
    The Sentry Slack integration posts "Issue spiking — 142 events in 10 min, first seen in release `web@2.8.0`."

    Click the Slack alert; it deep-links to the issue. Because a spike is many sessions, grab `subtext_url` from three or four different recent events.

    ```text theme={null}
    A Sentry issue is spiking right after we shipped web@2.8.0. Here are subtext_urls from several
    affected sessions:
    <paste 3–4 subtext_urls>

    Review each one and find the common thread. Is it the same flow, the same browser, the same
    input? Which sessions show real user impact vs. a swallowed error? Give me a single root-cause
    hypothesis and tell me whether this is a rollback or a hotfix.
    ```
  </Accordion>

  <Accordion title="On-call triage of the issue inbox">
    Your Sentry inbox has a stack of issues assigned to you for the shift.

    In the Issues view, filter `has:subtext_url` to surface only the issues that have a session attached — those are the ones you can actually *see*. Triage those first and copy the tag from each. Run the per-issue prompt above, and for anything where the session shows no real impact, downgrade it:

    ```text theme={null}
    Here's a Sentry issue assigned to me with a session attached: <paste subtext_url>

    Give me a fast Detect-vs-Diagnose read: did this actually block the user, or is it a swallowed /
    already-recovered error? If the session shows no real impact, say so plainly so I can downgrade it.
    ```
  </Accordion>
</AccordionGroup>

<Tip>
  After the review, paste the agent's structured summary back into the Sentry issue as a comment so the next person inherits the diagnosis, not just the stack trace. For a spike across several sessions, ask your agent to roll the findings into one proof document (`doc-create`) with a screenshot per session and a single shared link, then drop that link into the issue.
</Tip>

## For agents

An autonomous agent can run the whole loop against Sentry directly: discover the worst issues, extract the `subtext_url`, and hand each session to the Subtext MCP. Sentry ships a first-party MCP server at `https://mcp.sentry.dev/mcp` (Streamable HTTP, with SSE fallback); for self-hosted Sentry, run it locally over stdio with `npx @sentry/mcp-server@latest`. The hosted endpoint authenticates with OAuth through your Sentry org; local uses a User Auth Token with the scopes Sentry's MCP docs require: `org:read`, `project:read`, `project:write`, `team:read`, `team:write`, and `event:write`.

<Steps>
  <Step title="Discover the worst issues">
    Use `search_issues` to rank hotspots — pass a query and a sort, e.g. `search_issues(query="is:unresolved", sort="freq")`. Drill into an issue with `get_issue_details`, `search_issue_events`, `search_events`, or `get_event_stacktrace`. Over REST, the org-level Issues endpoint does the same (the project-level one is deprecated):

    ```
    GET /api/0/organizations/{org}/issues/?query=is:unresolved&sort=freq&statsPeriod=24h&limit=100
    # sort=freq → by event volume; sort=user → by # users affected
    # Authorization: Bearer <auth_token>
    ```

    Append `has:subtext_url` to the query to keep only issues with a session attached; its inverse `!has:subtext_url` surfaces capture gaps.
  </Step>

  <Step title="Extract the session URL">
    `subtext_url` is stored as an event tag. Read the aggregate of values seen on an issue with `get_issue_tag_values(tagKey="subtext_url")`. Over REST:

    ```
    # Per event (moment-precise)
    GET /api/0/projects/{org}/{project}/events/{event_id}/
    # response.tags is an array of {key, value}; find key === "subtext_url" → .value

    # Aggregate (all values seen on an issue)
    GET /api/0/organizations/{org}/issues/{issue_id}/tags/subtext_url/
    # response.topValues[N].value
    ```
  </Step>

  <Step title="Hand off to Subtext">
    For each URL, call `review-open(session_url=<subtext_url>)` on the Subtext MCP, then read the event map and step to the error moment for a Detect-vs-Diagnose read. When triaging a spike, pull `subtext_url` from several affected events and review them together to find the common trigger.
  </Step>
</Steps>

<Note>
  `subtext_url` only round-trips if it was attached at capture time (see [Attach the session URL](#attach-the-session-url)). `beforeSend` produces moment-precise links; the global tag opens at session start. Sentry caps tag values at 200 characters — session URLs fit, but verify if you ever see truncation.
</Note>

## Related

* [Session Review overview](/docs/session-review/overview) — what your agent does once a session is open.
* [Install the capture snippet](/docs/install/overview) — required before any session URL exists to attach.
