Guide Evaluation playbooks ~9 min read Updated June 16, 2026

Exporting your Sentry issue history before the DSN swap: what to pull, what to drop

Sentry's default retention is 90 days. Once you cancel billing, the events go with it. Pull what you need through four REST endpoints before you flip the DSN, archive the dump as searchable JSON, and let your self-hosted backend start clean.

TL;DR

20 seconds. Sentry deletes events after 90 days on Team and Business plans. When you cancel the contract, the deletion window closes. Pull issues, comments, owners, and integration config through the REST API before you cutover — there is no built-in export button.

60 seconds. Four endpoints carry the data worth saving: /projects/{org}/{project}/issues/ for the issue list and metadata, /issues/{id}/events/ for per-event payloads, /issues/{id}/comments/ for triage history, and /projects/{org}/{project}/rules/ for alert rules you will need to recreate. The API is paginated 100-at-a-time and rate-limited at roughly 40 requests per second per org. A real export for a project with 50,000 open events takes about twenty minutes and produces a few hundred megabytes of JSON. Source maps, debug files, replay videos, and profiling traces do not come along — pull those separately or accept the loss. On the urgentry side, the dump becomes an archive bucket your team can grep, not a live import.

This guide covers the retention cliff, the four endpoints, the pagination and rate-limit mechanics, what you cannot export, and how to use the dump after cutover.

The 90-day cliff

Sentry's documented default event retention is 90 days for the Team and Business plans. After 90 days, event payloads — stack traces, breadcrumbs, request bodies, tags, contexts — are deleted from the storage tier. The issue records themselves persist as group containers so that grouping continuity holds across new occurrences, but the per-event detail you actually read during triage is gone.

Enterprise contracts can extend retention to 180 or 365 days. Self-hosted Sentry runs on whatever SENTRY_RETENTION_DAYS your config.py declares. Most teams running Sentry SaaS sit at the 90-day default and do not realize this until they try to look at a regression from six months ago and find the events column empty.

The cliff matters specifically during a Sentry-to-self-hosted evaluation because two clocks are running at once. Your billing contract is winding down. The 90-day retention window is still moving forward. The moment billing terminates, you lose API access entirely — not just to new events but to the historical events that have not yet aged out. Whatever you have not exported by that timestamp is unrecoverable through normal channels.

A practical heuristic: assume any Sentry data older than your last paid day is gone. Plan the export to finish at least one full business day before the contract ends.

What's worth pulling

The export is not "everything Sentry knows." It is the subset of data your team will actually open after cutover. Four categories cover almost every legitimate use:

  • Open and recently-resolved issues with their metadata: title, culprit, first_seen, last_seen, level, status, assignee, count, userCount, tags. This is the searchable archive of "did we ever see this before?" Without it, every new occurrence looks brand new on the urgentry side for the first 90 days.
  • A sample of events per issue for forensic depth. Not every event — that path leads to terabytes — but enough to reconstruct what the failure looked like. Three events per issue (oldest, median, newest) is the working compromise most teams settle on.
  • Comments and discussion history on the issues that triggered actual investigation. The "we suspected the Stripe webhook retry path" thread is the institutional memory that takes a year to rebuild. It is also tiny — a comment is a few hundred bytes — so just pull all of them.
  • Alert rules, ownership rules, and integration config. These are not historical data in the same sense as events, but they exist only in Sentry. If you flip the DSN before you replicate them, the cutover is silent for two weeks until an outage reveals that no alerts are firing.

Anything outside these four buckets is usually not worth the export budget. Releases, deploys, environments, and teams are recreated naturally on the urgentry side as your services start sending again.

The four endpoints

Sentry exposes a REST API documented at docs.sentry.io/api/. The four endpoints that carry the export payload:

1. Issues list

GET /api/0/projects/{org}/{project}/issues/
    ?query=is:unresolved
    &limit=100
    &cursor={cursor}

Returns paginated issue summaries. The Link response header carries the next cursor in the standard rel="next" format. Walk it until you hit results="false".

The query parameter accepts the same syntax as Sentry's issue search bar. is:unresolved is the most common starting point. To also capture recently-resolved-but-relevant issues, run a second sweep with is:resolved age:-30d and merge.

2. Events per issue

GET /api/0/issues/{issue_id}/events/
    ?limit=100
    &cursor={cursor}

Each issue's event list is paginated separately. For the "sample three events" pattern, grab the first page sorted by full (the default), take the newest event, then request the page that starts at the oldest event ID, and pick a middle event by index.

A single event payload is typically 20-200 KB. Multiplied across 50,000 issues with three events each, you are looking at roughly 15-30 GB of JSON. Compress on write — gzip cuts it to a third.

3. Comments per issue

GET /api/0/issues/{issue_id}/comments/

Returns the comment thread on an issue. No pagination on most issues since comment counts are small. Walk this for every issue that has numComments > 0 in the issues-list response — skip the silent ones to save thousands of empty calls.

4. Alert rules and ownership

GET /api/0/projects/{org}/{project}/rules/
GET /api/0/projects/{org}/{project}/ownership/
GET /api/0/organizations/{org}/integrations/

Three endpoints, three categories of configuration. The rules/ endpoint returns issue alert rules including conditions, actions, and filters. The ownership/ endpoint returns the code-owner mapping that drives auto-assignment. The integrations/ endpoint lists installed integrations (Slack, PagerDuty, Linear, GitHub) so you know what you need to reauthorize on the urgentry side.

Pagination, rate limits, and the time budget

Sentry's REST API documents a global rate limit of 40 requests per second per organization, with the X-Sentry-Rate-Limit-Remaining header carrying the current burst budget. In practice the ceiling on long-running exports sits lower than that — the API has a separate concurrent-request limit that bites before the burst budget runs out. Plan for an effective 15-25 requests per second.

A worked time budget for one project with 50,000 open issues and three events each:

  • Issues list: 50,000 / 100 = 500 requests. At 20 req/s that is 25 seconds.
  • Events per issue: 50,000 × 2 requests (first page plus middle-event lookup) = 100,000 requests. At 20 req/s that is 83 minutes.
  • Comments per issue: assume 20% of issues have comments, so 10,000 requests. At 20 req/s that is 8 minutes.
  • Config endpoints: 3 requests, negligible.

Total wall clock is on the order of 90-120 minutes per project for a non-trivial backlog. Two projects in parallel hit the per-org limit, so run them serially or accept the throttling. The 429 responses come with a Retry-After header — respect it; backing off harder than necessary is faster than letting the API drop you for an hour.

The minimal export script

A pragmatic exporter written in any language you already have a Sentry SDK or HTTP client in. The shape, in pseudocode:

import requests, json, gzip, time

AUTH = {"Authorization": f"Bearer {SENTRY_AUTH_TOKEN}"}

def paginate(url):
    while url:
        r = requests.get(url, headers=AUTH)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        yield from r.json()
        url = next_cursor(r.headers.get("Link", ""))

with gzip.open("issues.json.gz", "wt") as f:
    for issue in paginate(f"{BASE}/projects/{ORG}/{PROJECT}/issues/?query=is:unresolved&limit=100"):
        events = list(islice(paginate(f"{BASE}/issues/{issue['id']}/events/?limit=100"), 3))
        comments = list(paginate(f"{BASE}/issues/{issue['id']}/comments/")) if issue["numComments"] else []
        f.write(json.dumps({"issue": issue, "events": events, "comments": comments}) + "\n")

The output is newline-delimited JSON, one record per issue, gzipped. NDJSON is the right shape because it streams cleanly into jq, splits on line boundaries for distribution, and never holds the whole file in memory at once.

The auth token comes from Settings → Auth Tokens in the Sentry UI. Use a scoped token with event:read and project:read — the export does not need write access to anything.

What you cannot export

Five categories of data either are not exposed through the REST API or have already been deleted by the time you reach them:

  • Source maps and debug files. The API returns metadata about uploaded artifacts but not the binaries. If you need the source maps for old releases for forensic symbolication, download them through sentry-cli releases files {VERSION} list before cutover, while the auth token still works.
  • Session replay videos. Replay payloads are stored as a separate blob format and are not part of the events API. Replays older than 30 days (the default replay retention) are also already gone. There is no practical way to bulk-export replays; accept the loss or pull a handful manually for the highest-traffic issues.
  • Profiling samples. Same shape as replays — separate storage, separate retention, no bulk export. The aggregated profile views in the UI are derived from samples that age out.
  • Internal grouping inputs. Sentry returns the resulting hash and fingerprint but not the algorithm steps that produced them. If a future grouping config on the urgentry side groups events differently, you cannot replay the old grouping decisions exactly.
  • Events past the retention horizon. The events endpoint cannot return events that have already been deleted. Issues will show count and userCount totals from before the cliff, but the underlying event payloads are not retrievable.

Using the dump after cutover

There is no official Sentry-to-urgentry importer, and there is unlikely to ever be one. Sentry's storage schema is an internal contract that changes between releases; treating it as a public import format would couple two products that should stay loosely joined.

The pattern that holds up in practice is the archive bucket:

  1. Write the NDJSON dump to an object store (S3, R2, MinIO) or a local volume mounted into your urgentry host.
  2. Keep it grep-able. zcat issues.json.gz | jq 'select(.issue.title | contains("StripeWebhookError"))' answers "have we seen this before?" in under a second for a couple-GB dump.
  3. Let urgentry start clean. New occurrences after cutover create new issues with new IDs; the historical dump is the forensic layer underneath.
  4. Cross-reference by fingerprint or stack-trace prefix when you need to join history to a live issue. Both systems expose the fingerprint hash; that is the natural join key.

A small internal tool — a Slack slash command or a CLI that takes an urgentry issue URL and runs the jq query against the archive — closes the loop. It is fifty lines of glue code and saves the institutional memory cost that drives most "Sentry is the only place this lives" arguments.

Sequencing the export with the rest of the cutover

The export is one step in a week-long cutover sequence, not a standalone task. The sequence that holds up:

  • Day 1: scope what you need. Run the issues-list endpoint with limit=1 to confirm auth, count active projects, estimate dump size.
  • Day 2: stand up urgentry on its production host. Run the switch proof with one test DSN to confirm ingest works.
  • Day 3: run the full export. Validate the NDJSON round-trips through jq without errors. Upload to the archive bucket.
  • Day 4: rebuild alert rules and ownership on the urgentry side from the config endpoints. Test each alert with a synthetic event.
  • Day 5: cutover. Flip the DSN environment variable on each service in deploy order (lowest-traffic first), watch the urgentry ingest stream, confirm events arrive.
  • Day 6-7: parallel-run window. Keep the Sentry DSN active on one canary service to compare event counts, per the side-by-side evaluation playbook.
  • Day 8+: cancel Sentry billing. The archive bucket remains accessible; the live API does not.

The most common compression of this sequence is "we'll just turn off Sentry today and start using urgentry tomorrow." It is also the most common reason a team comes back two weeks later asking whether there is a way to retrieve issues they deleted. There is not.

Frequently asked questions

What is Sentry's default event retention?

Sentry's default retention on the Team and Business plans is 90 days for events. Issue records persist longer for grouping continuity, but the per-event payload (stack traces, breadcrumbs, tags, request bodies) is deleted at 90 days. Enterprise contracts can extend retention to 180 or 365 days. Self-hosted Sentry retention is whatever your operator configured.

Can I download my Sentry events as a single file?

There is no one-click export. The events endpoint is paginated at 100 items per page with strict rate limits. A full export means walking the API for each project, persisting the JSON to disk, and stitching pages together. For one project with 50K open events that is roughly 500 requests, twenty minutes at the documented rate limit, and a few hundred megabytes on disk.

What can't be exported from Sentry's API?

Source maps, debug files, profiling data, replay videos, and any attachment older than the retention window. The API returns metadata for these but not the binary payloads. Internal grouping decisions (Sentry's hash inputs) are also opaque — you get the resulting group hash but not the algorithm output. If you depend on these, pull them while billing is still active.

Does urgentry import a Sentry JSON dump?

There is no official Sentry-to-urgentry importer because Sentry's storage schema is not a stable public contract. The practical pattern is an archive bucket: write the JSON dump to S3 or a local volume, keep it grep-able for forensic lookups, and let urgentry start fresh as your live store. The two systems are joined by the issue URL or fingerprint hash when you need to cross-reference.

How much time should I budget for the export before flipping the DSN?

Plan for one calendar week of overlap. Day one: scope what you actually need (issues, comments, owners, integrations config). Day two and three: pull the data, validate it round-trips through jq. Day four: rebuild alert rules on the urgentry side. Day five: cutover. Compressing this into a single afternoon is the most common reason teams discover three weeks later that an alert never fired.

Sources

  1. Sentry REST API reference — canonical documentation for the issues, events, comments, rules, and integrations endpoints used in the export.
  2. Sentry event retention and quotas guide — the documented 90-day default and the extended retention available on Business and Enterprise tiers.
  3. Sentry API rate limits — the 40-req/s ceiling, the X-Sentry-Rate-Limit-Remaining header, and the Retry-After backoff contract.
  4. sentry-cli releases reference — the only practical path to pull uploaded source maps and debug files in bulk.
  5. Sentry auth tokens documentation — scoping a token with event:read and project:read for export-only access.
  6. urgentry side-by-side evaluation playbook — the seven-day parallel-run window that flanks the cutover step.

One DSN swap. Full Sentry SDK compatibility.

urgentry accepts the Sentry SDK envelope format on a $5 VPS. 218 API operations covered. SQLite by default, Postgres optional. Change one environment variable and events start arriving.