Repository navigation
Conversation
Run CI on `v1` like on `master`, and dispatch console compatibility checks to the same-named console branch for commits cut from `v1`.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2196 +/- ##
==========================================
+ Coverage 75.71% 75.98% +0.26%
==========================================
Files 103 104 +1
Lines 28316 29977 +1661
Branches 5208 5559 +351
==========================================
+ Hits 21439 22777 +1338
- Misses 5529 5808 +279
- Partials 1348 1392 +44 🚀 New features to boost your workflow:
|
The collective PR runs CI on every push to v1 already.
Pull request analyses are stored against the merge ref, so PRs into v1 had no baseline and compared against master.
Set CMake [`VERSION`](https://cmake.org/cmake/help/latest/prop_tgt/VERSION.html) and [`SOVERSION`](https://cmake.org/cmake/help/latest/prop_tgt/SOVERSION.html) properties. Close: #1428
* ci: include version in the release archive name Fixes: #745 * ci: keep a stable archive name for latest downloads Create a `sentry-native.zip` copy for the GCS `latest/` path so each release replaces the same object. Keep versioned archive names for GitHub and version-specific GCS paths.
BREAKING CHANGE: Linux, macOS, and Windows builds now default to the `native` backend instead of `crashpad`.
The custom capture hook required private SDK types, while screenshot options were still experimental. BREAKING CHANGE: Implement sentry_screenshot_capture(const char *, uint32_t), returning 0 on success.
The factory used a private-style `sentry__`-prefixed name even though custom transports must provide its implementation. Give it a public-style name and document the contract for custom implementations. BREAKING CHANGE: Custom transport implementations must define: ``` sentry_transport_t *sentry_transport_new_default(void) ```
Keep the scope write lock across a batch while allowing the owning thread to use existing scope accessors without relocking. Defer backend scope flushes until the outermost batch completes.
Split the existing global scope into an internal, read-only global scope for SDK/OS metadata and configured defaults, and a mutable isolation scope used by top-level APIs. Move user data, observers, and backend flushing to the isolation scope. Add `sentry_scope_acquire()` and `sentry_scope_release()` for direct isolation scope access. Implement Sentry's three-scope model with global -> isolation -> local scope layering. Retain parent references and merge layers when preparing events, telemetry, and attachments. Local values take precedence over isolation and global defaults. The SDK follows the global mode used by other Sentry SDKs for client applications: all threads share the same process-wide isolation scope. Changes made through top-level APIs or an acquired reference are shared across all threads. See: - https://develop.sentry.dev/sdk/foundations/state-management/scopes/#isolation-scope
Provide exception metadata, registers, and a basic crashing-thread stack for crash identification in native `on_crash`, matching `inproc`. Use the same payload for `before_send` when `on_crash` is unset. - Capture up to 128 fault-context IPs and unwind source in shared memory. Retain the faulting IP when available. - Linux: SDK `libunwind` with signal-safe logging. - macOS: SDK frame-pointer walker, guarded Mach reads, PAC stripping, `__DARWIN_OPAQUE_ARM_THREAD_STATE64` accessors, and `instruction_addr_adjustment=none`. - Android/Linux without `libunwind`: bounded frame-pointer capture via `process_vm_readv`; avoids `libunwindstack`'s C++ allocation. - Windows: MSVC x64 `RtlLookupFunctionEntry`/`RtlVirtualUnwind` walking without DbgHelp; SP progress permits recursive frames. Frame-pointer capture on x86/ARM64 and other x64 builds. - ARM32 frame-pointer fallbacks retain only the faulting IP. The daemon reuses partial captures and enriches existing frames. Preserve exception/register/frame edits and removals, and honor callback rejection.
Skip Windows frame symbol lookup when symbolize_stacktraces is disabled, including crash frames captured before daemon processing.
Add length-prefixed frames and app-to-daemon IPC based on Unix domain stream socket pairs and an anonymous pipe on Windows. Ref: #1834
* perf(native): Sync scope over IPC Instead of flushing the whole scope/event on disk on every scope change, send an initial scope snapshot and then minimal incremental updates to the crash daemon over IPC. Keep the latest complete state in daemon memory for crash reports. Discard interrupted messages and preserve crash callback changes through a final event and attachment snapshot. Close: #1834 * handle SENTRY_CRASH_STATE_CRASHING * signal.h
The inproc-based Android NDK uses `SENTRY_TRANSPORT=none` and installs a JNI function transport that writes to `outbox/`, leaving HTTP delivery to Java. Tests substitute it with an stdout callback to inspect envelopes without Java or HTTP. However, the Native daemon in #1725 cannot use the function transport from the separate daemon process. Provide (an internal) built-in stdout transport (`SENTRY_TRANSPORT=stdout`) that both the SDK and daemon can instantiate. This is mostly for out own integration testing purposes, but it's also super handy for quick experiments: ```sh $ make build SENTRY_BACKEND=inproc SENTRY_TRANSPORT=stdout ... $ ./build/sentry_example_crash ... [sentry] DEBUG sending envelope {"event_id":"0e1d5355-5ba8-42cb-9e96-c6610d4ee9c1",...} {"type":"event","length":2823} {"event_id":"0e1d5355-5ba8-42cb-9e96-c6610d4ee9c1","timestamp":"2026-10-16T16:19:26.968633Z","platform":"native","level":"fatal","exception":...} [sentry] INFO entering signal handler ... ```
* fix(native): group proc maps modules by load `capture_modules_from_proc_maps()` merged all mappings of a file into one module, producing oversized, overlapping images when a file is mapped more than once. This is common on Android, for example an APK that backs several native libraries. Keying by path and `start - offset` instead splits libraries whose segments differ in `p_vaddr - p_offset`, such as `libm.so.6` on x86_64 Linux. Start a new module at a mapping with offset 0 unless the file was captured last, which keeps small libraries that map every segment at offset 0 together. Other mappings extend the file's most recent module. Skip non-readable mappings and mappings whose offset exceeds their start address. Enable the `debug_meta` overlap check of the native crash tests on Linux. * Update CHANGELOG.md
Move the Android API < 23 syscall fallback from the module finder into `sentry__process_vm_readv()` in `sentry_os.c`, and use it in the module finder, the native minidump writer, and the crash daemon stack reader.
Remove obsolete implementations and legacy user-report tests. List replacements and migration details in the v1 changelog. BREAKING CHANGE: Deprecated public APIs are no longer available.
The Android API 35 job failed before emulator startup: > Warning: An error occurred while preparing SDK package Google APIs Intel > x86_64 Atom System Image: dl.google.com. > ... > error: could not connect to TCP port 5554: Connection refused https://github.com/getsentry/sentry-native/actions/runs/39443426624/job/118698972826 Retry SDK installation and emulator startup failures once while keeping failures after boot fatal on the original test step. Use smaller AOSP images to reduce setup downloads. The tests do not use Google Play services, and the API 35 x86_64 image is about 780 MB instead of 1.7 GB. Both variants still come from `dl.google.com`, so retries remain necessary for transient download failures.
The daemon logs from both its main thread and the transport worker thread, but `daemon_file_logger` used `localtime`, which shares a static result buffer and re-runs `tzset` on every call. Under TSAN, the resulting race report in the worker thread could deadlock against the main thread priming libunwind inside `dl_iterate_phdr`, hanging the daemon before it entered its main loop. Keep `localtime` on Windows, where the CRT result is thread-local.
`sentry__usec_time_to_iso8601` omits the fractional part when the microseconds are zero, which is valid RFC 3339. The transaction tests parsed timestamps with a fixed `%Y-%m-%dT%H:%M:%S.%fZ` format and failed whenever a timestamp landed exactly on a whole second. Parse them with `datetime.fromisoformat` instead, as `assertions.py` already does.
Deploy the selected crash handler beside `sentry_test_unit` so unit-only builds work with the default hierarchical output layout. Run CTest unit cases serially because they share filesystem fixtures. Allow `make test-unit` and `make test-leaks` to select `SENTRY_BACKEND` and `SENTRY_TRANSPORT`, preserving `none` as the default backend. This allows resuing the convenient ready-made targets for any backend and transport combination. Close: #2215
* feat: add `sentry_scope_set_trace` Add `sentry_scope_set_trace()` and `sentry_scope_set_trace_n()` to set the trace on a given scope, so that a local scope can carry a distinct trace for a concurrent unit of work. The global `sentry_set_trace_n()` now forwards to the scope-level setter. * fix: use the capture scope's DSC for event envelopes Scoped event and feedback capture apply the given scope to the event payload, but `sentry__envelope_add_event()` read the dynamic sampling context (DSC) and the propagation context's `sample_rand` from the isolation scope through `SENTRY_WITH_SCOPE`. Since `sentry_scope_set_trace()` lets a local scope carry its own propagation context and DSC, such an envelope mixed metadata from two scopes: - the event's trace context came from the local scope, - the header's `trace_id` matched, because it is copied from the event payload, - `sample_rand`, `sampled`, `release` and `environment` came from the isolation scope. For example, with a `traces_sample_rate` of 0.5, an isolation scope with a `sample_rand` of 0.25 and a local scope with 0.75, scoped capture sent `sampled: "true"` instead of the local scope's `"false"`. Feedback was affected the same way, because `sentry__envelope_add_feedback_event()` delegates to `sentry__envelope_add_event()`. Other Sentry SDKs avoid this by computing the DSC from the scope that is applied to the event and carrying it on the event as SDK-internal data, which the envelope builder moves into the header: - sentry-go stores it in `event.sdkMetaData.dsc` from `Scope.ApplyToEvent`, - sentry-php stores it in the event's SDK metadata from `Scope::applyToEvent`, - sentry-javascript stores it in `event.sdkProcessingMetadata`, - sentry-dotnet stores it in an internal `SentryEvent.DynamicSamplingContext`, - sentry-python stores it in `contexts.trace.dynamic_sampling_context`, which the client pops before building the envelope. In all of them the envelope is a plain container that never looks up a scope. Follow the same pattern: when the new `SENTRY_SCOPE_DSC` mode is set, `sentry__scope_apply_to_event()` stamps the DSC on non-transaction events under an internal `dsc` key, and `sentry__envelope_add_event()` removes it from the payload and uses it as the `trace` header. The envelope no longer reads any scope or options. The mode is opt-in because not every scope-applied event goes through `sentry__envelope_add_event()`. Crashpad's handler uploads the on-disk `__sentry-event` directly, and the native backend's daemon writes its own envelopes. Relay reports unknown top-level event attributes as invalid, so those paths apply their scopes without the mode. `SENTRY_SCOPE_ALL` includes it, which covers `sentry__prepare_event()`, and feedback requests it explicitly. As a result, crashpad envelopes built from reports in a later run no longer get a `trace` header from that run's isolation scope, which never described the crash. * test: add integration test for scoped trace propagation Capture an event with a local scope whose trace is set with `sentry_scope_set_trace()`, while the isolation scope carries a different trace, and check that both the event's trace context and the envelope `trace` header come from the local scope. The header's `sample_rand` must match the event's. Before the DSC was taken from the capture scope, it came from the isolation scope instead. * Update CHANGELOG.md
* feat: add `sentry_continue_trace` Add `sentry_continue_trace()`, `sentry_scope_continue_trace()` and their `_n` variants to continue a trace from incoming `sentry-trace` and `baggage` headers without starting a transaction, as required by the trace propagation spec's `continueTrace`. The headers are parsed with the same code as `sentry_transaction_context_update_from_header()`. When strict trace continuation allows it, the scope's propagation context takes the incoming trace and parent span IDs, sampling decision and `sample_rand`, and the incoming baggage is frozen as the DSC. Otherwise, a new trace is started. Like with `sentry_set_trace()`, transactions no longer act as trace boundaries afterwards. Per the spec, a missing `sentry-sample_rand` is derived from the incoming `sample_rate` and sampling decision, transactions started afterwards inherit the sampling decision, and the DSC of error events reports it instead of recomputing `sampled` from the local sample rate. * Update CHANGELOG.md * fix: set/continue traces from the initial scope `sentry_scope_continue_trace()` looked up the global options, which are not installed yet while `sentry_init()` runs the initial scope callback, so a trace continued from there was dropped. Take the options from the scope chain instead, where the isolation scope carries them already, and do the same in `sentry_scope_set_trace()`. Also build the default DSC before running the initial scope callback, so that it no longer overwrites a DSC frozen from incoming baggage. * fix: continue traces on cloned scopes A clone of the isolation scope has neither its own options nor a parent that carries them, so `ref_options()` found none, which made `sentry_scope_continue_trace()` a no-op on it and left the DSC stale in `sentry_scope_set_trace()`. Fall back to the global options when the scope chain carries none.
* feat: add `sentry_iter_trace_propagation_headers` Add `sentry_iter_trace_propagation_headers()` and `sentry_scope_iter_trace_propagation_headers()` to fetch the `sentry-trace` and `baggage` headers for downstream services without an active span, as required by the trace propagation spec's `getTraceData`. Until now, only `sentry_span_iter_headers()` and `sentry_transaction_iter_headers()` existed, so a trace could not be propagated without tracing. The headers come from the span or transaction bound to the scope, if any, otherwise from the scope's propagation context. Without a span, the sampling decision is deferred unless one was inherited with `sentry_continue_trace()`. Per the spec, the `sentry-trace` header then has no sampled flag and the SDK-built baggage has no `sentry-sampled`. The scoped variant falls back to the parent scopes for what the scope does not set. Ref: #2050 * Update CHANGELOG.md
Run the integration fixture twice against Sentry: an upstream app captures an event and prints its `sentry-trace` and `baggage` headers, and a downstream app continues the trace from them and captures its own event. Sentry must then report both events on the same trace, with the upstream span as the downstream parent.
The scope loaders for `tags`, `extra`, `contexts`, `attributes`, and `propagation_context` cloned the scope's own values before checking whether there were any. For an empty scope, the clone was freed right away, and the parent's values were returned instead. Every empty level still paid for allocating and freeing a `thing_t` handle for the empty value clone. Clone the values only when the scope has any or has no parent. An empty scope now returns the values loaded from its parent as is. The TODOs about optimizing merges into empty destinations are removed, because an empty scope no longer clones or merges anything. Measured against v1 (e42bdcb) with a root parent and a child scope on a pinned core, with CPU frequency scaling enabled, as the median CPU time of 40 samples per case: Values Parent Child Before After Improvement strings [1] 0 0 36.7-37.2 ns 20.3-20.7 ns 44.1-45.6% objects [2] 0 0 36.3-36.5 ns 20.4-20.5 ns 43.7-44.0% strings [1] 10 0 42.3-42.7 ns 25.3-25.7 ns 39.5-40.1% objects [2] 10 0 426.7-468.6 ns 411.8-457.4 ns 2.4-3.5% strings [1] 0 10 43.4-44.1 ns 43.9-44.1 ns ~0% objects [2] 0 10 417.6-482.8 ns 421.3-474.4 ns ~0% strings [1] 10 10 1626-1653 ns 1644-1667 ns ~0% objects [2] 10 10 2468-2575 ns 2474-2562 ns ~0% [1] `tags`, `extra`, `propagation_context` [2] `contexts`, `attributes`
…2241) * Report host OS context under Wine and Proton Under Wine, build the OS context from the host via wine_get_host_version and the host os-release read through the Z: drive, in the same shape as the native Linux OS context. Prefer /run/host to report the host instead of the Steam Runtime or Flatpak container. * Update changelog * Expect host OS context in Wine integration tests * Clean up * Update CHANGELOG to remove 'Features' section Removed the 'Features' section from the CHANGELOG. * Prefer host os-release on native Linux * Report SteamOS and Bazzite as the OS name * Expect named OS context in Linux test assertions
* feat(native): Support ELF modules at non-zero file offsets The minidump writer and `capture_modules_from_proc_maps()` grouped `/proc/<pid>/maps` entries by file name and expected an ELF header at offset 0. An ELF image can also start inside a larger file, such as an uncompressed library that Android maps straight from an APK, and such images were dropped from the module list. Start a module at each mapping whose file offset holds an ELF header, group the image's other segments by its load range, and read the build ID, size, and SONAME at that offset. Move `compute_elf_size_from_phdrs()` to `sentry_elf.h` to share it. This prepares the native backend for Android, where AGP's default packaging maps every app library from the APK. Ref: #1725 * fix(native): Apply the ELF offset to the build ID section headers The previous commit adjusted the section header offset in `elf_locate_symtab()` instead of `extract_elf_build_id_for_module()`, where `elf_offset` is not declared, so the crash daemon failed to compile. Move the adjustment to where the build ID is read. * fix(native): Keep embedded ELFs mapped below their file offset The `offset > start` guard in `capture_modules_from_proc_maps()` dropped mappings before an ELF image inside a larger file could be detected. The guard only protects the `start - offset` load base of a regular ELF file, so apply it there instead.
The crash daemon could only unwind the threads that did not crash with `libunwind_remote`, which Android builds lack because they use libunwindstack. Add a `libunwindstack_remote` backend, compiled in only with the native backend, that attaches to a thread with ptrace, reads its registers with `Regs::RemoteGet()`, and unwinds it. Share the ptrace attach loop with `libunwind_remote`. This prepares the native backend for Android.
* feat(native): Support ELF modules at non-zero file offsets The minidump writer and `capture_modules_from_proc_maps()` grouped `/proc/<pid>/maps` entries by file name and expected an ELF header at offset 0. An ELF image can also start inside a larger file, such as an uncompressed library that Android maps straight from an APK, and such images were dropped from the module list. Start a module at each mapping whose file offset holds an ELF header, group the image's other segments by its load range, and read the build ID, size, and SONAME at that offset. Move `compute_elf_size_from_phdrs()` to `sentry_elf.h` to share it. This prepares the native backend for Android, where AGP's default packaging maps every app library from the APK. Ref: #1725 * fix(native): Apply the ELF offset to the build ID section headers The previous commit adjusted the section header offset in `elf_locate_symtab()` instead of `extract_elf_build_id_for_module()`, where `elf_offset` is not declared, so the crash daemon failed to compile. Move the adjustment to where the build ID is read. * fix(native): Keep embedded ELFs mapped below their file offset The `offset > start` guard in `capture_modules_from_proc_maps()` dropped mappings before an ELF image inside a larger file could be detected. The guard only protects the `start - offset` load base of a regular ELF file, so apply it there instead. * feat(unwinder): Unwind remote threads with libunwindstack The crash daemon could only unwind the threads that did not crash with `libunwind_remote`, which Android builds lack because they use libunwindstack. Add a `libunwindstack_remote` backend, compiled in only with the native backend, that attaches to a thread with ptrace, reads its registers with `Regs::RemoteGet()`, and unwinds it. Share the ptrace attach loop with `libunwind_remote`. This prepares the native backend for Android. * feat(native): Support preloading the crash handler `sentry__backend_preload()` was a no-op for the native backend. Install its signal handlers early, so that they keep their place in the signal chain ahead of a managed runtime, and let a later `sentry_init()` adopt them. Like with `inproc`, shutdown keeps the preloaded handlers installed, forwarding crashes to the previous handlers until the next initialization.
Before `sentry_init()` and after `sentry_close()`, preloaded handlers are only a placeholder in the signal chain. A signal that reached them restored the previous handlers but left `g_handlers_installed` and `g_preloaded` set, so a later `sentry_init()` took the early return and never reinstalled the crash handler. The fall-through also re-raised the signal after the previous handler returned, killing the process even if a handler installed before the preload had recovered from it. Like the inproc backend, consume the preload state before forwarding the signal, and return to wherever the previous handler left off instead of re-raising. Re-raise only when the signal had its default action before the preload.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a4a98f3. Configure here.
Threads enumerated from `/proc/<pid>/task` have no saved registers, so the daemon called `process_vm_readv` at stack address 0 for each of them. The read always failed and logged before the remote unwinder took over. Skip the read when the stack pointer is unknown, and include `errno` in the remaining `process_vm_readv` and thread enumeration failures.
…2273) Allow `SENTRY_HANDLER_STRATEGY_CHAIN_AT_START` in the native backend on Android, as in the inproc backend, so that a signal that Mono turns into a managed exception, such as a `NullReferenceException`, is not captured as a native crash. As in the inproc backend, fall back to the default strategy below API 26, where debuggerd kills the process after the chained handler runs. This prepares the native backend for Android support (#1725).
* feat(linux): RISC-V support Support register capture, stack unwinding, ELF metadata, and native minidump generation. * Update CHANGELOG.md * context.version * llvm * setup-qemu-action for riscv64 * tweak skip conditions * gcc < 14 * ci: restore qemu-user for Arm32 * clarify & restore original phrasing
* feat: Add pkg-config support - Added support for generating and installing a .pc file for pkg-config - Controlled by the SENTRY_INSTALL_PKGCONFIG CMake option - Uses CMAKE_INSTALL_LIBDIR and CMAKE_INSTALL_INCLUDEDIR to support non-standard layouts (e.g., lib64) - Installs the generated file to the standard pkg-config directory * feat(pkgconfig): Expose the installed binary directory Export `bindir` in `sentry.pc` so consumers can locate the installed crash handler. Honor `CMAKE_INSTALL_BINDIR` when installing the native handler and its Windows companion so the metadata matches the layout. Add a standalone consumer that links through pkg-config and initializes the default backend using the installed handler and a writable database directory. * copy what inproc_stress_test does * fix(pkgconfig): Install sentry.pc only for shared builds A static `libsentry` needs its full private link graph, including vendored backend archives, system libraries and the C++ runtime. The CMake package already exports that graph. Install `sentry.pc` only for shared builds, where `-lsentry` is complete. --------- Co-authored-by: J-P Nurmi <jpnurmi@gmail.com>
Running the native backend tests on Android also enabled `test_pkgconfig`, which expects a desktop install. Android builds have no versioned `libsentry.so` symlinks, macOS hosts look for a `.dylib`, and the consumer would have to run on the device. Skip it like `test_shared_lib`.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Warning
WIP 🚧🔨⏳⛔
Close: #2044
#skip-changelog (WIP)