Blog Finding the cause

Release health: crash-free sessions per release

You shipped a new release and errors are coming in. A raw error count cannot tell you whether the new release is worse than the old one, because the release with more users will always have more errors. You need a rate: of the sessions that ran this release, how many crashed.

That is what release health shows. Your SDK reports one session per page load or per process, tagged with the release. ZipLogger counts how each session ended and shows the rates per release under Issues → Releases. This post covers what the numbers mean, how to turn it on in the browser SDK, the .NET client and Sentry SDKs, the alert, and what it does not do.

The Releases page

Open Issues and click Releases. Pick a window under Sessions started in: the last 24 hours, the last 7 days or the last 30 days. The table has one row per release:

ColumnWhat it shows
ReleaseThe release string exactly as your SDK sent it
Crash-free sessionsShare of sessions that did not crash, with the crashed count next to it
Crash-free usersShare of users with no crashed session, or "no user ids" when the SDK sent none
SessionsSessions that started in the window
AdoptionThis release's share of all sessions in the window
ErroredSessions that had errors but did not crash, with any abnormal ones next to it
Last sessionWhen the newest session of this release started

Above the table, a chart shows crash-free sessions over time for the five busiest releases in the window. It uses one point per hour for 24 hours, per 6 hours for 7 days and per day for 30 days. An hour with no sessions is a gap in the line, not 100%.

Windows select sessions by the time they started. Like everything else in ZipLogger, the page shows the environment you have selected.

What counts as a crash

Each session ends up with exactly one outcome, taken from what the SDK reported last:

OutcomeWhen
crashedThe SDK reported status: crashed: an unhandled error ended the session
abnormalThe SDK reported status: abnormal, for example the app was killed or hung
erroredThe SDK reported status: errored, or errors above 0 with any other ending
healthyEverything else

Crash-free sessions is 1 minus crashed sessions divided by all sessions. Abnormal sessions do not lower it; they are counted next to the errored ones.

Crash-free users works the same way per user. A user is the SDK's distinct id, did, which is usually the user id you set. Sessions without one count toward sessions but not toward users.

Browser: @ziplogger/browser 0.7.0+

Sessions are on by default as soon as you set release:

import { ZipLoggerBrowser } from '@ziplogger/browser'

export const ziplogger = new ZipLoggerBrowser({
  endpoint: 'https://app.ziplogger.ai',
  apiKey: 'YOUR_BROWSER_API_KEY',
  source: 'storefront',
  release: import.meta.env.VITE_RELEASE, // e.g. storefront@3f9a2c1
})

ziplogger.captureGlobalErrors()

The SDK starts one session per page load and sends it to POST /ingest/v1/sessions. After that it sends an update only when something changes:

  • The first log at error or fatal level marks the session errored and is reported right away. captureError() logs at error, so a handled error counts.
  • With captureGlobalErrors(), an uncaught error or an unhandled promise rejection marks the session crashed. Without that call, a browser session never crashes, it can only be errored.
  • When the page is left, the SDK reports the exit with a keepalive request so it still gets out.

The session's did is the user id when there is one, from the userId option or ziplogger.identify(user.id), and the browser's anonymous id otherwise. So crash-free users counts browsers until people sign in. To turn sessions off, pass trackSessions: false. ziplogger.sessionStatus tells you the current state: ok, exited or crashed.

.NET: ZipLogger.Client 0.7.0+

The .NET client tracks one session per process. With the Microsoft.Extensions.Logging provider:

builder.Logging.AddZipLogger(options =>
{
    options.Endpoint = "https://app.ziplogger.ai";
    options.ApiKey   = "zk_...";
    options.Release  = $"checkout-api@{builder.Configuration["GIT_SHA"]}";
});

With Serilog, set it in the configure callback: .WriteTo.ZipLogger("https://app.ziplogger.ai", "zk_...", o => o.Release = "checkout-api@3f9a2c1"). For a console app or worker that uses the shipper directly:

await using var shipper = new ZipLoggerShipper(new ZipLoggerClientOptions
{
    Endpoint = "https://app.ziplogger.ai",
    ApiKey = "zk_...",
    Release = "nightly-import@3f9a2c1",
    SessionDistinctId = Environment.MachineName,
});

The session is reported when the client starts, at the first error-level log (LogError, LogCritical, Serilog Error or Fatal), when the client is disposed (exited) and on an unhandled exception (crashed). The crash report is sent synchronously, because the process is going down. An exception that leaves Main through await using disposes the client first, so an exit is sent before the crash. The crash replaces it.

SessionDistinctId sets did, for crash-free users. For a server, a machine or tenant id is a reasonable choice. Turn sessions off with TrackSessions = false. Setting TrackSessions = true without a Release throws when the client is created.

Set Release in the options. The client fills the release on log lines from ZIPLOGGER_RELEASE or the assembly version when you leave it empty, but sessions only start when Release itself is set.

Sentry SDKs

Sentry SDKs send release health in the same envelopes as errors, when a release is configured. Point the SDK at a ZipLogger DSN (how to get one) and set release to service@sha:

import * as Sentry from '@sentry/browser'

Sentry.init({
  dsn: 'https://[email protected]/1',
  release: `storefront@${import.meta.env.VITE_COMMIT_SHA}`,
})
import os
import sentry_sdk

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    release=f"checkout-api@{os.environ['GIT_SHA']}",
)

ZipLogger reads both kinds of session item. A session item is one user or process session; a later update replaces the earlier one. A sessions item holds per-minute counts from SDKs running in server mode; each bucket adds to the totals, and an identical resend is counted once. The response to the envelope lists them under accepted.sessions.

Sentry's browser and Node SDKs send sessions by default. If yours does not, look for its session tracking option.

Other languages: the HTTP endpoint

Any code can send sessions in the same shape. POST /ingest/v1/sessions takes a JSON array of at most 100 sessions, with an ingest key in X-Api-Key:

curl -X POST https://app.ziplogger.ai/ingest/v1/sessions \
  -H "X-Api-Key: zk_..." \
  -H "Content-Type: application/json" \
  -d '[{"sid":"7c1f0e3a9b2d4e5f","did":"user-42","started":"2026-10-10T09:00:00Z","timestamp":"2026-10-10T09:12:00Z","status":"exited","errors":0,"attrs":{"release":"worker@3f9a2c1"}}]'

The answer is 202 with {"accepted":1,"stored":true}. Send the same sid again with a later timestamp to update it. A session without attrs.release or sid is skipped.

Alert when a release gets worse

Go to Alerts → Detection → Rules, click + New alert, and pick crash-free sessions below N % (release health) as the Condition. It has three fields of its own:

  • Release (optional): one release exactly as the SDK reports it, for example [email protected]. Leave it empty to watch all releases in the environment together; the notification then names the worst one.
  • Crash-free sessions below (%): the threshold, for example 99.5.
  • Window (min): sessions that started in the last N minutes. It must be at least 15.

With fewer than 20 sessions in the window, the rule keeps its current state instead of firing or resolving, so a quiet hour with a handful of sessions cannot flip it on a single crash. The notification says how many of how many sessions crashed. Routing works like any other rule; see log alert rules.

Details that matter

  • Sessions without a release are dropped. No release, no row.
  • The environment comes from the ingest key, not from the SDK's environment setting. Use one key per environment, as described in separate staging and production logs.
  • Sessions are kept for 90 days and are not metered, so they do not count toward your plan's volume.
  • Sends are best effort. Neither ZipLogger SDK retries a failed session update, and they never throw into your code.

What it does not do

  • No native crash handling. There is no minidump processing and no symbolication of native crashes. Sentry attachments, including minidumps, are acknowledged and dropped.
  • No ANR or hang detection of its own. A session is abnormal only when the SDK reports abnormal. ZipLogger's browser and .NET SDKs never do.
  • Issue pages show no crash-free rate. An Issue counts occurrences from logs, and there is no session total to compare with. Crash-free numbers are per release, on the Releases page only.
  • A browser session ends when the tab is hidden, not only when the page is closed. The SDK reports the exit when the visibility changes to hidden and does not start a new session when the tab comes back. A crash after the user switches back is still logged as an error, but it does not count against the release.
  • A browser crash means any uncaught error, even one the page recovers from. The first one marks the session crashed for good.
  • A server process is one long session. ASP.NET Core catches exceptions thrown while handling a request, so they make the session errored (when logged at error level), not crashed. A process killed from outside, by an out-of-memory kill or a forced stop, never sends its end and stays as last reported. Because windows select by start time, a process that started before the window is not in it.
  • Single-page apps get one session per page load, not one per route change.

Next step

The reference is in the docs under release health. To try it, create a free workspace, make an ingest key, set release in your SDK, and open Issues → Releases after your next deploy.

Use the Sentry SDK with ZipLogger

Keep your Sentry SDK and send its errors, traces and logs to ZipLogger by changing the DSN. Node, browser and Python setups, and what is dropped.

JavaScript and React error tracking

Catch uncaught JavaScript errors, promise rejections and React render errors with @ziplogger/browser. Real payloads, plus what it does not do.

ASP.NET Core request metrics: p95 and throughput

Time every ASP.NET Core request with one NuGet package and see average latency, p95 and throughput per service. Real payloads, config, and limits.

Try it on your own logs

There is a free plan and no sales call. Install an SDK, send a few log lines, and search them.