Releases, deploys and release health
A release is a version string your code reports (release on every event, and on every
session). Registering it, and recording where it was deployed, is what turns that string
into "what shipped, who wrote it, and where is it live".
# Register the release with the commit it was built from.
curl -X POST "$API/orgs/$ORG/projects/$PROJECT/releases" \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"version":"4.2.0","commitSha":"<40-hex sha>"}'
# Record a deploy of it.
curl -X POST "$API/orgs/$ORG/projects/$PROJECT/releases/4.2.0/deploys" \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"environment":"production"}'
Or, from CI, with the bucker CLI (cli.md), which does the same with retries,
source maps and a verify gate:
bucker releases new "$VERSION" --commit "$GIT_SHA"
bucker sourcemaps upload dist --release "$VERSION" # fails the job if a shipped id cannot resolve
bucker releases finalize "$VERSION"
bucker deploys new "$VERSION" --env production
CI recipes
All three recipes run the four commands above, in that order. Each takes the token from the
CI system's secret store. Use an org CI token (bkr_org_…, Settings → Tokens, scopes
project:read and release:write): it is not bound to a person, so the pipeline keeps working
after they leave. The files are in code/tooling/release-action/, and
tooling/bucker-cli/test/ci-recipes.test.ts runs each command in them through the real CLI.
GitHub Actions: the composite action bucker/release-action:
- run: npm run build
- uses: bucker/release-action@v0
with:
token: ${{ secrets.BUCKER_TOKEN }}
org: acme
project: web
sourcemaps: dist # omit to skip source maps
environment: production # '' records no deploy
GitLab CI/CD: a CI/CD component. BUCKER_TOKEN is a masked, protected CI/CD variable:
include:
- component: $CI_SERVER_FQDN/<group>/bucker-release/release@0.1.0
inputs:
org: acme
project: web
sourcemaps: dist
AWS CodeBuild: merge this into your buildspec.yml. The token comes from Secrets Manager:
env:
variables: { BUCKER_ORG: acme, BUCKER_PROJECT: web, CLI: '@bucker/cli@0.1.0' }
secrets-manager:
BUCKER_TOKEN: bucker/ci:token
phases:
post_build:
commands:
- export BUCKER_RELEASE="${BUCKER_RELEASE:-$CODEBUILD_RESOLVED_SOURCE_VERSION}"
- npx --yes "$CLI" releases new "$BUCKER_RELEASE" --commit "$CODEBUILD_RESOLVED_SOURCE_VERSION"
- npx --yes "$CLI" sourcemaps upload dist --release "$BUCKER_RELEASE"
- npx --yes "$CLI" releases finalize "$BUCKER_RELEASE"
- npx --yes "$CLI" deploys new "$BUCKER_RELEASE" --env production
What shipped in a release
When a release is registered with a commitSha, Bucker walks the commits between the
previous release's commit and this one's and stores them. The release page lists them,
newest first, with author, time and the files each touched.
GET …/releases/{version}/commits returns the same list.
The walk uses your connected repository through the SCM provider. It needs a repository connected for the organization and a code mapping for the project (the same mapping suspect commits use). Where the provider cannot compare two commits, Bucker falls back to the commits your repository webhook already delivered between the two releases and says so on the page.
A release never shows a bare empty list. The page states why it is empty:
| Status | Meaning and what to do |
|---|---|
no_commit_sha |
The release was registered without commitSha. Register it again with one. |
no_repository |
No repository is mapped to the project. Connect one and add a code mapping. |
consent_revoked |
Access to the mapped repository was revoked. |
app_not_configured |
The repository is connected through a GitHub App installation and this deployment has no GitHub App configured (GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY). Connect the repository with a token instead. |
provider_unavailable |
The provider could not be reached. Use Refresh (POST …/releases/{version}/sync-commits). |
commit_not_found |
The release's commit is in none of the mapped repositories. |
no_baseline |
No earlier release carries a commit, so only this release's own commit is known. |
Deploy diff
Each deploy row has a Changes button: the commits this deploy carried into its
environment, that is, the release's range minus what the environment already ran
(GET …/deploys/{deployId}/diff).
Suspect commits
Once a release's commits are recorded, the suspect commits of an issue are drawn from the range of the release the issue was first seen in. A commit outside that release cannot be the cause. An issue whose first release has no recorded range keeps the 14-day window.
Resolving by release
An issue resolved without a release reopens on its very next event, including the tail of the deploy that still has the bug. Pinned to a release, only an event from a later release reopens it. There are three ways to pin without knowing the version yet.
Resolve in the next release
Resolve in release… has a Resolve in next release button. The fix is merged but not
released: the issue stays resolved, nothing reopens it, and the first release registered after
that moment becomes its fix. The activity feed records the moment it binds ("The next release
arrived: now resolved in 4.2.0"). Over the API this is {"action":"resolve","resolvedInRelease":"@next-release"}.
A release is "registered" when POST …/releases creates it, or when the first event naming a
new version arrives.
Resolve from a commit message
A push to the repository's default branch whose commit message says
fixes BKR-123, closes BKR-123, BKR-124 or resolves BKR-123 resolves those issues in the
next release, with an activity row naming the commit. Only the project that repository is mapped
to is searched, so another project's BKR-123 is never touched. Issues already resolved
keep their release, and archived issues stay archived. A bare mention (see BKR-123) resolves nothing.
The commit message is what is read, which includes the title Git hosts write for a squash or
merge commit. The body of a pull request is not.
Auto-resolve on deploy (opt-in)
Settings › Projects › Release settings › Auto-resolve issues quiet for (days). With a value
set, recording a new deploy (POST …/releases/{version}/deploys) resolves the project's open
issues that have not been seen for that many days, pinned to the deployed release; the activity
row says which deploy and how long they had been quiet. Empty means off, which is the default.
Archived issues are never touched. GET/PUT …/release-settings is the same setting over the API.
Release-health alerts and a stability target
Alert on crash-free rate and adoption
A metric alert rule can watch three release-health rates, through the same router, channels, debounce and history as every other alert:
| Aggregate | Value |
|---|---|
crash_free_sessions |
1 - crashed / sessions for the release being watched |
crash_free_users |
1 - crashed users / users (needs identified users; server-SDK aggregates carry none) |
release_adoption |
the release's share of the project's sessions in the window |
All three are rates between 0 and 1, so the threshold is 0.995, not 99.5. Use the
lt comparator to alert when one drops:
{ "id": "metric", "aggregate": "crash_free_sessions", "comparator": "lt",
"threshold": 0.995, "windowMinutes": 180, "minSessions": 200 }
- Which release. The latest one in the environment: the release whose first session
bucket is most recent, so the rule follows your project through its releases without
being edited. A
releasefilter (equals) pins it to one version; anenvironmentfilter narrows the sessions read. Alevelfilter is refused, since sessions have no level. - No reading is not a breach. With fewer than
minSessionssessions (users, forcrash_free_users) in the window, default 100, the rule reports "no reading" and neither fires nor records a recovery. Two crashes in the first five sessions of a rollout is not an incident. - Freshness. Sessions and adoption read the same hourly rollup the release pages read, so a reading is at most one rollup pass old, and the alert and the page cannot disagree. Users are counted from the session table.
- The anomaly (learned-band) condition is not offered for these rates.
In the rule editor, choose a metric rule and the aggregate under Aggregate.
Stability target
Settings › Projects › Release settings › Crash-free sessions target (%) is an opt-in target for
the project, for example 99.5. It is shown on every release page as met or missed against that
release's own crash-free session rate, and the staged-rollout guardian uses it: a rollout
whose cohort has at least the sample floor of sessions and misses the target beyond chance (a
one-sided test at the guardian's p-value ceiling) halts with the verdict BELOW_TARGET, with or
without a baseline release to compare against. Leave it empty and the guardian judges by the
baseline alone, as before. Over the API: GET/PUT …/release-settings (crashFreeTarget, a rate).
Comparing releases, and the suspect deploy
Compare with another release
The release page has a Compare with another release panel. Pick any other release (the
predecessor is offered first) and the page puts the two side by side on crash-free sessions and
users, adoption, issues the release introduced, issues it brought back (regressions whose event
carried this release) and issues pinned as resolved in it, and the p95 duration of its root spans
(the last 30 days). The change column is computed on the server:
GET …/release-compare?base=&head=&environment=.
These are each release's own totals over its whole life. That is the right question for "which release was the worse one", and a different one from the comparison above it on the page, which asks whether a release is worse than its predecessor was at the same point in its rollout (the question the staged-rollout guardian halts on). A figure with nothing behind it, such as no sessions or no spans, is shown as a dash, never as zero.
Suspect deploy on an issue
The issue's rail has a Suspect deploy row: the last deploy to the environment the issue began
in, at or before the moment it was first seen, the release that deploy carried, and the commits
that shipped with it (see above). It says how sure it is: whether the issue's first event carried
that release, and how long the deploy preceded it, so a deploy from last week reads as the
coincidence it probably is. It is a pointer to where to look, not a verdict. The row is absent for
a project that records no deploys. GET …/issues/{id}/suspect-deploy returns the same.
Release health by OS, browser, device and environment
The release page has a Health by segment panel: the release's sessions grouped by
operating system, browser, device class (desktop, mobile, tablet, bot, other) or
environment, each row with its sessions, share, crash-free sessions and crash-free users, every
rate beside its denominator. The same grouping is GET …/release-health/{version}?groupBy=os|browser|device|environment
(a segments object on the usual response), and the crash-free rate dashboard widget takes a
Break down by setting that adds the same rows.
- Where the dimensions come from. A session item's
attrs.user_agent. The browser SDK sendsnavigator.userAgent; the server reads the OS, browser family and device class from it when the session arrives and does not keep the string. Versions are not kept either: "Chrome 124.0.6367.91" is thousands of groups of one, and a group of one is not a crash-free rate. Sentry's browser SDKs sendattrs.user_agenttoo, so they segment with no change. - Unknown is a row. A session that sent no User-Agent (an older SDK, a runtime that is not a browser) and every session recorded before this existed is Unknown, so the rows always add up to the release's sessions and you can see how much of it was measurable. There is no backfill.
- Aggregate sessions have no device. A Sentry server SDK in request mode sends counts per
minute (
sessionsitems, stored inrelease_session_aggregates), not sessions, so there is no User-Agent or device behind them. They are in the release's totals and in the environment grouping (which they do carry), and in no OS, browser or device row. The panel and the API (excludedAggregatedSessions) say how many, rather than guessing a split. - iPad. iPadOS 13+ identifies itself as a Mac and is counted as one: nothing in the User-Agent tells them apart.