From c3fb970a28edd6d661b7c044920b264e83fc42c3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:01:35 +0200 Subject: [PATCH 01/27] chore: disable mtime tests on switch (#1357) --- tests/unit/test_path.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_path.c b/tests/unit/test_path.c index 62d418f6e..938d6c53b 100644 --- a/tests/unit/test_path.c +++ b/tests/unit/test_path.c @@ -251,6 +251,9 @@ SENTRY_TEST(path_directory) SENTRY_TEST(path_mtime) { +#if defined(SENTRY_PLATFORM_NX) + return SKIP_TEST(); +#endif sentry_path_t *path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX "foo.txt"); TEST_ASSERT(!!path); From c9f5602de7d7dd3af0324ba53ab575ff366a766d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Tue, 2 Sep 2025 20:12:42 +0200 Subject: [PATCH 02/27] test: explicitly specify release in envelope tests (#1356) this failed on downstream SDKs that automatically determine release --- tests/unit/test_envelopes.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index d6abece84..5acfea790 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -10,7 +10,7 @@ static char *const SERIALIZED_ENVELOPE_STR = "{\"dsn\":\"https://foo@sentry.invalid/42\"," "\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"trace\":{" "\"public_key\":\"foo\",\"org_id\":\"\",\"sample_rate\":0,\"sample_" - "rand\":0.01006918276309107,\"release\":null,\"environment\":" + "rand\":0.01006918276309107,\"release\":\"test-release\",\"environment\":" "\"production\",\"sampled\":\"false\"}}\n" "{\"type\":\"event\",\"length\":71}\n" "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"some-" @@ -255,6 +255,7 @@ create_test_envelope() { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(options, "test-release"); sentry_init(options); sentry_uuid_t event_id From 9a5c14f9aefdba6b3727c6601e4b56ff69dc840b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:56:28 +0200 Subject: [PATCH 03/27] fix: Add Xbox networking initialization to WinHTTP transport (#1359) * fix: Add Xbox networking initialization to WinHTTP transport - Add Xbox-specific network initialization before WinHTTP operations - Ensures Xbox XNetworking APIs are ready before HTTP requests - Fixes WinHTTP error 12007 (ERROR_WINHTTP_CANNOT_CONNECT) on Xbox platforms - Uses 60-second timeout to prevent indefinite blocking - Graceful failure with warning when network not ready - Minimal changes using existing SENTRY_PLATFORM_XBOX ifdef pattern Addresses Xbox networking requirements where WinHTTP needs network connectivity to be established through Xbox-specific APIs before HTTP operations can succeed. * Fix include formatting for Xbox transport header * Remove timeout from Xbox network initialization check * chore: changelog --- CHANGELOG.md | 6 ++++++ src/transports/sentry_transport_winhttp.c | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5097215be..c8bab372c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Internal:** + +- Support downstream Xbox SDK specifying networking initialization mechanism ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) + ## 0.10.1 **Internal:** diff --git a/src/transports/sentry_transport_winhttp.c b/src/transports/sentry_transport_winhttp.c index d9c7b60b1..ebc683def 100644 --- a/src/transports/sentry_transport_winhttp.c +++ b/src/transports/sentry_transport_winhttp.c @@ -9,6 +9,10 @@ #include "sentry_transport.h" #include "sentry_utils.h" +#ifdef SENTRY_PLATFORM_XBOX +# include "sentry_transport_xbox.h" +#endif + #include #include #include @@ -211,6 +215,15 @@ sentry__winhttp_send_task(void *_envelope, void *_state) url_components.dwUrlPathLength = 1024; WinHttpCrackUrl(url, 0, 0, &url_components); + +#ifdef SENTRY_PLATFORM_XBOX + // Ensure Xbox network connectivity is initialized before HTTP requests + if (!sentry__xbox_ensure_network_initialized()) { + SENTRY_WARN("Xbox: Network not ready, skipping HTTP request"); + goto exit; + } +#endif + if (!state->connect) { state->connect = WinHttpConnect(state->session, url_components.lpszHostName, url_components.nPort, 0); From 519554ff62e1b77564345d25c531e99dda7337f8 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 4 Sep 2025 14:37:14 +0200 Subject: [PATCH 04/27] ci: fix failing mingw build (#1361) * ci: fix failing mingw build * split `ASM_MASM_COMPILER` and `_FLAGS` * add `ASM_MASM_FLAGS` in `mingw` install step * specify the `CMAKE_ASM_MASM_COMPILER` as a `FILEPATH` * clean up CMAKE_DEFINES construction so it is easier to diff in the future * fix `LLVM_MINGW_INSTALL_PATH` to be referenced locally rather than $env --- .github/workflows/ci.yml | 19 ++++++++++++++++++- scripts/install-llvm-mingw.ps1 | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95242f17c..55062ad23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,7 +150,8 @@ jobs: os: windows-latest TEST_MINGW: 1 MINGW_PKG_PREFIX: x86_64-w64-mingw32 - MINGW_ASM_MASM_COMPILER: llvm-ml;-m64 + MINGW_ASM_MASM_COMPILER: llvm-ml + MINGW_ASM_MASM_FLAGS: -m64 # The Android emulator is currently only available on macos, see: # https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/android?view=azure-devops#test-on-the-android-emulator # TODO: switch to reactivecircus/android-emulator-runner, concurrently running emulators continuously fail now. @@ -244,12 +245,28 @@ jobs: if: ${{ runner.os == 'macOS' && matrix.os == 'macos-15-large' && matrix.RUN_ANALYZER == 'asan,llvm-cov' }} run: echo $(brew --prefix llvm@18)/bin >> $GITHUB_PATH + - name: Remove Strawberry Perl from PATH + if: ${{ runner.os == 'Windows' }} + shell: powershell + run: | + $strawberryBins = @( + 'C:\Strawberry\c\bin', + 'C:\Strawberry\perl\site\bin', + 'C:\Strawberry\perl\bin' + ) + + $cleanedPaths = $env:Path -split ';' | Where-Object { $_ -and ($strawberryBins -notcontains $_) } + $newPath = ($cleanedPaths -join ';') + + "PATH=$newPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Installing LLVM-MINGW Dependencies if: ${{ runner.os == 'Windows' && env['TEST_MINGW'] }} shell: powershell env: MINGW_PKG_PREFIX: ${{ matrix.MINGW_PKG_PREFIX }} MINGW_ASM_MASM_COMPILER: ${{ matrix.MINGW_ASM_MASM_COMPILER }} + MINGW_ASM_MASM_FLAGS: ${{ matrix.MINGW_ASM_MASM_FLAGS }} run: . "scripts\install-llvm-mingw.ps1" - name: Set up zlib for Windows diff --git a/scripts/install-llvm-mingw.ps1 b/scripts/install-llvm-mingw.ps1 index 3e6d40e08..470e207ef 100755 --- a/scripts/install-llvm-mingw.ps1 +++ b/scripts/install-llvm-mingw.ps1 @@ -55,4 +55,19 @@ Expand-Archive -LiteralPath "${NINJA_DL_PATH}" -DestinationPath "${NINJA_INSTALL "PATH=${NINJA_INSTALL_PATH}; $env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # export CMAKE_DEFINES to the runner environment -"CMAKE_DEFINES=${env:CMAKE_DEFINES} -DCMAKE_C_COMPILER=${env:MINGW_PKG_PREFIX}-gcc -DCMAKE_CXX_COMPILER=${env:MINGW_PKG_PREFIX}-g++ -DCMAKE_RC_COMPILER=${env:MINGW_PKG_PREFIX}-windres -DCMAKE_ASM_MASM_COMPILER=${env:MINGW_ASM_MASM_COMPILER}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append \ No newline at end of file +$cmakeDefines = @() + +if ($env:CMAKE_DEFINES) { + $cmakeDefines += $env:CMAKE_DEFINES +} + +$cmakeDefines += @( + "-DCMAKE_C_COMPILER=$($env:MINGW_PKG_PREFIX)-gcc" + "-DCMAKE_CXX_COMPILER=$($env:MINGW_PKG_PREFIX)-g++" + "-DCMAKE_RC_COMPILER=$($env:MINGW_PKG_PREFIX)-windres" + "-DCMAKE_ASM_MASM_COMPILER:FILEPATH=$($LLVM_MINGW_INSTALL_PATH -replace '\\','/')/bin/$($env:MINGW_ASM_MASM_COMPILER).exe" + "-DCMAKE_ASM_MASM_FLAGS=$env:MINGW_ASM_MASM_FLAGS" +) + +"CMAKE_DEFINES=$($cmakeDefines -join ' ')" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + From 7b4769bd4639d5beeeed15ed4ea82894f23aa7a5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:22:47 +0200 Subject: [PATCH 05/27] feat: add version embedding functionality for downstream platform SDKs (#1340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add version embedding functionality for platform SDKs Add CMake options to embed version information in the binary: - SENTRY_EMBED_INFO: Enable/disable version embedding - SENTRY_BUILD_PLATFORM: Platform name (defaults to CMAKE_SYSTEM_NAME) - SENTRY_BUILD_VARIANT: Build variant identifier - SENTRY_BUILD_ID: Build identifier (defaults to timestamp) - SENTRY_EMBED_INFO_ITEMS: Additional custom key:value pairs The embedded information is stored as a C string `sentry_library_info` containing semicolon-separated key:value pairs for easy parsing. This allows platform SDKs (Switch, PlayStation, Xbox, etc.) to embed build metadata that can be extracted from binaries for debugging and support purposes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * test: add unit tests for version embedding functionality Add comprehensive unit tests for the version embedding feature: - Test embedded info format and content validation - Test proper fallback when feature is disabled - Verify SENTRY_VERSION field contains valid version string - Validate semicolon-separated field format Tests work correctly in both scenarios: - When SENTRY_EMBED_INFO=ON: validates actual embedded content - When SENTRY_EMBED_INFO=OFF: confirms feature is properly disabled 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * refactor: improve embedded info tests with individual skip checks - Replace single ifdef with per-test-case conditional logic - Use SKIP_TEST() for better test reporting when conditions not met - Add exact version string validation in embedded_info_sentry_version - Fix template file to include proper trailing newline - Improve test clarity and maintainability Tests now properly skip when SENTRY_EMBED_INFO is not applicable rather than always passing with stub implementations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * docs: add changelog entry for version embedding feature Add entry to Unreleased section for version embedding functionality as requested by danger bot. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * docs: remove changelog entry for internal feature Version embedding is an internal build feature that doesn't affect the public API or user-facing functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * test: add Python integration tests for embedded version info Add comprehensive pytest integration tests that: - Test embedded info functionality with various CMake configurations - Verify binary inspection using strings command - Test custom items and build parameters - Validate both enabled and disabled scenarios - Use existing cmake test infrastructure for consistent builds These tests integrate with the existing Python test suite and provide end-to-end validation of the version embedding feature. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * chore: improve cmake build error reporting in CI Show actual CMake build errors in CI instead of just 'cmake build failed'. This will help diagnose build issues more quickly. * chore: improve cmake configure error reporting in CI Also show actual CMake configure errors in CI, not just build errors. This will help diagnose both configuration and build issues. * fix: skip embedded info binary test on 32-bit Linux Use the existing has_http condition to skip the binary inspection test on 32-bit Linux builds where CURL dependencies are not available. This follows the same pattern used by other tests in the codebase. * fix: use SENTRY_API for Windows DLL symbol export/import The sentry_library_info symbol needs to be properly exported from Windows DLLs using SENTRY_API (__declspec(dllexport/dllimport)). This fixes linking issues on Windows ClangCL builds. * fix: resolve embedded info symbol linking and Windows DLL export issues - Use conditional SENTRY_API only on Windows for DLL export/import semantics - Use simple extern "C" on other platforms to avoid symbol visibility issues - Add generated embedded info file to sentry target sources automatically - Remove manual cache variable handling - target_sources() handles inclusion automatically - All unit tests and Python integration tests now pass * fix: Windows ClangCL compilation issues for embedded info - Replace strdup with _strdup on Windows to avoid deprecation warnings - Restructure extern "C" block in template for proper symbol declaration on Windows ClangCL - Both fixes target specific Windows compilation warnings that were causing CI failures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: resolve Windows symbol linkage issues for embedded info The embedded info symbol was failing to link properly on Windows due to: 1. ClangCL/LLVM-MinGW: const variables in C++ have internal linkage by default, but dllexport requires external linkage 2. MSVC: unresolved external symbol when building tests Solution: - Add explicit 'extern' keyword for DLL builds to ensure external linkage - Clarify that test builds always compile the symbol directly into the test executable (not imported from DLL) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: simplify Windows ClangCL compatibility for embedded info - Add forward declaration to satisfy ClangCL's -Wmissing-variable-declarations - Remove all platform-specific conditionals by leveraging SENTRY_API macro - Reduce code complexity from 31 lines to 15 lines - Maintain compatibility with all platforms and build configurations - Fix CI failures on Windows ClangCL builds 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * revert: remove debugging changes from tests/cmake.py Revert the debugging error reporting changes that were added to help diagnose CI build issues. Since the actual Windows ClangCL issue has been fixed, these debugging changes are no longer needed. This reverts commits: - a6c7e86 (chore: improve cmake configure error reporting in CI) - 011691e (chore: improve cmake build error reporting in CI) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Update tests/unit/test_embedded_info.c * fix: improve robustness of embedded version info implementation - Add validation for SENTRY_EMBED_INFO_ITEMS format (key:value) - Fail build on invalid format instead of warning - Escape special characters in custom items to prevent CMake substitution issues - Replace fixed buffer with dynamic allocation in version parsing test - Add proper error handling for memory allocation failures - Use TEST_ASSERT for critical NULL checks in tests Addresses review feedback to prevent potential buffer overflows and parsing issues with malformed embedded info items. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: use TEST_ASSERT for null check to prevent crash on strlen The embedded_info_basic test now uses TEST_ASSERT instead of TEST_CHECK for the sentry_library_info null check. This prevents a potential crash on the subsequent strlen() call if the pointer is NULL, ensuring consistent error handling with other tests in the file. --------- Co-authored-by: Claude --- CMakeLists.txt | 50 +++++++++++ src/sentry_embedded_info.cpp.in | 15 ++++ tests/test_embedded_info.py | 152 ++++++++++++++++++++++++++++++++ tests/unit/CMakeLists.txt | 6 ++ tests/unit/test_embedded_info.c | 106 ++++++++++++++++++++++ tests/unit/tests.inc | 4 + 6 files changed, 333 insertions(+) create mode 100644 src/sentry_embedded_info.cpp.in create mode 100644 tests/test_embedded_info.py create mode 100644 tests/unit/test_embedded_info.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 773f4fd6c..9a55ff05d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,25 @@ option(SENTRY_BUILD_TESTS "Build sentry-native tests" "${SENTRY_MAIN_PROJECT}") option(SENTRY_BUILD_EXAMPLES "Build sentry-native example(s)" "${SENTRY_MAIN_PROJECT}") option(SENTRY_BUILD_BENCHMARKS "Build sentry-native benchmarks" OFF) +# Platform version embedding options +option(SENTRY_EMBED_INFO "Embed version information in binary" OFF) +set(SENTRY_BUILD_PLATFORM "${CMAKE_SYSTEM_NAME}" CACHE STRING "Platform name for embedded version (e.g., switch, playstation, xbox)") +set(SENTRY_BUILD_VARIANT "" CACHE STRING "Build variant for embedded version (e.g., native, unreal)") +set(SENTRY_BUILD_ID "" CACHE STRING "Build identifier for embedded version (defaults to timestamp)") +set(SENTRY_EMBED_INFO_ITEMS "" CACHE STRING "Additional items to embed (semicolon-separated key:value pairs)") + +# Validate the format of custom items +if(SENTRY_EMBED_INFO_ITEMS) + # Convert semicolon-separated string to list for iteration + string(REPLACE ";" "\n" TEMP_ITEMS "${SENTRY_EMBED_INFO_ITEMS}") + string(REPLACE "\n" ";" ITEMS_LIST "${TEMP_ITEMS}") + foreach(ITEM IN LISTS ITEMS_LIST) + if(ITEM AND NOT ITEM MATCHES "^[^:]+:[^:]*$") + message(FATAL_ERROR "Invalid embedded info item format: ${ITEM}. Expected format: key:value") + endif() + endforeach() +endif() + if(NOT XBOX) option(SENTRY_LINK_PTHREAD "Link platform threads library" ON) if(SENTRY_LINK_PTHREAD) @@ -286,6 +305,37 @@ if (XBOX) endif() target_sources(sentry PRIVATE "${PROJECT_SOURCE_DIR}/include/sentry.h") add_library(sentry::sentry ALIAS sentry) + +# Configure version embedding if enabled +if(SENTRY_EMBED_INFO) + # Use timestamp as default build ID if not specified + if(NOT SENTRY_BUILD_ID) + string(TIMESTAMP SENTRY_BUILD_ID "%Y-%m-%d %H:%M:%S UTC" UTC) + endif() + + # Validate and escape special characters in custom items if needed + if(SENTRY_EMBED_INFO_ITEMS) + # Sanitize input to prevent CMake substitution issues + string(REPLACE "\"" "\\\"" SENTRY_EMBED_INFO_ITEMS "${SENTRY_EMBED_INFO_ITEMS}") + endif() + + # Ensure custom items end with semicolon if not empty + if(SENTRY_EMBED_INFO_ITEMS AND NOT SENTRY_EMBED_INFO_ITEMS MATCHES ";$") + set(SENTRY_EMBED_INFO_ITEMS "${SENTRY_EMBED_INFO_ITEMS};") + endif() + + # Ensure the output directory exists + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/src") + + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/sentry_embedded_info.cpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/src/sentry_embedded_info.cpp" + @ONLY + ) + + target_sources(sentry PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/src/sentry_embedded_info.cpp") +endif() + add_subdirectory(src) target_compile_definitions(sentry PRIVATE SENTRY_HANDLER_STACK_SIZE=${SENTRY_HANDLER_STACK_SIZE}) diff --git a/src/sentry_embedded_info.cpp.in b/src/sentry_embedded_info.cpp.in new file mode 100644 index 000000000..db4401d80 --- /dev/null +++ b/src/sentry_embedded_info.cpp.in @@ -0,0 +1,15 @@ +// src/sentry_embedded_info.cpp.in +#include "sentry.h" + +// Forward declaration to satisfy ClangCL's -Wmissing-variable-declarations +extern "C" SENTRY_API const char sentry_library_info[]; + +// Definition +extern "C" SENTRY_API const char sentry_library_info[] = + "SENTRY_VERSION:" SENTRY_SDK_VERSION ";" + "PLATFORM:@SENTRY_BUILD_PLATFORM@;" + "BUILD:@SENTRY_BUILD_ID@;" + "VARIANT:@SENTRY_BUILD_VARIANT@;" + "CONFIG:@CMAKE_BUILD_TYPE@;" + "@SENTRY_EMBED_INFO_ITEMS@" + "END"; \ No newline at end of file diff --git a/tests/test_embedded_info.py b/tests/test_embedded_info.py new file mode 100644 index 000000000..042532e79 --- /dev/null +++ b/tests/test_embedded_info.py @@ -0,0 +1,152 @@ +import os +import subprocess +import pytest +from . import run +from .conditions import has_http + + +def test_embedded_info_enabled(cmake): + """Test that version embedding works when enabled""" + cwd = cmake( + ["sentry_test_unit"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_EMBED_INFO": "ON", + "SENTRY_BUILD_PLATFORM": "test-platform", + "SENTRY_BUILD_VARIANT": "pytest", + "SENTRY_BUILD_ID": "test-build-123", + "SENTRY_EMBED_INFO_ITEMS": "TEST:python;FRAMEWORK:pytest;", + }, + ) + + # Run the embedded info tests + env = dict(os.environ) + + # Test basic functionality + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_basic"], + check=True, + env=env, + ) + + # Test format validation + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_format"], + check=True, + env=env, + ) + + # Test version matching + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_sentry_version"], + check=True, + env=env, + ) + + +def test_embedded_info_disabled(cmake): + """Test that version embedding is properly disabled when OFF""" + cwd = cmake( + ["sentry_test_unit"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_EMBED_INFO": "OFF", + }, + ) + + env = dict(os.environ) + + # Test that disabled test passes + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_disabled"], + check=True, + env=env, + ) + + # Basic test should skip when disabled + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_basic"], + check=True, + env=env, + ) + + +@pytest.mark.skipif(not has_http, reason="test needs http transport (curl)") +def test_embedded_info_binary_inspection(cmake): + """Test that embedded info appears in the actual binary""" + cwd = cmake( + ["sentry"], # Build the library itself + { + "SENTRY_EMBED_INFO": "ON", + "SENTRY_BUILD_PLATFORM": "binary-test", + "SENTRY_BUILD_VARIANT": "inspection", + "SENTRY_BUILD_ID": "bin-test-456", + }, + ) + + # Find the library file + library_file = None + for file in os.listdir(cwd): + if file.startswith("libsentry") and ( + file.endswith(".so") or file.endswith(".dylib") or file.endswith(".dll") + ): + library_file = os.path.join(cwd, file) + break + + if library_file is None: + pytest.skip("Could not find sentry library file") + + # Use strings command to check embedded content + try: + result = subprocess.run( + ["strings", library_file], capture_output=True, text=True, check=True + ) + output = result.stdout + + # Verify embedded information is present + assert "SENTRY_VERSION:" in output + assert "PLATFORM:binary-test" in output + assert "VARIANT:inspection" in output + assert "BUILD:bin-test-456" in output + assert "CONFIG:" in output + assert "END" in output + + except (subprocess.CalledProcessError, FileNotFoundError): + pytest.skip("strings command not available or failed") + + +def test_embedded_info_custom_items(cmake): + """Test that custom items are properly embedded""" + cwd = cmake( + ["sentry_test_unit"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + "SENTRY_EMBED_INFO": "ON", + "SENTRY_EMBED_INFO_ITEMS": "CUSTOM1:value1;CUSTOM2:value2;ENGINE:unreal;", + }, + ) + + # Build a simple test to check the embedded string content + env = dict(os.environ) + + # Run format test which will validate the custom items are present + run( + cwd, + "sentry_test_unit", + ["--no-summary", "embedded_info_format"], + check=True, + env=env, + ) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 624655e70..9add26e97 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -25,6 +25,7 @@ add_executable(sentry_test_unit test_basic.c test_consent.c test_concurrency.c + test_embedded_info.c test_envelopes.c test_failures.c test_fuzzfailures.c @@ -55,6 +56,11 @@ add_executable(sentry_test_unit # FIXME: cmake 3.13 introduced target_link_options target_compile_definitions(sentry_test_unit PRIVATE ${SENTRY_COMPILE_DEFINITIONS}) + +# Add SENTRY_EMBED_INFO define to tests if embedding is enabled +if(SENTRY_EMBED_INFO) + target_compile_definitions(sentry_test_unit PRIVATE SENTRY_EMBED_INFO=1) +endif() target_include_directories(sentry_test_unit PRIVATE ${SENTRY_INTERFACE_INCLUDE_DIRECTORIES} ${SENTRY_INCLUDE_DIRECTORIES} diff --git a/tests/unit/test_embedded_info.c b/tests/unit/test_embedded_info.c new file mode 100644 index 000000000..4c4e94c0c --- /dev/null +++ b/tests/unit/test_embedded_info.c @@ -0,0 +1,106 @@ +#include "sentry_testsupport.h" +#include +#include + +#ifdef SENTRY_EMBED_INFO +// The embedded info symbol is always compiled into the test executable itself, +// so we always use extern for the declaration +# ifdef _WIN32 +extern SENTRY_API const char sentry_library_info[]; +# else +extern const char sentry_library_info[]; +# endif +#endif + +SENTRY_TEST(embedded_info_basic) +{ +#ifdef SENTRY_EMBED_INFO + // Test that the embedded info string exists and has expected format + TEST_ASSERT(sentry_library_info != NULL); + TEST_CHECK(strlen(sentry_library_info) > 0); + + // Test that required fields are present + TEST_CHECK(strstr(sentry_library_info, "SENTRY_VERSION:") != NULL); + TEST_CHECK(strstr(sentry_library_info, "PLATFORM:") != NULL); + TEST_CHECK(strstr(sentry_library_info, "BUILD:") != NULL); + TEST_CHECK(strstr(sentry_library_info, "CONFIG:") != NULL); + TEST_CHECK(strstr(sentry_library_info, "END") != NULL); +#else + SKIP_TEST(); +#endif +} + +SENTRY_TEST(embedded_info_format) +{ +#ifdef SENTRY_EMBED_INFO + // Test that the string is properly semicolon-separated +# ifdef _WIN32 + char *info = _strdup(sentry_library_info); +# else + char *info = strdup(sentry_library_info); +# endif + TEST_ASSERT(info != NULL); + + int field_count = 0; + char *token = strtok(info, ";"); + while (token != NULL) { + // Each field should contain a colon (except END) + if (strcmp(token, "END") != 0) { + TEST_CHECK(strchr(token, ':') != NULL); + } + field_count++; + token = strtok(NULL, ";"); + } + + // Should have at least 5 fields: VERSION, PLATFORM, BUILD, VARIANT, CONFIG, + // END + TEST_CHECK(field_count >= 6); + + free(info); +#else + SKIP_TEST(); +#endif +} + +SENTRY_TEST(embedded_info_sentry_version) +{ +#ifdef SENTRY_EMBED_INFO + // Test that SENTRY_VERSION field contains the actual SDK version + const char *version_field = strstr(sentry_library_info, "SENTRY_VERSION:"); + TEST_ASSERT(version_field != NULL); + + // Extract the version value + const char *version_start = version_field + strlen("SENTRY_VERSION:"); + const char *version_end = strchr(version_start, ';'); + TEST_ASSERT(version_end != NULL); + + size_t version_len = version_end - version_start; + TEST_ASSERT(version_len > 0); + + // Use dynamic allocation or larger buffer + char *embedded_version = malloc(version_len + 1); + TEST_ASSERT(embedded_version != NULL); + strncpy(embedded_version, version_start, version_len); + embedded_version[version_len] = '\0'; + + // Version should contain at least one dot (e.g., "0.10.0") + TEST_CHECK(strchr(embedded_version, '.') != NULL); + + // Test that it matches the actual SDK version + TEST_CHECK_STRING_EQUAL(embedded_version, SENTRY_SDK_VERSION); + + free(embedded_version); +#else + SKIP_TEST(); +#endif +} + +SENTRY_TEST(embedded_info_disabled) +{ +#ifndef SENTRY_EMBED_INFO + // When SENTRY_EMBED_INFO is not defined, the feature is properly disabled + TEST_CHECK(1); // Always pass - confirms the feature is disabled +#else + SKIP_TEST(); // Skip when embedding is enabled +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 63c71eeb1..152bd293c 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -204,3 +204,7 @@ XX(value_unicode) XX(value_user) XX(value_wrong_type) XX(write_raw_envelope_to_file) +XX(embedded_info_basic) +XX(embedded_info_disabled) +XX(embedded_info_format) +XX(embedded_info_sentry_version) From 9307ab47d2e55fb76f5f1ca1edeb73cf4e4501da Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 11 Sep 2025 13:17:20 +0200 Subject: [PATCH 06/27] ci: specify `SDKROOT` on all macOS runners (#1367) --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55062ad23..1c8471eda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -245,6 +245,10 @@ jobs: if: ${{ runner.os == 'macOS' && matrix.os == 'macos-15-large' && matrix.RUN_ANALYZER == 'asan,llvm-cov' }} run: echo $(brew --prefix llvm@18)/bin >> $GITHUB_PATH + - name: Set macOS SDKROOT + if: ${{ runner.os == 'macOS' }} + run: echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "$GITHUB_ENV" + - name: Remove Strawberry Perl from PATH if: ${{ runner.os == 'Windows' }} shell: powershell From 63f3eeeac0159abab5bc14f77d2d99199df6f6f7 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 11 Sep 2025 14:09:19 +0200 Subject: [PATCH 07/27] docs: raise CMake 4 `SDKROOT` requirement in README (#1368) * docs: raise CMake 4 `SDKROOT` requirement in readme * Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ README.md | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bab372c..f7e57cb3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Support downstream Xbox SDK specifying networking initialization mechanism ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) +**Docs**: + +- Document the CMake 4 requirement on macOS `SDKROOT` due to its empty default for `CMAKE_OSX_SYSROOT` in the `README`. ([#1368](https://github.com/getsentry/sentry-native/pull/1368)) + ## 0.10.1 **Internal:** diff --git a/README.md b/README.md index a0e88b734..9858a50b1 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ $ ninja -C build **MacOS**: Building universal binaries/libraries is possible out of the box when using the -[`CMAKE_OSX_ARCHITECTURES`](https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_ARCHITECTURES.html) define, both with the `Xcode` generator as well +[`CMAKE_OSX_ARCHITECTURES`](https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_ARCHITECTURES.html) variable, both with the `Xcode` generator as well as the default generator: ```sh @@ -179,13 +179,17 @@ $ lipo -info defaultbuild/libsentry.dylib Architectures in the fat file: defaultbuild/libsentry.dylib are: x86_64 arm64 ``` -Make sure that MacOSX SDK 11 or later is used. It is possible that this requires -manually overriding the `SDKROOT`: +Ensure that macOS SDK 11 or later is used (we currently only test against versions 13 and above). This may require +specifying the `SDKROOT`: ```sh $ export SDKROOT=$(xcrun --sdk macosx --show-sdk-path) ``` +If you build on macOS using _CMake 4_, then you _must_ specify the `SDKROOT`, because +[CMake 4 defaults to an empty `CMAKE_OSX_SYSROOT`](https://cmake.org/cmake/help/latest/variable/CMAKE_OSX_SYSROOT.html), +which could lead to inconsistent include paths when CMake tries to gather the `sysroot` later in the build. + ### Compile-Time Options The following options can be set when running the cmake generator, for example From 89e25d48b5eabbbda65846bd9165caa02841642d Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:51:48 +0200 Subject: [PATCH 08/27] feat: update `traces_sampler` to also take `user_data` argument (#1346) * update `traces_sampler` to also take `user_data` argument * update CHANGELOG.md * actually check user_data * remove unnecessary allocation --- CHANGELOG.md | 6 +++++- examples/example.c | 8 ++++++-- include/sentry.h | 9 ++++++--- src/sentry_core.c | 3 ++- src/sentry_options.c | 5 +++-- src/sentry_options.h | 1 + tests/unit/test_sampling.c | 8 ++++++-- 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e57cb3a..031b4b3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ ## Unreleased +**Breaking changes**: + +- Add `user_data` parameter to `traces_sampler`. ([#1346](https://github.com/getsentry/sentry-native/pull/1346)) + **Internal:** -- Support downstream Xbox SDK specifying networking initialization mechanism ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) +- Support downstream Xbox SDK specifying networking initialization mechanism. ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) **Docs**: diff --git a/examples/example.c b/examples/example.c index bb4f1cd5a..6515a54c0 100644 --- a/examples/example.c +++ b/examples/example.c @@ -60,8 +60,11 @@ get_current_thread_id() static double traces_sampler_callback(const sentry_transaction_context_t *transaction_ctx, - sentry_value_t custom_sampling_ctx, const int *parent_sampled) + sentry_value_t custom_sampling_ctx, const int *parent_sampled, + void *user_data) { + (void)user_data; + if (parent_sampled != NULL) { if (*parent_sampled) { return 0.8; // high sample rate for children of sampled transactions @@ -346,7 +349,8 @@ main(int argc, char **argv) } if (has_arg(argc, argv, "traces-sampler")) { - sentry_options_set_traces_sampler(options, traces_sampler_callback); + sentry_options_set_traces_sampler( + options, traces_sampler_callback, NULL); } if (has_arg(argc, argv, "override-sdk-name")) { diff --git a/include/sentry.h b/include/sentry.h index 9a7ada89e..ca4202b45 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1867,15 +1867,18 @@ struct sentry_transaction_context_s; typedef struct sentry_transaction_context_s sentry_transaction_context_t; typedef double (*sentry_traces_sampler_function)( const sentry_transaction_context_t *transaction_ctx, - sentry_value_t custom_sampling_ctx, const int *parent_sampled); + sentry_value_t custom_sampling_ctx, const int *parent_sampled, + void *user_data); /** * Sets the traces sampler callback. Should be a function that returns a double * and takes in a sentry_transaction_context_t pointer, a sentry_value_t for - * a custom sampling context and an int pointer for the parent sampled flag. + * a custom sampling context int pointer for the parent sampled flag and some + * optional user_data. */ SENTRY_EXPERIMENTAL_API void sentry_options_set_traces_sampler( - sentry_options_t *opts, sentry_traces_sampler_function callback); + sentry_options_t *opts, sentry_traces_sampler_function callback, + void *user_data); #ifdef SENTRY_PLATFORM_LINUX diff --git a/src/sentry_core.c b/src/sentry_core.c index e671f3129..7d99ef13a 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -586,7 +586,8 @@ static sampling_ctx->transaction_context, sampling_ctx->custom_sampling_context, sampling_ctx->parent_sampled == NULL ? NULL - : &parent_sampled_int); + : &parent_sampled_int, + options->traces_sampler_data); send = sampling_ctx->sample_rand < result; } else { if (sampling_ctx->parent_sampled != NULL) { diff --git a/src/sentry_options.c b/src/sentry_options.c index 23eaef2b4..3fa26a770 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -661,10 +661,11 @@ sentry_options_get_traces_sample_rate(sentry_options_t *opts) } void -sentry_options_set_traces_sampler( - sentry_options_t *opts, sentry_traces_sampler_function callback) +sentry_options_set_traces_sampler(sentry_options_t *opts, + sentry_traces_sampler_function callback, void *user_data) { opts->traces_sampler = callback; + opts->traces_sampler_data = user_data; } void diff --git a/src/sentry_options.h b/src/sentry_options.h index 5316b69e5..4c1c63686 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -56,6 +56,7 @@ struct sentry_options_s { /* Experimentally exposed */ double traces_sample_rate; sentry_traces_sampler_function traces_sampler; + void *traces_sampler_data; size_t max_spans; /* everything from here on down are options which are stored here but diff --git a/tests/unit/test_sampling.c b/tests/unit/test_sampling.c index b99d3e02d..8d8c45622 100644 --- a/tests/unit/test_sampling.c +++ b/tests/unit/test_sampling.c @@ -12,8 +12,11 @@ SENTRY_TEST(sampling_decision) static double traces_sampler_callback(const sentry_transaction_context_t *transaction_ctx, - sentry_value_t custom_sampling_ctx, const int *parent_sampled) + sentry_value_t custom_sampling_ctx, const int *parent_sampled, + void *user_data) { + TEST_CHECK(user_data == (void *)0x12345678); + const char *name = sentry_transaction_context_get_name(transaction_ctx); const char *operation = sentry_transaction_context_get_operation(transaction_ctx); @@ -91,7 +94,8 @@ SENTRY_TEST(sampling_transaction) { // test the traces_sampler callback SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_traces_sampler(options, traces_sampler_callback); + sentry_options_set_traces_sampler( + options, traces_sampler_callback, (void *)0x12345678); sentry_options_set_traces_sample_rate(options, 1.0); TEST_CHECK(sentry_init(options) == 0); From 97a4a99dfdd59f8e032a4d27dfeb3836ae61d950 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 11 Sep 2025 16:02:26 +0200 Subject: [PATCH 09/27] ci: replace deprecated functions in tests and acutest (#1369) * replace sprintf with snprintf in acutest.h * replace vsprintf with vsnprintf in test_logger.c * replace sprintf with snprintf in test_value.c --- tests/unit/test_logger.c | 2 +- tests/unit/test_value.c | 6 +++--- vendor/acutest.h | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_logger.c b/tests/unit/test_logger.c index 89e50553e..d748f2d1f 100644 --- a/tests/unit/test_logger.c +++ b/tests/unit/test_logger.c @@ -18,7 +18,7 @@ test_logger( TEST_CHECK(level == SENTRY_LEVEL_WARNING); char formatted[128]; - vsprintf(formatted, message, args); + vsnprintf(formatted, sizeof(formatted), message, args); TEST_CHECK_STRING_EQUAL(formatted, "Oh this is bad"); } diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 0ab2bde1e..d735df241 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -288,12 +288,12 @@ SENTRY_TEST(value_object) sentry_value_t val = sentry_value_new_object(); for (size_t i = 0; i < 10; i++) { char key[100]; - sprintf(key, "key%d", (int)i); + snprintf(key, sizeof(key), "key%d", (int)i); sentry_value_set_by_key(val, key, sentry_value_new_int32((int32_t)i)); } for (size_t i = 0; i < 20; i++) { char key[100]; - sprintf(key, "key%d", (int)i); + snprintf(key, sizeof(key), "key%d", (int)i); sentry_value_t child = sentry_value_get_by_key(val, key); if (i < 10) { TEST_CHECK(sentry_value_as_int32(child) == (int32_t)i); @@ -316,7 +316,7 @@ SENTRY_TEST(value_object) for (size_t i = 0; i < 10; i += 2) { char key[100]; - sprintf(key, "key%d", (int)i); + snprintf(key, sizeof(key), "key%d", (int)i); sentry_value_remove_by_key(val, key); } diff --git a/vendor/acutest.h b/vendor/acutest.h index 18bf44339..c3b296fa9 100644 --- a/vendor/acutest.h +++ b/vendor/acutest.h @@ -1059,7 +1059,7 @@ test_run__(const struct test__* test, int index, int master_index) case SIGSEGV: signame = "SIGSEGV"; break; case SIGILL: signame = "SIGILL"; break; case SIGTERM: signame = "SIGTERM"; break; - default: sprintf(tmp, "signal %d", WTERMSIG(exit_code)); signame = tmp; break; + default: snprintf(tmp, sizeof(tmp), "signal %d", WTERMSIG(exit_code)); signame = tmp; break; } test_error__("Test interrupted by %s.", signame); } else { @@ -1234,7 +1234,7 @@ test_cmdline_read__(const TEST_CMDLINE_OPTION__* options, int argc, char** argv, if(opt->flags & (TEST_CMDLINE_OPTFLAG_OPTIONALARG__ | TEST_CMDLINE_OPTFLAG_REQUIREDARG__)) { ret = callback(opt->id, argv[i]+2+len+1); } else { - sprintf(auxbuf, "--%s", opt->longname); + snprintf(auxbuf, sizeof(auxbuf), "--%s", opt->longname); ret = callback(TEST_CMDLINE_OPTID_BOGUSARG__, auxbuf); } break; From c2adc7e6dc2c5f9aff6dcbbd465f79230d9b8d17 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:50:44 +0200 Subject: [PATCH 10/27] fix: resolve 'void function returning a value' compilation warnings (#1372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: resolve 'void function returning a value' compilation warnings - Change SKIP_TEST() macro from `(void)0` to `return` - Fix incorrect usage of `return SKIP_TEST()` patterns in test files - Clean up conditional compilation blocks to prevent unreachable code - Remove unused test entry from tests.inc 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Apply suggestion from @vaind * fix: restore conditional compilation blocks in test files Fix build failures on Windows by properly restoring #if/#else/#endif blocks that were incorrectly restructured. Test code should only run on supported platforms, not on all platforms after SKIP_TEST(). 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- tests/unit/sentry_testsupport.h | 2 +- tests/unit/test_modulefinder.c | 3 ++- tests/unit/test_path.c | 3 ++- tests/unit/test_symbolizer.c | 3 ++- tests/unit/test_unwinder.c | 3 ++- tests/unit/test_value.c | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/unit/sentry_testsupport.h b/tests/unit/sentry_testsupport.h index d7360541c..f3deb69d8 100644 --- a/tests/unit/sentry_testsupport.h +++ b/tests/unit/sentry_testsupport.h @@ -17,7 +17,7 @@ #define CONCAT(A, B) A##B #define SENTRY_TEST(Name) void CONCAT(test_sentry_, Name)(void) -#define SKIP_TEST() (void)0 +#define SKIP_TEST() return #define TEST_CHECK_STRING_EQUAL(Val, ReferenceVal) \ do { \ diff --git a/tests/unit/test_modulefinder.c b/tests/unit/test_modulefinder.c index f244e2f50..84551bdb5 100644 --- a/tests/unit/test_modulefinder.c +++ b/tests/unit/test_modulefinder.c @@ -8,8 +8,9 @@ SENTRY_TEST(module_finder) { #if defined(SENTRY_PLATFORM_NX) - return SKIP_TEST(); + SKIP_TEST(); #endif + // make sure that we are able to do multiple cleanup cycles sentry_value_decref(sentry_get_modules_list()); sentry_clear_modulecache(); diff --git a/tests/unit/test_path.c b/tests/unit/test_path.c index 938d6c53b..daa565d61 100644 --- a/tests/unit/test_path.c +++ b/tests/unit/test_path.c @@ -252,8 +252,9 @@ SENTRY_TEST(path_directory) SENTRY_TEST(path_mtime) { #if defined(SENTRY_PLATFORM_NX) - return SKIP_TEST(); + SKIP_TEST(); #endif + sentry_path_t *path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX "foo.txt"); TEST_ASSERT(!!path); diff --git a/tests/unit/test_symbolizer.c b/tests/unit/test_symbolizer.c index 4e04a2393..436eb4c46 100644 --- a/tests/unit/test_symbolizer.c +++ b/tests/unit/test_symbolizer.c @@ -31,8 +31,9 @@ asserter(const sentry_frame_info_t *info, void *data) SENTRY_TEST(symbolizer) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_XBOX) - return SKIP_TEST(); + SKIP_TEST(); #endif + int called = 0; #ifdef SENTRY_PLATFORM_AIX sentry__symbolize( diff --git a/tests/unit/test_unwinder.c b/tests/unit/test_unwinder.c index d99179dc9..70568b94d 100644 --- a/tests/unit/test_unwinder.c +++ b/tests/unit/test_unwinder.c @@ -39,8 +39,9 @@ find_frame(const sentry_frame_info_t *info, void *data) SENTRY_TEST(unwinder) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_XBOX) - return SKIP_TEST(); + SKIP_TEST(); #endif + void *backtrace1[MAX_FRAMES] = { 0 }; size_t frame_count1 = invoke_unwinder(backtrace1); size_t invoker_frame = 0; diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index d735df241..b06324c06 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -814,8 +814,9 @@ SENTRY_TEST(value_get_by_null_key) SENTRY_TEST(value_set_stacktrace) { #if defined(SENTRY_PLATFORM_NX) - return SKIP_TEST(); + SKIP_TEST(); #endif + sentry_value_t exc = sentry_value_new_exception("std::out_of_range", "vector"); sentry_value_set_stacktrace(exc, NULL, 0); From 311c6ce03812fd04790b826bcccefdc12a1922bc Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Tue, 16 Sep 2025 11:55:48 +0200 Subject: [PATCH 11/27] ci: bump kcov (#1376) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c8471eda..27d105be2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,7 +202,7 @@ jobs: git clone https://github.com/SimonKagstrom/kcov.git cd kcov # pin to a known good version with the coveralls git integration and performance bottlenecks fixed - git checkout 8afe9f29c58ef575877664c7ba209328233b70cc + git checkout b370df05ccc96facfd83a9e101e35263457b8035 mkdir build cd build cmake .. From 48ae2ec8bdc691f311efe1c3f5c8d77ee35a5f25 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 16 Sep 2025 21:26:55 +0200 Subject: [PATCH 12/27] feat: Implement logging enable/disable feature, with option to disable logging in handlers (#1371) * Implemented logging enable/disable feature * Added a new option to enable/disable handling while handling crashes * Extended all backends to support new feature * Extended unit tests * Added logger integration tests --------- Co-authored-by: Mischan Toosarani-Hausberger --- CHANGELOG.md | 4 + CONTRIBUTING.md | 4 + examples/example.c | 37 +++++ include/sentry.h | 9 ++ src/backends/sentry_backend_breakpad.cpp | 8 ++ src/backends/sentry_backend_crashpad.cpp | 9 ++ src/backends/sentry_backend_inproc.c | 8 ++ src/sentry_logger.c | 17 +++ src/sentry_logger.h | 3 + src/sentry_options.c | 7 + src/sentry_options.h | 1 + tests/test_integration_logger.py | 168 +++++++++++++++++++++++ tests/unit/test_logger.c | 38 +++++ tests/unit/test_options.c | 22 +++ tests/unit/tests.inc | 2 + 15 files changed, 337 insertions(+) create mode 100644 tests/test_integration_logger.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 031b4b3e5..7799e655c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add `user_data` parameter to `traces_sampler`. ([#1346](https://github.com/getsentry/sentry-native/pull/1346)) +**Features:** + +- Add a configuration to disable logging after a crash has been detected - `sentry_options_set_logger_enabled_when_crashed()`. ([#1371](https://github.com/getsentry/sentry-native/pull/1371)) + **Internal:** - Support downstream Xbox SDK specifying networking initialization mechanism. ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f09fb6276..d70145828 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -190,6 +190,10 @@ The example currently supports the following commands: - `attach-to-scope`: Same as `attachment` but attaches the file to the local scope. - `clear-attachments`: Clears all attachments from the global scope. - `capture-user-feedback`: Captures a user feedback event. +- `test-logger`: Sets up a test logger for integration tests that outputs in a format the integration tests can parse. +- `disable-logger-when-crashed`: Disables logging during crash handling. +- `enable-logger-when-crashed`: Explicitly enables logging during crash handling (default behavior). +- `test-logger-before-crash`: Outputs marker directly using printf for test parsing before crash. Only on Linux using crashpad: - `crashpad-wait-for-upload`: Couples application shutdown to complete the upload in the `crashpad_handler`. diff --git a/examples/example.c b/examples/example.c index 6515a54c0..71ba67af7 100644 --- a/examples/example.c +++ b/examples/example.c @@ -158,6 +158,22 @@ discarding_before_transaction_callback(sentry_value_t tx, void *user_data) return tx; } +// Test logger that outputs in a format the integration tests can parse +static void +test_logger_callback( + sentry_level_t level, const char *message, va_list args, void *userdata) +{ + (void)level; + (void)userdata; + + char formatted_message[1024]; + vsnprintf(formatted_message, sizeof(formatted_message), message, args); + + // Output in a format that the Python tests can detect + printf("SENTRY_LOG:%s\n", formatted_message); + fflush(stdout); +} + static void print_envelope(sentry_envelope_t *envelope, void *unused_state) { @@ -385,6 +401,21 @@ main(int argc, char **argv) sentry_options_add_view_hierarchy(options, "./view-hierarchy.json"); } + if (has_arg(argc, argv, "test-logger")) { + // Set up the test logger for integration tests + sentry_options_set_logger(options, test_logger_callback, NULL); + } + + if (has_arg(argc, argv, "disable-logger-when-crashed")) { + // Disable logging during crash handling + sentry_options_set_logger_enabled_when_crashed(options, 0); + } + + if (has_arg(argc, argv, "enable-logger-when-crashed")) { + // Explicitly enable logging during crash handling (default behavior) + sentry_options_set_logger_enabled_when_crashed(options, 1); + } + sentry_init(options); if (has_arg(argc, argv, "attachment")) { @@ -507,6 +538,12 @@ main(int argc, char **argv) sleep_s(10); } + if (has_arg(argc, argv, "test-logger-before-crash")) { + // Output marker directly using printf for test parsing + printf("pre-crash-log-message\n"); + fflush(stdout); + } + if (has_arg(argc, argv, "crash")) { trigger_crash(); } diff --git a/include/sentry.h b/include/sentry.h index ca4202b45..635dcf00d 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1275,6 +1275,15 @@ typedef void (*sentry_logger_function_t)( SENTRY_API void sentry_options_set_logger( sentry_options_t *opts, sentry_logger_function_t func, void *userdata); +/** + * Enables or disables console logging after crash. + * When disabled, Sentry will not invoke logger callbacks after crash + * has been detected. This can be useful to avoid potential issues during + * crash handling that logging might cause. This is enabled by default. + */ +SENTRY_API void sentry_options_set_logger_enabled_when_crashed( + sentry_options_t *opts, int enabled); + /** * Enables or disables automatic session tracking. * diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 89be05165..11f60e24b 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -7,6 +7,7 @@ extern "C" { #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_logger.h" #include "sentry_options.h" #ifdef SENTRY_PLATFORM_WINDOWS # include "sentry_os.h" @@ -74,6 +75,13 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, void *UNUSED(context), bool succeeded) #endif { + // Disable logging during crash handling if the option is set + SENTRY_WITH_OPTIONS (options) { + if (!options->enable_logging_when_crashed) { + sentry__logger_disable(); + } + } + SENTRY_INFO("entering breakpad minidump callback"); // this is a bit strange, according to docs, `succeeded` should be true when diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index c3552d475..2c6de727b 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -7,6 +7,7 @@ extern "C" { #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_logger.h" #include "sentry_options.h" #ifdef SENTRY_PLATFORM_WINDOWS # include "sentry_os.h" @@ -280,6 +281,13 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) { sentry__page_allocator_enable(); # endif + // Disable logging during crash handling if the option is set + SENTRY_WITH_OPTIONS (options) { + if (!options->enable_logging_when_crashed) { + sentry__logger_disable(); + } + } + SENTRY_INFO("flushing session and queue before crashpad handler"); bool should_dump = true; @@ -336,6 +344,7 @@ sentry__crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) } SENTRY_INFO("handing control over to crashpad"); + // If we __don't__ want a minidump produced by crashpad we need to either // exit or longjmp at this point. The crashpad client handler which calls // back here (SetFirstChanceExceptionHandler) does the same if the diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index c2ee95fa0..4ed124990 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -6,6 +6,7 @@ #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_logger.h" #include "sentry_options.h" #if defined(SENTRY_PLATFORM_WINDOWS) # include "sentry_os.h" @@ -523,6 +524,13 @@ make_signal_event( static void handle_ucontext(const sentry_ucontext_t *uctx) { + // Disable logging during crash handling if the option is set + SENTRY_WITH_OPTIONS (options) { + if (!options->enable_logging_when_crashed) { + sentry__logger_disable(); + } + } + SENTRY_INFO("entering signal handler"); const struct signal_slot *sig_slot = NULL; diff --git a/src/sentry_logger.c b/src/sentry_logger.c index 5051dab73..94c67f759 100644 --- a/src/sentry_logger.c +++ b/src/sentry_logger.c @@ -1,11 +1,13 @@ #include "sentry_logger.h" #include "sentry_core.h" #include "sentry_string.h" +#include "sentry_sync.h" #include #include static sentry_logger_t g_logger = { NULL, NULL, SENTRY_LEVEL_DEBUG }; +static volatile long g_logger_enabled = 1; void sentry__logger_set_global(sentry_logger_t logger) @@ -89,6 +91,9 @@ sentry__logger_describe(sentry_level_t level) void sentry__logger_log(sentry_level_t level, const char *message, ...) { + if (!sentry__atomic_fetch(&g_logger_enabled)) { + return; + } if (g_logger.logger_level != SENTRY_LEVEL_DEBUG && level < g_logger.logger_level) { return; @@ -101,3 +106,15 @@ sentry__logger_log(sentry_level_t level, const char *message, ...) va_end(args); } } + +void +sentry__logger_enable(void) +{ + sentry__atomic_store(&g_logger_enabled, 1); +} + +void +sentry__logger_disable(void) +{ + sentry__atomic_store(&g_logger_enabled, 0); +} diff --git a/src/sentry_logger.h b/src/sentry_logger.h index af21b016d..0f5e25034 100644 --- a/src/sentry_logger.h +++ b/src/sentry_logger.h @@ -18,6 +18,9 @@ const char *sentry__logger_describe(sentry_level_t level); void sentry__logger_log(sentry_level_t level, const char *message, ...); +void sentry__logger_enable(void); +void sentry__logger_disable(void); + #define SENTRY_DEBUGF(message, ...) \ sentry__logger_log(SENTRY_LEVEL_DEBUG, message, __VA_ARGS__) diff --git a/src/sentry_options.c b/src/sentry_options.c index 3fa26a770..0b1622bf7 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -50,6 +50,7 @@ sentry_options_new(void) opts->system_crash_reporter_enabled = false; opts->attach_screenshot = false; opts->crashpad_wait_for_upload = false; + opts->enable_logging_when_crashed = true; opts->symbolize_stacktraces = // AIX doesn't have reliable debug IDs for server-side symbolication, // and the diversity of Android makes it infeasible to have access to debug @@ -421,6 +422,12 @@ sentry_options_set_logger_level(sentry_options_t *opts, sentry_level_t level) opts->logger.logger_level = level; } +void +sentry_options_set_logger_enabled_when_crashed(sentry_options_t *opts, int val) +{ + opts->enable_logging_when_crashed = !!val; +} + void sentry_options_set_auto_session_tracking(sentry_options_t *opts, int val) { diff --git a/src/sentry_options.h b/src/sentry_options.h index 4c1c63686..6e61721d2 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -41,6 +41,7 @@ struct sentry_options_s { bool system_crash_reporter_enabled; bool attach_screenshot; bool crashpad_wait_for_upload; + bool enable_logging_when_crashed; sentry_attachment_t *attachments; sentry_run_t *run; diff --git a/tests/test_integration_logger.py b/tests/test_integration_logger.py new file mode 100644 index 000000000..a1b02d579 --- /dev/null +++ b/tests/test_integration_logger.py @@ -0,0 +1,168 @@ +import shutil +import subprocess +import sys + +import pytest + +import os + +from . import run +from .conditions import has_breakpad, has_crashpad, is_android + + +def _run_logger_crash_test(backend, cmake, logger_option): + """Helper function to run logger crash tests with the specified logger option. + + Args: + backend: The sentry backend to use (inproc, breakpad, crashpad) + cmake: The cmake fixture + logger_option: Either 'enable-logger-when-crashed' or 'disable-logger-when-crashed' + + Returns: + tuple: (output, parsed_data) where output is the raw subprocess output + and parsed_data is the parsed logging data + """ + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) + + # Make sure we are isolated from previous runs + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) + + # Run the example with the specified logger option - expect it to crash + child = run( + tmp_path, + "sentry_example", + [ + logger_option, # enable or disable logging during crash + "log", # Enable debug logging + "test-logger", # Use our custom test logger + "test-logger-before-crash", # Log before crash + "crash", # Trigger crash + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + # Process should have crashed (non-zero exit code) + assert ( + child.returncode + ), f"Expected crash but process completed normally. Output: {child.stdout.decode('utf-8', errors='replace')}" + + output = child.stdout.decode("utf-8", errors="replace") + + # Parse the output to check logging behavior + parsed_data = parse_logger_output(output) + + return parsed_data + + +def parse_logger_output(output): + lines = output.split("\n") + + parsed_data = { + "pre_crash_log_completed": False, + "sentry_logs": [], + "logs_after_pre_crash": [], + } + + pre_crash_completed = False + + for line in lines: + if line.strip() == "pre-crash-log-message": + parsed_data["pre_crash_log_completed"] = True + pre_crash_completed = True + elif line.startswith("SENTRY_LOG:"): + log_message = line[len("SENTRY_LOG:") :].strip() + parsed_data["sentry_logs"].append(log_message) + + # Track logs that occur after the pre-crash marker + if pre_crash_completed: + parsed_data["logs_after_pre_crash"].append(log_message) + + return parsed_data + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param( + "inproc", + marks=pytest.mark.skipif( + bool(is_android), + reason="skip inproc tests on Android", + ), + ), + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + pytest.param( + "crashpad", + marks=[ + pytest.mark.skipif( + not has_crashpad, reason="crashpad backend not available" + ), + pytest.mark.skipif( + sys.platform == "darwin", + reason="crashpad has no client handler on macOS", + ), + ], + ), + ], +) +def test_logger_enabled_when_crashed(backend, cmake): + """Test that logging works during crash handling when enabled (default behavior).""" + data = _run_logger_crash_test(backend, cmake, "enable-logger-when-crashed") + + # Verify that pre-crash logging worked + assert data["pre_crash_log_completed"], "Pre-crash log marker should be present" + assert len(data["sentry_logs"]) > 0, "Should have some SENTRY_LOG messages" + + # When logging is enabled, we should see logs after the pre-crash marker + # Only check this on Linux, as other platforms don't reliably log during crash + if sys.platform == "linux" and backend != "crashpad": + assert ( + len(data["logs_after_pre_crash"]) > 0 + ), "Should have SENTRY_LOG messages after crash when logging is enabled" + + +@pytest.mark.parametrize( + "backend", + [ + pytest.param( + "inproc", + marks=pytest.mark.skipif( + bool(is_android), + reason="skip inproc tests on Android", + ), + ), + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + pytest.param( + "crashpad", + marks=pytest.mark.skipif( + not has_crashpad, reason="crashpad backend not available" + ), + ), + ], +) +def test_logger_disabled_when_crashed(backend, cmake): + """Test that logging is disabled during crash handling when the option is set.""" + data = _run_logger_crash_test(backend, cmake, "disable-logger-when-crashed") + + # Verify that pre-crash logging worked + assert data["pre_crash_log_completed"], "Pre-crash log marker should be present" + assert len(data["sentry_logs"]) > 0, "Should have some SENTRY_LOG messages" + + # When logging is disabled, we should NOT see any logs after the pre-crash marker + # The last log should be from before the crash + assert ( + len(data["logs_after_pre_crash"]) == 0 + ), f"Should have NO SENTRY_LOG messages after crash when logging is disabled, but got: {data['logs_after_pre_crash']}" diff --git a/tests/unit/test_logger.c b/tests/unit/test_logger.c index d748f2d1f..e28b1f7c6 100644 --- a/tests/unit/test_logger.c +++ b/tests/unit/test_logger.c @@ -1,5 +1,6 @@ #include "sentry_core.h" #include "sentry_logger.h" +#include "sentry_sync.h" #include "sentry_testsupport.h" typedef struct { @@ -51,3 +52,40 @@ SENTRY_TEST(custom_logger) sentry_close(); } } + +SENTRY_TEST(logger_enable_disable_functionality) +{ + logger_test_t data = { 0, false }; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_debug(options, true); + sentry_options_set_logger(options, test_logger, &data); + + sentry_init(options); + + // Test logging is enabled by default + data.called = 0; + data.assert_now = true; + SENTRY_WARNF("Oh this is %s", "bad"); + TEST_CHECK_INT_EQUAL(data.called, 1); + + // Test disabling logging + sentry__logger_disable(); + data.called = 0; + data.assert_now = false; + SENTRY_WARNF("Don't log %s", "this"); + TEST_CHECK_INT_EQUAL(data.called, 0); + + // Test re-enabling logging + sentry__logger_enable(); + data.called = 0; + data.assert_now = true; + SENTRY_WARNF("Oh this is %s", "bad"); + TEST_CHECK_INT_EQUAL(data.called, 1); + data.assert_now = false; + + // Clear the logger instance + SENTRY_TEST_OPTIONS_NEW(clean_options); + sentry_init(clean_options); + sentry_close(); +} diff --git a/tests/unit/test_options.c b/tests/unit/test_options.c index 539e404cb..21d5e5ea6 100644 --- a/tests/unit/test_options.c +++ b/tests/unit/test_options.c @@ -52,3 +52,25 @@ SENTRY_TEST(options_sdk_name_invalid) sentry_options_free(options); } + +SENTRY_TEST(options_logger_enabled_when_crashed_default) +{ + SENTRY_TEST_OPTIONS_NEW(options); + + // Enabled by default + TEST_CHECK_INT_EQUAL(options->enable_logging_when_crashed, 1); + + // Test setting to false + sentry_options_set_logger_enabled_when_crashed(options, 0); + TEST_CHECK_INT_EQUAL(options->enable_logging_when_crashed, 0); + + // Test setting to true + sentry_options_set_logger_enabled_when_crashed(options, 1); + TEST_CHECK_INT_EQUAL(options->enable_logging_when_crashed, 1); + + // Test setting with non-zero value (should be converted to 1) + sentry_options_set_logger_enabled_when_crashed(options, 42); + TEST_CHECK_INT_EQUAL(options->enable_logging_when_crashed, 1); + + sentry_options_free(options); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 152bd293c..a957f36a7 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -37,6 +37,7 @@ XX(count_sampled_events) XX(crash_marker) XX(crashed_last_run) XX(custom_logger) +XX(logger_enable_disable_functionality) XX(deserialize_envelope) XX(deserialize_envelope_empty) XX(deserialize_envelope_empty_attachments) @@ -87,6 +88,7 @@ XX(multiple_transactions) XX(options_sdk_name_custom) XX(options_sdk_name_defaults) XX(options_sdk_name_invalid) +XX(options_logger_enabled_when_crashed_default) XX(os) XX(os_release_non_existent_files) XX(os_releases_snapshot) From f4f7379551ab50e51ccb359550f936e2bd7aba98 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 17 Sep 2025 16:20:20 +0200 Subject: [PATCH 13/27] fix: TOCTOU race between session life-cycle and event capture (#1377) * fix: TOCTOU race between session life-cycle and event capture * Apply suggestion from @mujacica Co-authored-by: Amir Mujacic --------- Co-authored-by: Amir Mujacic --- CHANGELOG.md | 14 +++++++++----- src/sentry_core.c | 29 ++++++++++++++++++++++------- src/sentry_tsan.h | 29 +++++++++++++++++++++++++++++ tests/unit/tests.inc | 12 ++++++------ 4 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/sentry_tsan.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 7799e655c..5e6657626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,15 @@ - Add `user_data` parameter to `traces_sampler`. ([#1346](https://github.com/getsentry/sentry-native/pull/1346)) -**Features:** +**Fixes**: + +- Fixed a TOCTOU race between session init/shutdown and event capture. ([#1377](https://github.com/getsentry/sentry-native/pull/1377)) + +**Features**: - Add a configuration to disable logging after a crash has been detected - `sentry_options_set_logger_enabled_when_crashed()`. ([#1371](https://github.com/getsentry/sentry-native/pull/1371)) -**Internal:** +**Internal**: - Support downstream Xbox SDK specifying networking initialization mechanism. ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) @@ -20,9 +24,9 @@ ## 0.10.1 -**Internal:** +**Internal**: -- Correctly apply dynamic mutex initialization in unit-tests (fixes running unit-tests in downstream console SDKs) ([#1337](https://github.com/getsentry/sentry-native/pull/1337)) +- Correctly apply dynamic mutex initialization in unit-tests (fixes running unit-tests in downstream console SDKs). ([#1337](https://github.com/getsentry/sentry-native/pull/1337)) ## 0.10.0 @@ -47,7 +51,7 @@ - Marked deprecated functions with `SENTRY_DEPRECATED(msg)`. ([#1308](https://github.com/getsentry/sentry-native/pull/1308)) -**Internal:** +**Internal**: - Crash events from Crashpad now have `event_id` defined similarly to other backends. This makes it possible to associate feedback at the time of crash. ([#1319](https://github.com/getsentry/sentry-native/pull/1319)) diff --git a/src/sentry_core.c b/src/sentry_core.c index 7d99ef13a..dd920f486 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -17,6 +17,7 @@ #include "sentry_sync.h" #include "sentry_tracing.h" #include "sentry_transport.h" +#include "sentry_tsan.h" #include "sentry_value.h" #ifdef SENTRY_PLATFORM_WINDOWS @@ -534,14 +535,28 @@ sentry__capture_event(sentry_value_t event, sentry_scope_t *local_scope) options, event, &event_id, true, local_scope); } if (envelope) { - if (options->session) { + // Accept a racy read here, since SENTRY_WITH_OPTIONS only prevents + // the options from being deallocated while we use them, but no lock + // is active and session could change during session shutdown. + // We recheck below inside the lock and don't pay for a lock here. + // This also means we accept a missed window of opportunity if an + // event is being sent concurrently to session initialization, which + // is an acceptable design trade-off. + SENTRY_TSAN_IGNORE_READS_BEGIN(); + bool has_session = options->session; + SENTRY_TSAN_IGNORE_READS_END(); + if (has_session) { sentry_options_t *mut_options = sentry__options_lock(); - sentry__envelope_add_session(envelope, mut_options->session); - // we're assuming that if a session is added to an envelope - // it will be sent onwards. This means we now need to set - // the init flag to false because we're no longer the - // initial session update. - mut_options->session->init = false; + // recheck inside the lock since our previous read is racy. + if (mut_options->session) { + sentry__envelope_add_session( + envelope, mut_options->session); + // we're assuming that if a session is added to an envelope + // it will be sent onwards. This means we now need to set + // the init flag to false because we're no longer in the + // initial session update. + mut_options->session->init = false; + } sentry__options_unlock(); } diff --git a/src/sentry_tsan.h b/src/sentry_tsan.h new file mode 100644 index 000000000..530b61afd --- /dev/null +++ b/src/sentry_tsan.h @@ -0,0 +1,29 @@ +#ifndef SENTRY_TSAN_H_INCLUDED +#define SENTRY_TSAN_H_INCLUDED + +// Provide safe access to the thread sanitizer interface + +#ifdef __has_feature +# if __has_feature(thread_sanitizer) +# ifdef __cplusplus +extern "C" { +# endif +void AnnotateIgnoreReadsBegin(const char *file, int line); +void AnnotateIgnoreReadsEnd(const char *file, int line); +# ifdef __cplusplus +} +# endif +# define SENTRY_TSAN_IGNORE_READS_BEGIN() \ + AnnotateIgnoreReadsBegin(__FILE__, __LINE__) +# define SENTRY_TSAN_IGNORE_READS_END() \ + AnnotateIgnoreReadsEnd(__FILE__, __LINE__) +# else +# define SENTRY_TSAN_IGNORE_READS_BEGIN() +# define SENTRY_TSAN_IGNORE_READS_END() +# endif +#else +# define SENTRY_TSAN_IGNORE_READS_BEGIN() +# define SENTRY_TSAN_IGNORE_READS_END() +#endif + +#endif // SENTRY_TSAN_H_INCLUDED diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index a957f36a7..cbb63c71f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -37,7 +37,6 @@ XX(count_sampled_events) XX(crash_marker) XX(crashed_last_run) XX(custom_logger) -XX(logger_enable_disable_functionality) XX(deserialize_envelope) XX(deserialize_envelope_empty) XX(deserialize_envelope_empty_attachments) @@ -68,6 +67,10 @@ XX(dsn_with_ending_forward_slash_will_be_cleaned) XX(dsn_with_non_http_scheme_is_invalid) XX(dsn_without_project_id_is_invalid) XX(dsn_without_url_scheme_is_invalid) +XX(embedded_info_basic) +XX(embedded_info_disabled) +XX(embedded_info_format) +XX(embedded_info_sentry_version) XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) @@ -78,6 +81,7 @@ XX(invalid_dsn) XX(invalid_proxy) XX(iso_time) XX(lazy_attachments) +XX(logger_enable_disable_functionality) XX(message_with_null_text_is_valid) XX(module_addr) XX(module_finder) @@ -85,10 +89,10 @@ XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) XX(multiple_transactions) +XX(options_logger_enabled_when_crashed_default) XX(options_sdk_name_custom) XX(options_sdk_name_defaults) XX(options_sdk_name_invalid) -XX(options_logger_enabled_when_crashed_default) XX(os) XX(os_release_non_existent_files) XX(os_releases_snapshot) @@ -206,7 +210,3 @@ XX(value_unicode) XX(value_user) XX(value_wrong_type) XX(write_raw_envelope_to_file) -XX(embedded_info_basic) -XX(embedded_info_disabled) -XX(embedded_info_format) -XX(embedded_info_sentry_version) From 677e85e6af15c3e22e789ad72e571b0bb64f5d9e Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 17 Sep 2025 16:21:35 +0200 Subject: [PATCH 14/27] tests: remove flaky logger test from transport suite (#1378) --- tests/test_unit.py | 2 +- tests/unit/test_logger.c | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_unit.py b/tests/test_unit.py index f49eb8efa..47d5995fc 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -14,7 +14,7 @@ def test_unit(cmake, unittest): @pytest.mark.skipif(not has_http, reason="tests need http transport") def test_unit_transport(cmake, unittest): - if unittest in ["custom_logger"]: + if unittest in ["custom_logger", "logger_enable_disable_functionality"]: pytest.skip("excluded from transport test-suite") cwd = cmake(["sentry_test_unit"], {"SENTRY_BACKEND": "none"}) diff --git a/tests/unit/test_logger.c b/tests/unit/test_logger.c index e28b1f7c6..9d343eef9 100644 --- a/tests/unit/test_logger.c +++ b/tests/unit/test_logger.c @@ -8,6 +8,12 @@ typedef struct { bool assert_now; } logger_test_t; +// Note: All logger unit-tests must only run from the transportless unit-test +// suite, since the transport can concurrently log while we do our +// single-threaded test assertions here, leading to flaky test runs. +// To blacklist a test, add to the respective list of `test_unit_transport` +// in the `tests/test_unit.py` unit-test runner. + static void test_logger( sentry_level_t level, const char *message, va_list args, void *_data) From fc52e8bf0e503d9376356c3bc3302796f6ae1c86 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 18 Sep 2025 08:18:26 +0200 Subject: [PATCH 15/27] fix: prevent crashpad from leaking Objective-C ARC compile options (#1375) --- CHANGELOG.md | 4 ++++ external/crashpad | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e6657626..613aa7b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ **Fixes**: +- Include `stddef.h` explicitly in `crashpad` since future `libc++` revisions will stop providing this include transitively. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#132](https://github.com/getsentry/crashpad/pull/132)) +- Fall back on `JWASM` in the _MinGW_ `crashpad` build only if _no_ `CMAKE_ASM_MASM_COMPILER` has been defined. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#133](https://github.com/getsentry/crashpad/pull/133)) +- Prevent `crashpad` from leaking Objective-C ARC compile options into any parent target linkage. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#134](https://github.com/getsentry/crashpad/pull/134)) - Fixed a TOCTOU race between session init/shutdown and event capture. ([#1377](https://github.com/getsentry/sentry-native/pull/1377)) **Features**: @@ -17,6 +20,7 @@ **Internal**: - Support downstream Xbox SDK specifying networking initialization mechanism. ([#1359](https://github.com/getsentry/sentry-native/pull/1359)) +- Added `crashpad` support infrastructure for the external crash reporter feature. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#131](https://github.com/getsentry/crashpad/pull/131)) **Docs**: diff --git a/external/crashpad b/external/crashpad index e24b0f9e7..41e345a6e 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit e24b0f9e760e27464fe2ed30fdd7be45a27a67ad +Subproject commit 41e345a6eb64b3349d6a43f834e22bfddafb32e0 From 88ee955084439e264acc8379a9d298fddb54af18 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Thu, 18 Sep 2025 12:33:56 +0200 Subject: [PATCH 16/27] feat: Add comprehensive semver support for SENTRY_SDK_VERSION parsing (#1379) * chore: support + and - suffixes in SENTRY_SDK_VERSION parsing The regex now matches versions with build metadata or pre-release suffixes like '0.10.1+20250917' or '1.0.0-alpha+build.123', extracting only the base semver part for CMAKE project VERSION while preserving the full version string in the header. Uses * quantifier to support multiple suffixes in version strings. This enables console SDKs to embed their own versioning schemes while maintaining CMake compatibility. * refactor: update version extraction to support full semver format and improve resource file generation --- CMakeLists.txt | 30 ++++++++++++++++++++++-------- cmake/utils.cmake | 6 ------ sentry.rc.in | 4 ++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a55ff05d..557cffe7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,16 +9,30 @@ else() cmake_policy(SET CMP0077 NEW) endif() -#read sentry-native version -file(READ "include/sentry.h" _SENTRY_HEADER_CONTENT) -string(REGEX MATCH "#define SENTRY_SDK_VERSION \"([0-9\.]+)\"" _SENTRY_VERSION_MATCH "${_SENTRY_HEADER_CONTENT}") -set(SENTRY_VERSION "${CMAKE_MATCH_1}") -unset(_SENTRY_HEADER_CONTENT) -unset(_SENTRY_VERSION_MATCH) +# Extract version string from SENTRY_SDK_VERSION define in sentry.h +file(READ "include/sentry.h" _VERSION_STR_TMP) +# Supports full semver format including prerelease and build metadata +string(REGEX MATCH "#define SENTRY_SDK_VERSION \"([^\"]+)\"" _VERSION_STR_TMP "${_VERSION_STR_TMP}") +set(SENTRY_VERSION_FULL "${CMAKE_MATCH_1}") +# Extract just the major.minor.patch part from the full version +string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" _VERSION_STR_TMP "${SENTRY_VERSION_FULL}") +set(SENTRY_VERSION_MAJOR "${CMAKE_MATCH_1}") +set(SENTRY_VERSION_MINOR "${CMAKE_MATCH_2}") +set(SENTRY_VERSION_PATCH "${CMAKE_MATCH_3}") +set(SENTRY_VERSION_BASE "${SENTRY_VERSION_MAJOR}.${SENTRY_VERSION_MINOR}.${SENTRY_VERSION_PATCH}") +unset(_VERSION_STR_TMP) + +message(STATUS "Sentry SDK version (full): ${SENTRY_VERSION_FULL}") +message(STATUS "Sentry SDK version (base): ${SENTRY_VERSION_BASE}") +message(STATUS "Sentry SDK version major='${SENTRY_VERSION_MAJOR}' minor='${SENTRY_VERSION_MINOR}' patch='${SENTRY_VERSION_PATCH}'") + +if(NOT SENTRY_VERSION_FULL OR NOT SENTRY_VERSION_BASE OR SENTRY_VERSION_MAJOR STREQUAL "" OR SENTRY_VERSION_MINOR STREQUAL "" OR SENTRY_VERSION_PATCH STREQUAL "") + message(FATAL_ERROR "Parsed Sentry SDK version '${SENTRY_VERSION_FULL}' does not match expected semver format") +endif() project(Sentry-Native LANGUAGES C CXX ASM - VERSION ${SENTRY_VERSION} + VERSION ${SENTRY_VERSION_BASE} ) set(SENTRY_MAIN_PROJECT OFF) @@ -710,7 +724,7 @@ configure_package_config_file(sentry-config.cmake.in sentry-config.cmake # We would have liked to use `SameMinorVersion`, but that is only supported on # CMake >= 3.11. write_basic_package_version_file(sentry-config-version.cmake - VERSION ${SENTRY_VERSION} + VERSION ${SENTRY_VERSION_BASE} COMPATIBILITY SameMajorVersion) sentry_install(TARGETS sentry EXPORT sentry diff --git a/cmake/utils.cmake b/cmake/utils.cmake index f107af467..578921761 100644 --- a/cmake/utils.cmake +++ b/cmake/utils.cmake @@ -4,12 +4,6 @@ function(sentry_add_version_resource TGT FILE_DESCRIPTION) set(RESOURCE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${TGT}.rc") set(RESOURCE_PATH_TMP "${RESOURCE_PATH}.in") - # Extract major, minor and patch version from SENTRY_VERSION - string(REPLACE "." ";" _SENTRY_VERSION_LIST "${SENTRY_VERSION}") - list(GET _SENTRY_VERSION_LIST 0 SENTRY_VERSION_MAJOR) - list(GET _SENTRY_VERSION_LIST 1 SENTRY_VERSION_MINOR) - list(GET _SENTRY_VERSION_LIST 2 SENTRY_VERSION_PATCH) - # Produce the resource file with configure-time replacements configure_file("${SENTRY_SOURCE_DIR}/sentry.rc.in" "${RESOURCE_PATH_TMP}" @ONLY) diff --git a/sentry.rc.in b/sentry.rc.in index 921811728..ea401ae9d 100644 --- a/sentry.rc.in +++ b/sentry.rc.in @@ -7,12 +7,12 @@ BEGIN BLOCK "040904E4" BEGIN VALUE "FileDescription", "@FILE_DESCRIPTION@" - VALUE "FileVersion", "@SENTRY_VERSION@" + VALUE "FileVersion", "@SENTRY_VERSION_FULL@" VALUE "InternalName", "sentry-native" VALUE "LegalCopyright", "https://sentry.io" VALUE "OriginalFilename", "$" VALUE "ProductName", "Sentry Native SDK" - VALUE "ProductVersion", "@SENTRY_VERSION@" + VALUE "ProductVersion", "@SENTRY_VERSION_FULL@" END END BLOCK "VarFileInfo" From 2a4d53a774139e2022374d4f7c883fd673c0e7de Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 18 Sep 2025 15:26:25 +0200 Subject: [PATCH 17/27] fix: update `crashpad` submodule to a commit on `getsentry` branch (#1385) --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index 41e345a6e..79a91fe62 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 41e345a6eb64b3349d6a43f834e22bfddafb32e0 +Subproject commit 79a91fe62324c9c17c8ec1b8508778228ba955a7 From 1a246665b0fdd82615915dea306b556955f10c78 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 18 Sep 2025 15:49:55 +0200 Subject: [PATCH 18/27] fix: make windows resource generation multi-config aware (#1383) --- CHANGELOG.md | 1 + cmake/utils.cmake | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613aa7b4c..3ae73ebdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Fall back on `JWASM` in the _MinGW_ `crashpad` build only if _no_ `CMAKE_ASM_MASM_COMPILER` has been defined. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#133](https://github.com/getsentry/crashpad/pull/133)) - Prevent `crashpad` from leaking Objective-C ARC compile options into any parent target linkage. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#134](https://github.com/getsentry/crashpad/pull/134)) - Fixed a TOCTOU race between session init/shutdown and event capture. ([#1377](https://github.com/getsentry/sentry-native/pull/1377)) +- Make the Windows resource generation aware of config-specific output paths for multi-config generators. ([#1383](https://github.com/getsentry/sentry-native/pull/1383)) **Features**: diff --git a/cmake/utils.cmake b/cmake/utils.cmake index 578921761..00493aab9 100644 --- a/cmake/utils.cmake +++ b/cmake/utils.cmake @@ -1,8 +1,9 @@ # Generates a version resource file from the `sentry.rc.in` template for the `TGT` argument and adds it as a source. function(sentry_add_version_resource TGT FILE_DESCRIPTION) - # generate a resource output-path from the target name - set(RESOURCE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${TGT}.rc") - set(RESOURCE_PATH_TMP "${RESOURCE_PATH}.in") + # generate a multi-config aware resource output-path from the target name + set(RESOURCE_BASENAME "${TGT}.rc") + set(RESOURCE_PATH_TMP "${CMAKE_CURRENT_BINARY_DIR}/${RESOURCE_BASENAME}.in") + set(RESOURCE_PATH "${CMAKE_CURRENT_BINARY_DIR}/$>,$/,>${RESOURCE_BASENAME}") # Produce the resource file with configure-time replacements configure_file("${SENTRY_SOURCE_DIR}/sentry.rc.in" "${RESOURCE_PATH_TMP}" @ONLY) From c0a06b173b793d4280c0d55199ccc515510085d2 Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 18 Sep 2025 16:11:03 +0200 Subject: [PATCH 19/27] fix: remove ASM language from the top-level CMake project (#1384) We currently have no assembler requirement in the top-level project and thus were triggering CMake policy CMP194. There should be no assembler configuration at all as long as we haven't added any of the subprojects. Also, ensure that breakpad has an assembler for its Linux getcontext implementation --- CHANGELOG.md | 1 + CMakeLists.txt | 2 +- external/CMakeLists.txt | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ae73ebdd..5c12e094b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Prevent `crashpad` from leaking Objective-C ARC compile options into any parent target linkage. ([#1375](https://github.com/getsentry/sentry-native/pull/1375), [crashpad#134](https://github.com/getsentry/crashpad/pull/134)) - Fixed a TOCTOU race between session init/shutdown and event capture. ([#1377](https://github.com/getsentry/sentry-native/pull/1377)) - Make the Windows resource generation aware of config-specific output paths for multi-config generators. ([#1383](https://github.com/getsentry/sentry-native/pull/1383)) +- Remove the `ASM` language from the top-level CMake project, as this triggered CMake policy `CMP194` which isn't applicable to the top-level. ([#1384](https://github.com/getsentry/sentry-native/pull/1384)) **Features**: diff --git a/CMakeLists.txt b/CMakeLists.txt index 557cffe7c..1969a888b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ if(NOT SENTRY_VERSION_FULL OR NOT SENTRY_VERSION_BASE OR SENTRY_VERSION_MAJOR ST endif() project(Sentry-Native - LANGUAGES C CXX ASM + LANGUAGES C CXX VERSION ${SENTRY_VERSION_BASE} ) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 19b29a31c..4ccb3dd9a 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -144,6 +144,17 @@ if(LINUX OR ANDROID) if(HAVE_GETCONTEXT) target_compile_definitions(breakpad_client PRIVATE HAVE_GETCONTEXT) else() + # Enable ASM for getcontext once, only if not already enabled + get_property(_langs GLOBAL PROPERTY ENABLED_LANGUAGES) + if(NOT "ASM" IN_LIST _langs) + include(CheckLanguage) + check_language(ASM) + if(NOT CMAKE_ASM_COMPILER) + message(FATAL_ERROR "ASM required for breakpad's Linux context code, but no assembler found.") + endif() + enable_language(ASM) + endif() + target_sources(breakpad_client PRIVATE ${BREAKPAD_SOURCES_COMMON_LINUX_GETCONTEXT}) endif() From 558f724583df0c4a44a95fe7b64efea50b9ba660 Mon Sep 17 00:00:00 2001 From: Janohmat <53753460+JanFellner@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:44:06 +0200 Subject: [PATCH 20/27] fix: add `crashpad_mpack` to the MSVC static runtime config (#1386) + properly aligned sorting order of wer lib to follow the leading code segment --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1969a888b..35d4dd544 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -628,6 +628,7 @@ if(SENTRY_BACKEND_CRASHPAD) set_property(TARGET crashpad_handler PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_handler_lib PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_minidump PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + set_property(TARGET crashpad_mpack PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_snapshot PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_tools PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_util PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") @@ -643,12 +644,13 @@ if(SENTRY_BACKEND_CRASHPAD) set_target_properties(crashpad_handler PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_handler_lib PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_minidump PROPERTIES FOLDER ${SENTRY_FOLDER}) + set_target_properties(crashpad_mpack PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_snapshot PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_tools PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_util PROPERTIES FOLDER ${SENTRY_FOLDER}) + set_target_properties(crashpad_wer PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_zlib PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(mini_chromium PROPERTIES FOLDER ${SENTRY_FOLDER}) - set_target_properties(crashpad_wer PROPERTIES FOLDER ${SENTRY_FOLDER}) endif() target_link_libraries(sentry PRIVATE From b5a603f5a9a29261b437ccd5ecac962e34396d2f Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Thu, 18 Sep 2025 17:12:23 +0200 Subject: [PATCH 21/27] test: add build test for static runtime with crashpad (#1387) * test: add build test for static runtime with crashpad * add contribution thanks to unreleased section * add doc string to test, so people know where to look for fix to the failing test --- CHANGELOG.md | 4 ++++ tests/test_build_static.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c12e094b..070e8ed6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ - Document the CMake 4 requirement on macOS `SDKROOT` due to its empty default for `CMAKE_OSX_SYSROOT` in the `README`. ([#1368](https://github.com/getsentry/sentry-native/pull/1368)) +**Thank you**: + +- [JanFellner](https://github.com/JanFellner) + ## 0.10.1 **Internal**: diff --git a/tests/test_build_static.py b/tests/test_build_static.py index 161ac8bb5..e575f3fd2 100644 --- a/tests/test_build_static.py +++ b/tests/test_build_static.py @@ -55,6 +55,24 @@ def test_static_crashpad(cmake): ) +@pytest.mark.skipif(not has_crashpad, reason="test needs crashpad backend") +@pytest.mark.skipif(not sys.platform == "win32", reason="test requires Windows") +def test_static_crashpad_static_runtime(cmake): + """ + When this test fails it is most likely that you didn't reflect a target change inside the `crashpad` build in the + top-level `crashpad` target properties (`FOLDER`, `MSVC_RUNTIME_LIBRARY`) for Windows builds. + """ + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "crashpad", + "SENTRY_TRANSPORT": "none", + "BUILD_SHARED_LIBS": "OFF", + "SENTRY_BUILD_RUNTIMESTATIC": "ON", + }, + ) + + @pytest.mark.skipif(not has_breakpad, reason="test needs breakpad backend") def test_static_breakpad(cmake): tmp_path = cmake( From 3bd091313ae97be90be62696a2babe591a988eb8 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 18 Sep 2025 17:14:33 +0000 Subject: [PATCH 22/27] release: 0.11.0 --- CHANGELOG.md | 2 +- include/sentry.h | 2 +- ndk/gradle.properties | 2 +- tests/assertions.py | 4 ++-- tests/test_integration_http.py | 2 +- tests/win_utils.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 070e8ed6b..1c920596f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.11.0 **Breaking changes**: diff --git a/include/sentry.h b/include/sentry.h index 635dcf00d..094ff06de 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -78,7 +78,7 @@ extern "C" { # define SENTRY_SDK_NAME "sentry.native" # endif #endif -#define SENTRY_SDK_VERSION "0.10.1" +#define SENTRY_SDK_VERSION "0.11.0" #define SENTRY_SDK_USER_AGENT SENTRY_SDK_NAME "/" SENTRY_SDK_VERSION /* marks a function as part of the sentry API */ diff --git a/ndk/gradle.properties b/ndk/gradle.properties index 5836ef108..2bf245e7a 100644 --- a/ndk/gradle.properties +++ b/ndk/gradle.properties @@ -7,7 +7,7 @@ org.gradle.parallel=true android.useAndroidX=true # Release information, used for maven publishing -versionName=0.10.1 +versionName=0.11.0 # disable renderscript, it's enabled by default android.defaults.buildfeatures.renderscript=false diff --git a/tests/assertions.py b/tests/assertions.py index 83f5e8d96..349587942 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -105,9 +105,9 @@ def assert_event_meta( } expected_sdk = { "name": "sentry.native", - "version": "0.10.1", + "version": "0.11.0", "packages": [ - {"name": "github:getsentry/sentry-native", "version": "0.10.1"}, + {"name": "github:getsentry/sentry-native", "version": "0.11.0"}, ], } if is_android: diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 51d846b4f..83076384e 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -44,7 +44,7 @@ # fmt: off auth_header = ( - "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.10.1" + "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.11.0" ) # fmt: on diff --git a/tests/win_utils.py b/tests/win_utils.py index 331dba54f..0492ac430 100644 --- a/tests/win_utils.py +++ b/tests/win_utils.py @@ -1,7 +1,7 @@ import pathlib import win32api -sentry_version = "0.10.1" +sentry_version = "0.11.0" def check_binary_version(binary_path: pathlib.Path): From 1f8e4c45f03d1036161132aa0bc990316ee99c3a Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 23 Sep 2025 15:33:10 +0200 Subject: [PATCH 23/27] feat: structured logging (#1271) * add sentry logs option * add sentry logs option to example * feat(logs): add sentry log API + send first logs (#1272) * add sentry log API + send first logs * fix log_level_as_string * attach attributes to logs * attach formatted message + args * add to example * add more attributes * cleanup * windows warning-as-error * windows warning-as-error v2 * windows warning-as-error v2 (final) * add unit tests for initial logs * memleak attempted fix * memleak attempted fix 2 * cleanup * use `sentry_level_t` instead of new log level enum * add SENTRY_LEVEL_TRACE to sentry_logger * quick anti-brownout fix - see https://github.com/getsentry/sentry-native/pull/1274 * fix missing SENTRY_LEVEL_INFO string return * fix logger level check + add test * cleanup logs parameter extraction * warn-as-error fix * const char* fix * static function * feat(logs): add (u)int64 sentry_value_t type (#1301) * add (u)int64 sentry_value_t type * add value_to_msgpack missing switch cases * remove undefined behavior test (C99 6.3.1.4) * avoid Windows sized integer name collision * cleanup & apply code review feedback * more cleanup & remove type coercion * update logs param conversion * own uint64 string * apply suggestions from code review * fixed log parameter conversion * update example to avoid warning-as-error * feat(logs): batching (#1338) * initial queue attempt * add timer * prototype double buffer approach * update logs unit tests for batching * replace timer with bgworker * add first integration tests * update example.c with correct log thread amounts * cleanup * add wait for 'adding' logs in flush logic * go back to single queue for performance testing * add time checks * add ToDos + cleanup sentry_value_t cloning * initial attempt * cond_wait for timer + 'adding' spinlock * add sleep for tests * add sleep for tests * force flush before attempting timer_worker shutdown * add proper cond_wait for 'adding' counter * revert to manual flush on shutdown instead of timer thread flush * add separate timer_stop atomic * cleanup + replace 'adding' cond_wait by pure spinlock * change bgworker for simpler thread implementation * cleanup * fix memleak * fixes * cleanup * cleanup * windows fixes * update shutdown order * fixes * explicit check to stop timer task * windows cleanup * loosen threaded test assertion for CI - too much variability in thread scheduling, so we can expect pretty much anything * add continue for unexpected logs flush trigger instead of attempting flush * Windows re-add condition variable trigger case * feat(logs): add `before_send_log` (#1355) * add `before_send_log` callback * add `before_send_log` callback tests * (temporary) add debug for calling sentry_options_free * remove early return * add late return * cleanup * fix ownership issues in single buffer batching (#1362) * let the producer thread sleep for 1ms between logs * fix two missing NULL checks in the json serializer * clean up logging and early exits in `enqueue_log_single()` * clean up ownerships in logs * eliminate clones (we expect that everything outlives the logs being sent except local construction) * use incref everywhere where we ref global state. this was the cause of the UAF, partially solved with the clones but a few were missing. no reason to clone if we do not want to disconnect for a particular object graph * clarify that add_attribute expects ownership * minimize scope_mut by moving os_context out * raise that log output in throughput tests add to variability (stdout logging should be turned off when running a limit) * log error in case we weren't able to start the log batching worker * fix clang-cl warning * ci: fix failing mingw build (#1361) * ci: fix failing mingw build * split `ASM_MASM_COMPILER` and `_FLAGS` * add `ASM_MASM_FLAGS` in `mingw` install step * specify the `CMAKE_ASM_MASM_COMPILER` as a `FILEPATH` * clean up CMAKE_DEFINES construction so it is easier to diff in the future * fix `LLVM_MINGW_INSTALL_PATH` to be referenced locally rather than $env (cherry picked from commit 519554ff62e1b77564345d25c531e99dda7337f8) * use UNREACHABLE macro to fix anal warnings * batching double buffered (#1365) * first attempt at double buffered * remove the sleep from the windows thread func * clean up thread waiting in the example * adapt the double buffer to use retries, fix remaining issues, clean up and write inline docs * return early in example on sentry_init error. * fix formatting via shorter name for thread gate atomic * improve inline docs of log_buffer_t members * fetch os_context from scope * move scope/options data retrieval into separate function + add expected keys to test * update logs API to return status code * cleanup * add log-event trace connection test * remove duplicate test * specify macOS SDKROOT --------- Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> * add flush retry for missed flush requests * move flush retry into flush function * add docs --------- Co-authored-by: Mischan Toosarani-Hausberger * update CHANGELOG.md * use `trace_id` from scoped spans for logs * fix copy-paste leftover + docs * add log_sleep for thread test + variable NUM_LOGS * no `usleep` on windows :( * fix seconds->milliseconds * cleanup * test(logs): add 32-bit vargs test (#1370) * add vargs conversion test * add ifdef for 32-bit systems * Update tests/unit/test_logs.c Co-authored-by: Mischan Toosarani-Hausberger * add comment * fix comment --------- Co-authored-by: Mischan Toosarani-Hausberger * Apply suggestions from code review Co-authored-by: Mischan Toosarani-Hausberger * post-merge cleanup * pin ruamel version * let's unpin ruamel.yaml.clib to get 0.2.14 which seemingly fixes the missing header: https://sourceforge.net/p/ruamel-yaml-clib/tickets/47/#de77 * add empty payload check * log output of logger tests if we fail to find the pre-crash marker * fix: move `is_tsan` marker into the `has_crashpad` condition... ...so we can ignore in which module a `crashpad` test runs. * fix: update `has_crashpad` condition comment * really only move `is_tsan`, but keep the module level `pytestmark` * CHANGELOG.md update * CHANGELOG.md update * CHANGELOG.md update --------- Co-authored-by: Mischan Toosarani-Hausberger --- CHANGELOG.md | 6 + examples/example.c | 129 ++++- include/sentry.h | 78 +++ src/CMakeLists.txt | 2 + src/sentry_core.c | 15 + src/sentry_envelope.c | 41 ++ src/sentry_envelope.h | 6 + src/sentry_logger.c | 5 +- src/sentry_logger.h | 5 + src/sentry_logs.c | 767 +++++++++++++++++++++++++++++ src/sentry_logs.h | 24 + src/sentry_options.c | 20 + src/sentry_options.h | 3 + src/sentry_string.h | 11 + src/sentry_value.c | 24 +- tests/__init__.py | 1 + tests/assertions.py | 32 ++ tests/conditions.py | 9 +- tests/test_integration_crashpad.py | 6 +- tests/test_integration_http.py | 222 +++++++++ tests/test_integration_logger.py | 8 + tests/unit/CMakeLists.txt | 1 + tests/unit/test_logger.c | 66 +++ tests/unit/test_logs.c | 166 +++++++ tests/unit/test_process.c | 2 +- tests/unit/tests.inc | 5 + 26 files changed, 1640 insertions(+), 14 deletions(-) create mode 100644 src/sentry_logs.c create mode 100644 src/sentry_logs.h create mode 100644 tests/unit/test_logs.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c920596f..439a29b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add support for structured logs. It is currently experimental, and one can enable it by setting `sentry_options_set_enable_logs`. When enabled, you can capture a log using `sentry_log_info()` (or another log level). Logs can be filtered by setting the `before_send_log` hook. ([#1271](https://github.com/getsentry/sentry-native/pull/1271/)) + ## 0.11.0 **Breaking changes**: diff --git a/examples/example.c b/examples/example.c index 71ba67af7..4538d9440 100644 --- a/examples/example.c +++ b/examples/example.c @@ -24,6 +24,7 @@ # define sleep_s(SECONDS) Sleep((SECONDS) * 1000) #else +# include # include # include @@ -158,6 +159,33 @@ discarding_before_transaction_callback(sentry_value_t tx, void *user_data) return tx; } +static sentry_value_t +before_send_log_callback(sentry_value_t log, void *user_data) +{ + (void)user_data; + sentry_value_t attribute = sentry_value_new_object(); + sentry_value_set_by_key( + attribute, "value", sentry_value_new_string("little")); + sentry_value_set_by_key( + attribute, "type", sentry_value_new_string("string")); + sentry_value_set_by_key(sentry_value_get_by_key(log, "attributes"), + "coffeepot.size", attribute); + return log; +} + +static sentry_value_t +discarding_before_send_log_callback(sentry_value_t log, void *user_data) +{ + (void)user_data; + if (sentry_value_is_null( + sentry_value_get_by_key(sentry_value_get_by_key(log, "attributes"), + "sentry.message.template"))) { + sentry_value_decref(log); + return sentry_value_new_null(); + } + return log; +} + // Test logger that outputs in a format the integration tests can parse static void test_logger_callback( @@ -291,6 +319,42 @@ create_debug_crumb(const char *message) return debug_crumb; } +#define NUM_THREADS 50 +#define NUM_LOGS 100 // how many log calls each thread makes +#define LOG_SLEEP_MS 1 // time (in ms) between log calls + +#if defined(SENTRY_PLATFORM_WINDOWS) +# define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) +#else +# define sleep_ms(MILLISECONDS) usleep(MILLISECONDS * 1000) +#endif + +#ifdef SENTRY_PLATFORM_WINDOWS +DWORD WINAPI +log_thread_func(LPVOID lpParam) +{ + (void)lpParam; + for (int i = 0; i < NUM_LOGS; i++) { + sentry_log_debug( + "thread log %d on thread %lu", i, get_current_thread_id()); + sleep_ms(LOG_SLEEP_MS); + } + return 0; +} +#else +void * +log_thread_func(void *arg) +{ + (void)arg; + for (int i = 0; i < NUM_LOGS; i++) { + sentry_log_debug( + "thread log %d on thread %llu", i, get_current_thread_id()); + sleep_ms(LOG_SLEEP_MS); + } + return NULL; +} +#endif + int main(int argc, char **argv) { @@ -364,6 +428,16 @@ main(int argc, char **argv) options, discarding_before_transaction_callback, NULL); } + if (has_arg(argc, argv, "before-send-log")) { + sentry_options_set_before_send_log( + options, before_send_log_callback, NULL); + } + + if (has_arg(argc, argv, "discarding-before-send-log")) { + sentry_options_set_before_send_log( + options, discarding_before_send_log_callback, NULL); + } + if (has_arg(argc, argv, "traces-sampler")) { sentry_options_set_traces_sampler( options, traces_sampler_callback, NULL); @@ -416,7 +490,13 @@ main(int argc, char **argv) sentry_options_set_logger_enabled_when_crashed(options, 1); } - sentry_init(options); + if (has_arg(argc, argv, "enable-logs")) { + sentry_options_set_enable_logs(options, true); + } + + if (0 != sentry_init(options)) { + return EXIT_FAILURE; + } if (has_arg(argc, argv, "attachment")) { sentry_attachment_t *bytes @@ -424,6 +504,45 @@ main(int argc, char **argv) sentry_attachment_set_content_type(bytes, "application/octet-stream"); } + if (sentry_options_get_enable_logs(options)) { + if (has_arg(argc, argv, "capture-log")) { + sentry_log_debug("I'm a log message!"); + } + if (has_arg(argc, argv, "logs-timer")) { + for (int i = 0; i < 10; i++) { + sentry_log_info("Informational log nr.%d", i); + } + // sleep >5s to trigger logs timer + sleep_s(6); + // we should see two envelopes make its way to Sentry + sentry_log_debug("post-sleep log"); + } + if (has_arg(argc, argv, "logs-threads")) { + // Spawn multiple threads to test concurrent logging +#ifdef SENTRY_PLATFORM_WINDOWS + HANDLE threads[NUM_THREADS]; + for (int t = 0; t < NUM_THREADS; t++) { + threads[t] + = CreateThread(NULL, 0, log_thread_func, NULL, 0, NULL); + } + + WaitForMultipleObjects(NUM_THREADS, threads, TRUE, INFINITE); + + for (int t = 0; t < NUM_THREADS; t++) { + CloseHandle(threads[t]); + } +#else + pthread_t threads[NUM_THREADS]; + for (int t = 0; t < NUM_THREADS; t++) { + pthread_create(&threads[t], NULL, log_thread_func, NULL); + } + for (int t = 0; t < NUM_THREADS; t++) { + pthread_join(threads[t], NULL); + } +#endif + } + } + if (!has_arg(argc, argv, "no-setup")) { sentry_set_transaction("test-transaction"); sentry_set_level(SENTRY_LEVEL_WARNING); @@ -670,9 +789,15 @@ main(int argc, char **argv) sentry_value_t event = sentry_value_new_message_event( SENTRY_LEVEL_INFO, "my-logger", "Hello World!"); sentry_capture_event(event); + if (has_arg(argc, argv, "logs-scoped-transaction")) { + sentry_log_debug("logging during scoped transaction event"); + } } sentry_transaction_finish(tx); + if (has_arg(argc, argv, "logs-scoped-transaction")) { + sentry_log_debug("logging after scoped transaction event"); + } } if (has_arg(argc, argv, "capture-minidump")) { @@ -689,4 +814,6 @@ main(int argc, char **argv) if (has_arg(argc, argv, "crash-after-shutdown")) { trigger_crash(); } + + return EXIT_SUCCESS; } diff --git a/include/sentry.h b/include/sentry.h index 094ff06de..74378ca2f 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -442,6 +442,7 @@ SENTRY_API char *sentry_value_to_json(sentry_value_t value); * Sentry levels for events and breadcrumbs. */ typedef enum sentry_level_e { + SENTRY_LEVEL_TRACE = -2, SENTRY_LEVEL_DEBUG = -1, SENTRY_LEVEL_INFO = 0, SENTRY_LEVEL_WARNING = 1, @@ -1889,6 +1890,83 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_traces_sampler( sentry_options_t *opts, sentry_traces_sampler_function callback, void *user_data); +/** + * Enables or disables the structured logging feature. + * When disabled, all calls to sentry_logger_X() are no-ops. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs( + sentry_options_t *opts, int enable_logs); +SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( + const sentry_options_t *opts); + +/** + * The potential returns of calling any of the sentry_log_X functions + * - Success means a log was enqueued + * - Discard means the `before_send_log` function discarded the log + * - Failed means the log wasn't enqueued. This happens if the buffers are full + * - Disabled means the option `enable_logs` was false. + */ +typedef enum { + SENTRY_LOG_RETURN_SUCCESS = 0, + SENTRY_LOG_RETURN_DISCARD = 1, + SENTRY_LOG_RETURN_FAILED = 2, + SENTRY_LOG_RETURN_DISABLED = 3 +} log_return_value_t; + +/** + * Structured logging interface. Minimally blocks the client trying to log, + * but is therefore lossy when enqueueing a log fails + * (e.g. when both buffers are full). + * + * Format string restrictions: + * Only a subset of printf format specifiers are supported for parameter + * extraction. Supported specifiers include: + * - %d, %i - signed integers (treated as long long) + * - %u, %x, %X, %o - unsigned integers (treated as unsigned long long) + * - %f, %F, %e, %E, %g, %G - floating point numbers (treated as double) + * - %c - single character + * - %s - null-terminated string (null pointers are handled as "(null)") + * - %p - pointer value (formatted as hexadecimal string) + * + * Unsupported format specifiers will consume their corresponding argument + * but will be recorded as "(unknown)" in the structured log data. + * Length modifiers (h, l, L, z, j, t) are parsed but ignored. + * + * Because of this, please only use 64-bit types/casts for your arguments. + * + * Flags, width, and precision specifiers are parsed but currently ignored for + * parameter extraction purposes. + */ +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_trace( + const char *message, ...); +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_debug( + const char *message, ...); +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_info( + const char *message, ...); +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_warn( + const char *message, ...); +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_error( + const char *message, ...); +SENTRY_EXPERIMENTAL_API log_return_value_t sentry_log_fatal( + const char *message, ...); + +/** + * Type of the `before_send_log` callback. + * + * The callback takes ownership of the `log`, and should usually return + * that same log. In case the log should be discarded, the + * callback needs to call `sentry_value_decref` on the provided log, and + * return a `sentry_value_new_null()` instead. + */ +typedef sentry_value_t (*sentry_before_send_log_function_t)( + sentry_value_t log, void *user_data); + +/** + * Sets the `before_send_log` callback. + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_before_send_log( + sentry_options_t *opts, sentry_before_send_log_function_t func, void *data); + #ifdef SENTRY_PLATFORM_LINUX /** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48e8b0e82..5b8ccabc5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -17,6 +17,8 @@ sentry_target_sources_cwd(sentry sentry_json.h sentry_logger.c sentry_logger.h + sentry_logs.c + sentry_logs.h sentry_options.c sentry_options.h sentry_os.c diff --git a/src/sentry_core.c b/src/sentry_core.c index dd920f486..eced483ed 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -8,6 +8,7 @@ #include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_logs.h" #include "sentry_options.h" #include "sentry_path.h" #include "sentry_random.h" @@ -165,6 +166,7 @@ sentry_init(sentry_options_t *options) sentry_close(); sentry_logger_t logger = { NULL, NULL, SENTRY_LEVEL_DEBUG }; + if (options->debug) { logger = options->logger; } @@ -287,6 +289,10 @@ sentry_init(sentry_options_t *options) sentry_start_session(); } + if (options->enable_logs) { + sentry__logs_startup(); + } + sentry__mutex_unlock(&g_options_lock); return 0; @@ -313,6 +319,15 @@ sentry_flush(uint64_t timeout) int sentry_close(void) { + // Shutdown logs system before locking options to ensure logs are flushed. + // This prevents a potential deadlock on the options during log envelope + // creation. + SENTRY_WITH_OPTIONS (options) { + if (options->enable_logs) { + sentry__logs_shutdown(options->shutdown_timeout); + } + } + SENTRY__MUTEX_INIT_DYN_ONCE(g_options_lock); // this function is to be called only once, so we do not allow more than one // caller diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 797bc8937..0a9b82d77 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -265,6 +265,9 @@ sentry__envelope_add_event(sentry_envelope_t *envelope, sentry_value_t event) item->event = event; sentry__jsonwriter_write_value(jw, event); item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + if (!item->payload) { + return NULL; + } sentry__envelope_item_set_header( item, "type", sentry_value_new_string("event")); @@ -347,6 +350,9 @@ sentry__envelope_add_transaction( item->event = transaction; sentry__jsonwriter_write_value(jw, transaction); item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + if (!item->payload) { + return NULL; + } sentry__envelope_item_set_header( item, "type", sentry_value_new_string("transaction")); @@ -401,6 +407,38 @@ sentry__envelope_add_transaction( return item; } +sentry_envelope_item_t * +sentry__envelope_add_logs(sentry_envelope_t *envelope, sentry_value_t logs) +{ + sentry_envelope_item_t *item = envelope_add_item(envelope); + if (!item) { + return NULL; + } + + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + if (!jw) { + return NULL; + } + + sentry__jsonwriter_write_value(jw, logs); + item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + if (!item->payload) { + return NULL; + } + + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("log")); + sentry__envelope_item_set_header(item, "item_count", + sentry_value_new_int32((int32_t)sentry_value_get_length( + sentry_value_get_by_key(logs, "items")))); + sentry__envelope_item_set_header(item, "content_type", + sentry_value_new_string("application/vnd.sentry.items.log+json")); + sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); + sentry__envelope_item_set_header(item, "length", length); + + return item; +} + sentry_envelope_item_t * sentry__envelope_add_user_report( sentry_envelope_t *envelope, sentry_value_t user_report) @@ -419,6 +457,9 @@ sentry__envelope_add_user_report( sentry__jsonwriter_write_value(jw, user_report); item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + if (!item->payload) { + return NULL; + } sentry__envelope_item_set_header( item, "type", sentry_value_new_string("user_report")); diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index a41985dce..c52871217 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -49,6 +49,12 @@ sentry_envelope_item_t *sentry__envelope_add_transaction( sentry_envelope_item_t *sentry__envelope_add_user_report( sentry_envelope_t *envelope, sentry_value_t user_report); +/** + * Add a list of logs to this envelope. + */ +sentry_envelope_item_t *sentry__envelope_add_logs( + sentry_envelope_t *envelope, sentry_value_t logs); + /** * Add a user feedback to this envelope. */ diff --git a/src/sentry_logger.c b/src/sentry_logger.c index 94c67f759..5bbf493ec 100644 --- a/src/sentry_logger.c +++ b/src/sentry_logger.c @@ -73,6 +73,8 @@ const char * sentry__logger_describe(sentry_level_t level) { switch (level) { + case SENTRY_LEVEL_TRACE: + return "TRACE "; case SENTRY_LEVEL_DEBUG: return "DEBUG "; case SENTRY_LEVEL_INFO: @@ -94,8 +96,7 @@ sentry__logger_log(sentry_level_t level, const char *message, ...) if (!sentry__atomic_fetch(&g_logger_enabled)) { return; } - if (g_logger.logger_level != SENTRY_LEVEL_DEBUG - && level < g_logger.logger_level) { + if (level < g_logger.logger_level) { return; } sentry_logger_t logger = g_logger; diff --git a/src/sentry_logger.h b/src/sentry_logger.h index 0f5e25034..87191da72 100644 --- a/src/sentry_logger.h +++ b/src/sentry_logger.h @@ -21,6 +21,11 @@ void sentry__logger_log(sentry_level_t level, const char *message, ...); void sentry__logger_enable(void); void sentry__logger_disable(void); +#define SENTRY_TRACEF(message, ...) \ + sentry__logger_log(SENTRY_LEVEL_TRACE, message, __VA_ARGS__) + +#define SENTRY_TRACE(message) sentry__logger_log(SENTRY_LEVEL_TRACE, message) + #define SENTRY_DEBUGF(message, ...) \ sentry__logger_log(SENTRY_LEVEL_DEBUG, message, __VA_ARGS__) diff --git a/src/sentry_logs.c b/src/sentry_logs.c new file mode 100644 index 000000000..f4c09dfba --- /dev/null +++ b/src/sentry_logs.c @@ -0,0 +1,767 @@ +#include "sentry_logs.h" +#include "sentry_core.h" +#include "sentry_envelope.h" +#include "sentry_options.h" +#include "sentry_os.h" +#include "sentry_scope.h" +#include "sentry_sync.h" +#if defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_NX) +# include "sentry_unix_spinlock.h" +#endif +#include +#include + +#ifdef SENTRY_UNITTEST +# define QUEUE_LENGTH 5 +#else +# define QUEUE_LENGTH 100 +#endif +#define FLUSH_TIMER 5 + +typedef struct { + sentry_value_t logs[QUEUE_LENGTH]; + long index; // (atomic) index for producer threads to get a unique slot + long adding; // (atomic) count of in-flight writers on this buffer + long sealed; // (atomic) 0=writeable, 1=sealed (meaning we drop) +} log_buffer_t; + +static struct { + log_buffer_t buffers[2]; // double buffer + long active_idx; // (atomic) index to the active buffer + long flushing; // (atomic) reentrancy guard to the flusher + long batching_stop; // (atomic) run variable of the batching thread + sentry_cond_t request_flush; // condition variable to schedule a flush + sentry_threadid_t batching_thread; // the batching thread +} g_logs_state = { + { + { + .index = 0, + .adding = 0, + .sealed = 0, + }, + { + .index = 0, + .adding = 0, + .sealed = 0, + }, + }, + .active_idx = 0, + .flushing = 0, +}; + +// checks whether the currently active buffer should be flushed. +// otherwise we could miss the trigger of adding the last log if we're actively +// flushing the other buffer already. +// we can safely check the state of the active buffer, as the only thread that +// can change which buffer is active is the one calling this check function +// inside flush_logs_queue() below +static bool +check_for_flush_condition(void) +{ + // In flush_logs_queue, after finishing a flush: + long current_active = sentry__atomic_fetch(&g_logs_state.active_idx); + log_buffer_t *current_buf = &g_logs_state.buffers[current_active]; + + // Check if current active buffer is also full + // We could even lower the threshold for high-contention scenarios + return sentry__atomic_fetch(¤t_buf->index) >= QUEUE_LENGTH; +} + +static void +flush_logs_queue(void) +{ + const long already_flushing + = sentry__atomic_store(&g_logs_state.flushing, 1); + if (already_flushing) { + return; + } + do { + // prep both buffers + long old_buf_idx = sentry__atomic_fetch(&g_logs_state.active_idx); + long new_buf_idx = 1 - old_buf_idx; + log_buffer_t *old_buf = &g_logs_state.buffers[old_buf_idx]; + log_buffer_t *new_buf = &g_logs_state.buffers[new_buf_idx]; + + // reset new buffer... + sentry__atomic_store(&new_buf->index, 0); + sentry__atomic_store(&new_buf->adding, 0); + sentry__atomic_store(&new_buf->sealed, 0); + + // ...and make it active (after this we're good to go producer side) + sentry__atomic_store(&g_logs_state.active_idx, new_buf_idx); + + // seal old buffer + sentry__atomic_store(&old_buf->sealed, 1); + + // Wait for all in-flight producers of the old buffer + while (sentry__atomic_fetch(&old_buf->adding) > 0) { + // TODO currently only on unix +#ifdef SENTRY_PLATFORM_UNIX + sentry__cpu_relax(); +#endif + } + + long n = sentry__atomic_store(&old_buf->index, 0); + if (n > QUEUE_LENGTH) { + n = QUEUE_LENGTH; + } + + if (n > 0) { + // now we can do the actual batching of the old buffer + + sentry_value_t logs = sentry_value_new_object(); + sentry_value_t log_items = sentry_value_new_list(); + int i; + for (i = 0; i < n; i++) { + sentry_value_append(log_items, old_buf->logs[i]); + } + sentry_value_set_by_key(logs, "items", log_items); + + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry__envelope_add_logs(envelope, logs); + SENTRY_WITH_OPTIONS (options) { + sentry__capture_envelope(options->transport, envelope); + } + sentry_value_decref(logs); + } + } while (check_for_flush_condition()); + + sentry__atomic_store(&g_logs_state.flushing, 0); +} + +#define ENQUEUE_MAX_RETRIES 2 + +static bool +enqueue_log(sentry_value_t log) +{ + for (int attempt = 0; attempt <= ENQUEUE_MAX_RETRIES; attempt++) { + // retrieve the active buffer + const long active_idx = sentry__atomic_fetch(&g_logs_state.active_idx); + log_buffer_t *active = &g_logs_state.buffers[active_idx]; + + // if the buffer is already sealed we retry or drop and exit early. + if (sentry__atomic_fetch(&active->sealed) != 0) { + if (attempt == ENQUEUE_MAX_RETRIES) { + return false; + } + continue; + } + + // `adding` is our boundary for this buffer since it keeps the flusher + // blocked. We have to recheck that the flusher hasn't already switched + // the active buffer or sealed the one this thread is on. If either is + // true we have to unblock the flusher and retry or drop the log. + sentry__atomic_fetch_and_add(&active->adding, 1); + const long active_idx_check + = sentry__atomic_fetch(&g_logs_state.active_idx); + const long sealed_check = sentry__atomic_fetch(&active->sealed); + if (active_idx != active_idx_check) { + sentry__atomic_fetch_and_add(&active->adding, -1); + if (attempt == ENQUEUE_MAX_RETRIES) { + return false; + } + continue; + } + if (sealed_check) { + sentry__atomic_fetch_and_add(&active->adding, -1); + if (attempt == ENQUEUE_MAX_RETRIES) { + return false; + } + continue; + } + + // Now we can finally request a slot and check if the log fits in this + // buffer. + const long log_idx = sentry__atomic_fetch_and_add(&active->index, 1); + if (log_idx < QUEUE_LENGTH) { + // got a slot, write log to the buffer and unblock flusher + active->logs[log_idx] = log; + sentry__atomic_fetch_and_add(&active->adding, -1); + + // Check if active buffer is now full and trigger flush. We could + // introduce additional watermarks here to trigger the flush earlier + // under high contention. + // TODO replace with a level-triggered flag + if (log_idx == QUEUE_LENGTH - 1) { + sentry__cond_wake(&g_logs_state.request_flush); + } + return true; + } + // ping the batching thread to flush, since we could miss a cond_wake + // on adding the last item + sentry__cond_wake(&g_logs_state.request_flush); + // Buffer is already full, roll back our increments and retry or drop. + sentry__atomic_fetch_and_add(&active->adding, -1); + if (attempt == ENQUEUE_MAX_RETRIES) { + // TODO report this (e.g. client reports) + return false; + } + } + return false; +} + +SENTRY_THREAD_FN +batching_thread_func(void *data) +{ + (void)data; + SENTRY_DEBUG("Starting batching thread"); + sentry_mutex_t task_lock; + sentry__mutex_init(&task_lock); + sentry__mutex_lock(&task_lock); + + // check if thread got a shut-down signal + while (sentry__atomic_fetch(&g_logs_state.batching_stop) == 0) { + // Sleep for 5 seconds or until request_flush hits + const int triggered_by = sentry__cond_wait_timeout( + &g_logs_state.request_flush, &task_lock, 5000); + + // make sure loop invariant still holds + if (sentry__atomic_fetch(&g_logs_state.batching_stop) != 0) { + break; + } + + switch (triggered_by) { + case 0: +#ifdef SENTRY_PLATFORM_WINDOWS + if (GetLastError() == ERROR_TIMEOUT) { + SENTRY_DEBUG("Logs flushed by timeout"); + break; + } +#endif + SENTRY_DEBUG("Logs flushed by filled buffer"); + break; +#ifdef SENTRY_PLATFORM_UNIX + case ETIMEDOUT: + SENTRY_DEBUG("Logs flushed by timeout"); + break; +#endif +#ifdef SENTRY_PLATFORM_WINDOWS + case 1: + SENTRY_DEBUG("Logs flushed by filled buffer"); + break; +#endif + default: + SENTRY_WARN("Logs flush trigger returned unexpected value"); + continue; + } + + // Try to flush logs + flush_logs_queue(); + } + + sentry__mutex_unlock(&task_lock); + sentry__mutex_free(&task_lock); + return 0; +} + +static const char * +level_as_string(sentry_level_t level) +{ + switch (level) { + case SENTRY_LEVEL_TRACE: + return "trace"; + case SENTRY_LEVEL_DEBUG: + return "debug"; + case SENTRY_LEVEL_INFO: + return "info"; + case SENTRY_LEVEL_WARNING: + return "warn"; + case SENTRY_LEVEL_ERROR: + return "error"; + case SENTRY_LEVEL_FATAL: + return "fatal"; + default: + return "unknown"; + } +} + +// TODO to be portable, pass in the length format specifier +#ifndef SENTRY_UNITTEST +static +#endif + sentry_value_t + construct_param_from_conversion(const char conversion, va_list *args_copy) +{ + sentry_value_t param_obj = sentry_value_new_object(); + switch (conversion) { + case 'd': + case 'i': { + long long val = va_arg(*args_copy, long long); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_int64(val)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("integer")); + break; + } + case 'u': + case 'x': + case 'X': + case 'o': { + unsigned long long int val = va_arg(*args_copy, unsigned long long int); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_uint64(val)); + // TODO update once unsigned 64-bit can be sent + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 'f': + case 'F': + case 'e': + case 'E': + case 'g': + case 'G': { + double val = va_arg(*args_copy, double); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_double(val)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("double")); + break; + } + case 'c': { + int val = va_arg(*args_copy, int); + char str[2] = { (char)val, '\0' }; + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(str)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 's': { + const char *val = va_arg(*args_copy, const char *); + if (val) { + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(val)); + } else { + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string("(null)")); + } + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + case 'p': { + void *val = va_arg(*args_copy, void *); + char ptr_str[32]; + snprintf(ptr_str, sizeof(ptr_str), "%p", val); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string(ptr_str)); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + default: + // Unknown format specifier, skip the argument + (void)va_arg(*args_copy, void *); + sentry_value_set_by_key( + param_obj, "value", sentry_value_new_string("(unknown)")); + sentry_value_set_by_key( + param_obj, "type", sentry_value_new_string("string")); + break; + } + + return param_obj; +} + +static const char * +skip_flags(const char *fmt_ptr) +{ + while (*fmt_ptr + && (*fmt_ptr == '-' || *fmt_ptr == '+' || *fmt_ptr == ' ' + || *fmt_ptr == '#' || *fmt_ptr == '0')) { + fmt_ptr++; + } + return fmt_ptr; +} + +static const char * +skip_width(const char *fmt_ptr) +{ + while (*fmt_ptr && (*fmt_ptr >= '0' && *fmt_ptr <= '9')) { + fmt_ptr++; + } + return fmt_ptr; +} + +static const char * +skip_precision(const char *fmt_ptr) +{ + + if (*fmt_ptr == '.') { + fmt_ptr++; + while (*fmt_ptr && (*fmt_ptr >= '0' && *fmt_ptr <= '9')) { + fmt_ptr++; + } + } + return fmt_ptr; +} + +static const char * +skip_length(const char *fmt_ptr) +{ + while (*fmt_ptr + && (*fmt_ptr == 'h' || *fmt_ptr == 'l' || *fmt_ptr == 'L' + || *fmt_ptr == 'z' || *fmt_ptr == 'j' || *fmt_ptr == 't')) { + fmt_ptr++; + } + return fmt_ptr; +} + +// returns how many parameters were added to the attributes object +#ifndef SENTRY_UNITTEST +static +#endif + int + populate_message_parameters( + sentry_value_t attributes, const char *message, va_list args) +{ + if (!message || sentry_value_is_null(attributes)) { + return 0; + } + + const char *fmt_ptr = message; + int param_index = 0; + va_list args_copy; + va_copy(args_copy, args); + + while (*fmt_ptr) { + // Find the next format specifier + if (*fmt_ptr == '%') { + fmt_ptr++; // Skip the '%' + + if (*fmt_ptr == '%') { + // Escaped '%', not a format specifier + fmt_ptr++; + continue; + } + + fmt_ptr = skip_flags(fmt_ptr); + fmt_ptr = skip_width(fmt_ptr); + fmt_ptr = skip_precision(fmt_ptr); + fmt_ptr = skip_length(fmt_ptr); + + // Get the conversion specifier + char conversion = *fmt_ptr; + if (conversion) { + char key[64]; + snprintf(key, sizeof(key), "sentry.message.parameter.%d", + param_index); + sentry_value_t param_obj + = construct_param_from_conversion(conversion, &args_copy); + sentry_value_set_by_key(attributes, key, param_obj); + param_index++; + fmt_ptr++; + } + } else { + fmt_ptr++; + } + } + + va_end(args_copy); + return param_index; +} + +/** + * This function assumes that `value` is owned, so we have to make sure that the + * `value` was created or cloned by the caller or even better inc_refed. + */ +static void +add_attribute(sentry_value_t attributes, sentry_value_t value, const char *type, + const char *name) +{ + sentry_value_t param_obj = sentry_value_new_object(); + sentry_value_set_by_key(param_obj, "value", value); + sentry_value_set_by_key(param_obj, "type", sentry_value_new_string(type)); + sentry_value_set_by_key(attributes, name, param_obj); +} + +/** + * Extracts data from the scope and options, and adds it to the attributes + * as well as directly setting `trace_id` for the log. + * + * We clone most values instead of incref, since they might otherwise change + * between constructing the log & flushing it to an envelope. + */ +static void +add_scope_and_options_data(sentry_value_t log, sentry_value_t attributes) +{ + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_value_t trace_id = sentry_value_get_by_key( + sentry_value_get_by_key(scope->propagation_context, "trace"), + "trace_id"); + sentry_value_incref(trace_id); + sentry_value_set_by_key(log, "trace_id", trace_id); + + sentry_value_t parent_span_id = sentry_value_new_object(); + sentry_value_t scoped_span_trace_id = sentry_value_new_null(); + if (scope->transaction_object) { + sentry_value_t span_id = sentry_value_get_by_key( + scope->transaction_object->inner, "span_id"); + sentry_value_incref(span_id); + sentry_value_set_by_key(parent_span_id, "value", span_id); + scoped_span_trace_id = sentry_value_get_by_key( + scope->transaction_object->inner, "trace_id"); + sentry_value_incref(scoped_span_trace_id); + } else if (scope->span) { + sentry_value_t span_id + = sentry_value_get_by_key(scope->span->inner, "span_id"); + sentry_value_incref(span_id); + sentry_value_set_by_key(parent_span_id, "value", span_id); + scoped_span_trace_id + = sentry_value_get_by_key(scope->span->inner, "trace_id"); + sentry_value_incref(scoped_span_trace_id); + } + sentry_value_set_by_key( + parent_span_id, "type", sentry_value_new_string("string")); + if (scope->transaction_object || scope->span) { + sentry_value_set_by_key( + attributes, "sentry.trace.parent_span_id", parent_span_id); + sentry_value_set_by_key(log, "trace_id", scoped_span_trace_id); + } else { + sentry_value_decref(parent_span_id); + sentry_value_decref(scoped_span_trace_id); + } + + if (!sentry_value_is_null(scope->user)) { + sentry_value_t user_id = sentry_value_get_by_key(scope->user, "id"); + if (!sentry_value_is_null(user_id)) { + sentry_value_incref(user_id); + add_attribute(attributes, user_id, "string", "user.id"); + } + + sentry_value_t user_username + = sentry_value_get_by_key(scope->user, "username"); + if (!sentry_value_is_null(user_username)) { + sentry_value_incref(user_username); + add_attribute(attributes, user_username, "string", "user.name"); + } + + sentry_value_t user_email + = sentry_value_get_by_key(scope->user, "email"); + if (!sentry_value_is_null(user_email)) { + sentry_value_incref(user_email); + add_attribute(attributes, user_email, "string", "user.email"); + } + } + sentry_value_t os_context + = sentry_value_get_by_key(scope->contexts, "os"); + if (!sentry_value_is_null(os_context)) { + sentry_value_t os_name + = sentry_value_get_by_key(os_context, "name"); + sentry_value_t os_version + = sentry_value_get_by_key(os_context, "version"); + if (!sentry_value_is_null(os_name)) { + sentry_value_incref(os_name); + add_attribute(attributes, os_name, "string", "os.name"); + } + if (!sentry_value_is_null(os_version)) { + sentry_value_incref(os_version); + add_attribute(attributes, os_version, "string", "os.version"); + } + } + } + + SENTRY_WITH_OPTIONS (options) { + if (options->environment) { + add_attribute(attributes, + sentry_value_new_string(options->environment), "string", + "sentry.environment"); + } + if (options->release) { + add_attribute(attributes, sentry_value_new_string(options->release), + "string", "sentry.release"); + } + } + + add_attribute(attributes, sentry_value_new_string("sentry.native"), + "string", "sentry.sdk.name"); + add_attribute(attributes, sentry_value_new_string(sentry_sdk_version()), + "string", "sentry.sdk.version"); +} + +static sentry_value_t +construct_log(sentry_level_t level, const char *message, va_list args) +{ + sentry_value_t log = sentry_value_new_object(); + sentry_value_t attributes = sentry_value_new_object(); + + va_list args_copy_1, args_copy_2, args_copy_3; + va_copy(args_copy_1, args); + va_copy(args_copy_2, args); + va_copy(args_copy_3, args); + int len = vsnprintf(NULL, 0, message, args_copy_1) + 1; + va_end(args_copy_1); + size_t size = (size_t)len; + char *fmt_message = sentry_malloc(size); + if (!fmt_message) { + va_end(args_copy_2); + va_end(args_copy_3); + return sentry_value_new_null(); + } + + vsnprintf(fmt_message, size, message, args_copy_2); + va_end(args_copy_2); + + sentry_value_set_by_key(log, "body", sentry_value_new_string(fmt_message)); + sentry_free(fmt_message); + sentry_value_set_by_key( + log, "level", sentry_value_new_string(level_as_string(level))); + + // timestamp in seconds + uint64_t usec_time = sentry__usec_time(); + sentry_value_set_by_key(log, "timestamp", + sentry_value_new_double((double)usec_time / 1000000.0)); + + // adds data from the scope & options to the attributes, and adds `trace_id` + // to the log + add_scope_and_options_data(log, attributes); + + // Parse variadic arguments and add them to attributes + if (populate_message_parameters(attributes, message, args_copy_3)) { + // only add message template if we have parameters + add_attribute(attributes, sentry_value_new_string(message), "string", + "sentry.message.template"); + } + va_end(args_copy_3); + + sentry_value_set_by_key(log, "attributes", attributes); + + return log; +} + +log_return_value_t +sentry__logs_log(sentry_level_t level, const char *message, va_list args) +{ + bool enable_logs = false; + SENTRY_WITH_OPTIONS (options) { + if (options->enable_logs) + enable_logs = true; + } + if (enable_logs) { + bool discarded = false; + // create log from message + sentry_value_t log = construct_log(level, message, args); + SENTRY_WITH_OPTIONS (options) { + if (options->before_send_log_func) { + log = options->before_send_log_func( + log, options->before_send_log_data); + if (sentry_value_is_null(log)) { + SENTRY_DEBUG( + "log was discarded by the `before_send_log` hook"); + discarded = true; + } + } + } + if (discarded) { + return SENTRY_LOG_RETURN_DISCARD; + } + if (!enqueue_log(log)) { + sentry_value_decref(log); + return SENTRY_LOG_RETURN_FAILED; + } + return SENTRY_LOG_RETURN_SUCCESS; + } + return SENTRY_LOG_RETURN_DISABLED; +} + +log_return_value_t +sentry_log_trace(const char *message, ...) +{ + va_list args; + va_start(args, message); + log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_TRACE, message, args); + va_end(args); + return result; +} + +log_return_value_t +sentry_log_debug(const char *message, ...) +{ + va_list args; + va_start(args, message); + const log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_DEBUG, message, args); + va_end(args); + return result; +} + +log_return_value_t +sentry_log_info(const char *message, ...) +{ + va_list args; + va_start(args, message); + const log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_INFO, message, args); + va_end(args); + return result; +} + +log_return_value_t +sentry_log_warn(const char *message, ...) +{ + va_list args; + va_start(args, message); + const log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_WARNING, message, args); + va_end(args); + return result; +} + +log_return_value_t +sentry_log_error(const char *message, ...) +{ + va_list args; + va_start(args, message); + const log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_ERROR, message, args); + va_end(args); + return result; +} + +log_return_value_t +sentry_log_fatal(const char *message, ...) +{ + va_list args; + va_start(args, message); + const log_return_value_t result + = sentry__logs_log(SENTRY_LEVEL_FATAL, message, args); + va_end(args); + return result; +} + +void +sentry__logs_startup(void) +{ + sentry__cond_init(&g_logs_state.request_flush); + + sentry__thread_init(&g_logs_state.batching_thread); + int spawn_result = sentry__thread_spawn( + &g_logs_state.batching_thread, batching_thread_func, NULL); + + if (spawn_result == 1) { + SENTRY_ERROR("Failed to start batching thread"); + } +} + +void +sentry__logs_shutdown(uint64_t timeout) +{ + (void)timeout; + SENTRY_DEBUG("shutting down logs system"); + + // Signal the batching thread to stop running + if (sentry__atomic_store(&g_logs_state.batching_stop, 1) != 0) { + SENTRY_DEBUG("preventing double shutdown of logs system"); + return; + } + sentry__cond_wake(&g_logs_state.request_flush); + sentry__thread_join(g_logs_state.batching_thread); + + // Perform final flush to ensure any remaining logs are sent + flush_logs_queue(); + + sentry__thread_free(&g_logs_state.batching_thread); + + SENTRY_DEBUG("logs system shutdown complete"); +} diff --git a/src/sentry_logs.h b/src/sentry_logs.h new file mode 100644 index 000000000..b23e65263 --- /dev/null +++ b/src/sentry_logs.h @@ -0,0 +1,24 @@ +#ifndef SENTRY_LOGS_H_INCLUDED +#define SENTRY_LOGS_H_INCLUDED + +#include "sentry_boot.h" + +log_return_value_t sentry__logs_log( + sentry_level_t level, const char *message, va_list args); + +/** + * Sets up the logs timer/flush thread + */ +void sentry__logs_startup(void); + +/** + * Instructs the logs timer/flush thread to shut down. + */ +void sentry__logs_shutdown(uint64_t timeout); + +#ifdef SENTRY_UNITTEST +int populate_message_parameters( + sentry_value_t attributes, const char *message, va_list args); +#endif + +#endif diff --git a/src/sentry_options.c b/src/sentry_options.c index 0b1622bf7..253e00dae 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -154,6 +154,14 @@ sentry_options_set_before_transaction( opts->before_transaction_data = user_data; } +void +sentry_options_set_before_send_log(sentry_options_t *opts, + sentry_before_send_log_function_t func, void *user_data) +{ + opts->before_send_log_func = func; + opts->before_send_log_data = user_data; +} + void sentry_options_set_dsn_n( sentry_options_t *opts, const char *raw_dsn, size_t raw_dsn_len) @@ -682,6 +690,18 @@ sentry_options_set_backend(sentry_options_t *opts, sentry_backend_t *backend) opts->backend = backend; } +void +sentry_options_set_enable_logs(sentry_options_t *opts, int enable_logs) +{ + opts->enable_logs = !!enable_logs; +} + +int +sentry_options_get_enable_logs(const sentry_options_t *opts) +{ + return opts->enable_logs; +} + #ifdef SENTRY_PLATFORM_LINUX sentry_handler_strategy_t diff --git a/src/sentry_options.h b/src/sentry_options.h index 6e61721d2..3b780a815 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -53,12 +53,15 @@ struct sentry_options_s { void *on_crash_data; sentry_transaction_function_t before_transaction_func; void *before_transaction_data; + sentry_before_send_log_function_t before_send_log_func; + void *before_send_log_data; /* Experimentally exposed */ double traces_sample_rate; sentry_traces_sampler_function traces_sampler; void *traces_sampler_data; size_t max_spans; + bool enable_logs; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_string.h b/src/sentry_string.h index 6b98c5766..48fa86f97 100644 --- a/src/sentry_string.h +++ b/src/sentry_string.h @@ -193,6 +193,17 @@ sentry__int64_to_string(int64_t val) return sentry__string_clone(buf); } +/** + * Converts an uint64_t into a string. + */ +static inline char * +sentry__uint64_to_string(uint64_t val) +{ + char buf[24]; + snprintf(buf, sizeof(buf), "%" PRIu64, val); + return sentry__string_clone(buf); +} + #ifdef SENTRY_PLATFORM_WINDOWS /** * Create a utf-8 string from a Wide String. diff --git a/src/sentry_value.c b/src/sentry_value.c index 5cfe25bad..af4c8709b 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -110,6 +110,8 @@ static const char * level_as_string(sentry_level_t level) { switch (level) { + case SENTRY_LEVEL_TRACE: + return "trace"; case SENTRY_LEVEL_DEBUG: return "debug"; case SENTRY_LEVEL_WARNING: @@ -315,7 +317,7 @@ sentry_value_new_int32(int32_t value) sentry_value_t sentry_value_new_double(double value) { - thing_t *thing = sentry_malloc(sizeof(thing_t)); + thing_t *thing = SENTRY_MAKE(thing_t); if (!thing) { return sentry_value_new_null(); } @@ -331,7 +333,7 @@ sentry_value_new_double(double value) sentry_value_t sentry_value_new_int64(int64_t value) { - thing_t *thing = sentry_malloc(sizeof(thing_t)); + thing_t *thing = SENTRY_MAKE(thing_t); if (!thing) { return sentry_value_new_null(); } @@ -347,7 +349,7 @@ sentry_value_new_int64(int64_t value) sentry_value_t sentry_value_new_uint64(uint64_t value) { - thing_t *thing = sentry_malloc(sizeof(thing_t)); + thing_t *thing = SENTRY_MAKE(thing_t); if (!thing) { return sentry_value_new_null(); } @@ -1055,7 +1057,13 @@ sentry__jsonwriter_write_value(sentry_jsonwriter_t *jw, sentry_value_t value) sentry__jsonwriter_write_str(jw, sentry_value_as_string(value)); break; case SENTRY_VALUE_TYPE_LIST: { - const list_t *l = value_as_thing(value)->payload._ptr; + const thing_t *thing = value_as_thing(value); + if (!thing) { + UNREACHABLE("thing of a list is NULL during serialization"); + return; + } + + const list_t *l = thing->payload._ptr; sentry__jsonwriter_write_list_start(jw); for (size_t i = 0; i < l->len; i++) { sentry__jsonwriter_write_value(jw, l->items[i]); @@ -1064,7 +1072,13 @@ sentry__jsonwriter_write_value(sentry_jsonwriter_t *jw, sentry_value_t value) break; } case SENTRY_VALUE_TYPE_OBJECT: { - const obj_t *o = value_as_thing(value)->payload._ptr; + const thing_t *thing = value_as_thing(value); + if (!thing) { + UNREACHABLE("thing of an object is NULL during serialization"); + return; + } + + const obj_t *o = thing->payload._ptr; sentry__jsonwriter_write_object_start(jw); for (size_t i = 0; i < o->len; i++) { sentry__jsonwriter_write_key(jw, o->pairs[i].k); diff --git a/tests/__init__.py b/tests/__init__.py index 255ce8ff3..cfe3e9dd8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -284,6 +284,7 @@ def deserialize_from( "session", "transaction", "user_report", + "log", ]: rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: diff --git a/tests/assertions.py b/tests/assertions.py index 349587942..ce768cb21 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -226,6 +226,38 @@ def assert_attachment(envelope): ) +def assert_logs(envelope, expected_item_count=1, expected_trace_id=None): + logs = None + for item in envelope: + assert item.headers.get("type") == "log" + # >= because of random #lost logs in test_logs_threaded + assert item.headers.get("item_count") >= expected_item_count + assert ( + item.headers.get("content_type") == "application/vnd.sentry.items.log+json" + ) + logs = item.payload.json + + assert isinstance(logs, dict) + assert "items" in logs + # >= because of random #lost logs in test_logs_threaded + assert len(logs["items"]) >= expected_item_count + for i in range(expected_item_count): + log_item = logs["items"][i] + assert "body" in log_item + assert "level" in log_item + assert "timestamp" in log_item # TODO do we need to validate the timestamp? + assert "trace_id" in log_item + assert "attributes" in log_item + assert "os.name" in log_item["attributes"] + assert "os.version" in log_item["attributes"] + assert "sentry.environment" in log_item["attributes"] + assert "sentry.release" in log_item["attributes"] + assert "sentry.sdk.name" in log_item["attributes"] + assert "sentry.sdk.version" in log_item["attributes"] + if expected_trace_id: + assert log_item["trace_id"] == expected_trace_id + + def assert_attachment_view_hierarchy(envelope): expected = { "type": "attachment", diff --git a/tests/conditions.py b/tests/conditions.py index 4041eb72e..5af7757fb 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -23,9 +23,14 @@ and not is_android and not (is_asan and sys.platform == "darwin") ) -# crashpad requires http, needs porting to AIX, and doesn’t work with kcov/valgrind either +# crashpad requires http, needs porting to AIX, and doesn’t work with kcov/valgrind/tsan either has_crashpad = ( - has_http and not is_valgrind and not is_kcov and not is_android and not is_aix + has_http + and not is_valgrind + and not is_kcov + and not is_android + and not is_aix + and not is_tsan ) # android has no local filesystem has_files = not is_android diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index d456dea8d..8a7838071 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -11,6 +11,7 @@ run, Envelope, ) +from .conditions import has_crashpad from .proxy import ( setup_proxy_env_vars, cleanup_proxy_env_vars, @@ -22,11 +23,10 @@ assert_session, assert_gzip_file_header, ) -from .conditions import has_crashpad, is_tsan pytestmark = pytest.mark.skipif( - not has_crashpad or is_tsan, - reason="tests need crashpad backend and not run with TSAN", + not has_crashpad, + reason="Tests need a crashpad backend and a valid environment for it", ) # Windows and Linux are currently able to flush all the state on crash diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 83076384e..b6d9b8209 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -37,6 +37,7 @@ assert_gzip_file_header, assert_failed_proxy_auth_request, assert_attachment_view_hierarchy, + assert_logs, ) from .conditions import has_http, has_breakpad, has_files @@ -1312,3 +1313,224 @@ def test_capture_with_scope(cmake, httpserver): assert_breadcrumb(envelope, "scoped crumb") assert_attachment(envelope) + + +def test_logs_timer(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + # make sure we are isolated from previous runs + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "logs-timer"], + check=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == 2 + + req_0 = httpserver.log[0][0] + body_0 = req_0.get_data() + + envelope_0 = Envelope.deserialize(body_0) + assert_logs(envelope_0, 10) + + req_1 = httpserver.log[1][0] + body_1 = req_1.get_data() + + envelope_1 = Envelope.deserialize(body_1) + assert_logs(envelope_1) + + +def test_logs_event(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + # make sure we are isolated from previous runs + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "capture-log", "capture-event"], + check=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == 2 + + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + + event_envelope = Envelope.deserialize(event_body) + assert_event(event_envelope) + # ensure that the event and the log are part of the same trace + event_trace_id = event_envelope.items[0].payload.json["contexts"]["trace"][ + "trace_id" + ] + + log_req = httpserver.log[1][0] + log_body = log_req.get_data() + + log_envelope = Envelope.deserialize(log_body) + assert_logs(log_envelope, 1, event_trace_id) + + +def test_logs_scoped_transaction(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + # make sure we are isolated from previous runs + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + [ + "log", + "enable-logs", + "logs-scoped-transaction", + "capture-transaction", + "scope-transaction-event", + ], + check=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + assert len(httpserver.log) == 3 + + event_req = httpserver.log[0][0] + event_body = event_req.get_data() + + event_envelope = Envelope.deserialize(event_body) + assert_event(event_envelope) + # ensure that the event and the log are part of the same trace + event_trace_id = event_envelope.items[0].payload.json["contexts"]["trace"][ + "trace_id" + ] + + tx_req = httpserver.log[1][0] + tx_body = tx_req.get_data() + + tx_envelope = Envelope.deserialize(tx_body) + # ensure that the transaction, event, and logs are part of the same trace + tx_trace_id = tx_envelope.items[0].payload.json["contexts"]["trace"]["trace_id"] + assert tx_trace_id == event_trace_id + + log_req = httpserver.log[2][0] + log_body = log_req.get_data() + + log_envelope = Envelope.deserialize(log_body) + assert_logs(log_envelope, 2, event_trace_id) + + +def test_logs_threaded(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + # make sure we are isolated from previous runs + shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "logs-threads"], + check=True, + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + + # there is a chance we drop logs while flushing buffers + assert 1 <= len(httpserver.log) <= 50 + total_count = 0 + + for i in range(len(httpserver.log)): + req = httpserver.log[i][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + assert_logs(envelope) + total_count += envelope.items[0].headers["item_count"] + print(f"Total amount of captured logs: {total_count}") + assert total_count >= 100 + + +def test_before_send_log(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "capture-log", "before-send-log"], + check=True, + env=env, + ) + + assert len(httpserver.log) == 1 + req = httpserver.log[0][0] + body = req.get_data() + + envelope = Envelope.deserialize(body) + + # Show what the envelope looks like if the test fails. + envelope.print_verbose() + + # Extract the log item + (log_item,) = envelope.items + + assert log_item.headers["type"] == "log" + payload = log_item.payload.json + + # Get the first log item from the logs payload + log_entry = payload["items"][0] + attributes = log_entry["attributes"] + + # Check that the before_send_log callback added the expected attribute + assert "coffeepot.size" in attributes + assert attributes["coffeepot.size"]["value"] == "little" + assert attributes["coffeepot.size"]["type"] == "string" + + +def test_before_send_log_discard(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver), SENTRY_RELEASE="🤮🚀") + + run( + tmp_path, + "sentry_example", + ["log", "enable-logs", "capture-log", "discarding-before-send-log"], + check=True, + env=env, + ) + + # log should have been discarded + assert len(httpserver.log) == 0 diff --git a/tests/test_integration_logger.py b/tests/test_integration_logger.py index a1b02d579..a709757d8 100644 --- a/tests/test_integration_logger.py +++ b/tests/test_integration_logger.py @@ -54,6 +54,14 @@ def _run_logger_crash_test(backend, cmake, logger_option): # Parse the output to check logging behavior parsed_data = parse_logger_output(output) + # TODO: let's disable the logger tests on crashpad tsan (we already had a couple of issues at that boundary) + # however, first let's check if we can get any sensible output when we can't find the marker + if not parsed_data["pre_crash_log_completed"]: + print("Failed to parse the pre-crash log marker") + print("---Output start") + print(output) + print("---Output end") + return parsed_data diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 9add26e97..b6c8dc0fc 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable(sentry_test_unit test_fuzzfailures.c test_info.c test_logger.c + test_logs.c test_modulefinder.c test_mpack.c test_options.c diff --git a/tests/unit/test_logger.c b/tests/unit/test_logger.c index 9d343eef9..3e2b9369c 100644 --- a/tests/unit/test_logger.c +++ b/tests/unit/test_logger.c @@ -95,3 +95,69 @@ SENTRY_TEST(logger_enable_disable_functionality) sentry_init(clean_options); sentry_close(); } + +static void +test_log_level( + sentry_level_t level, const char *message, va_list args, void *_data) +{ + (void)level; + (void)message; + (void)args; + + logger_test_t *data = _data; + if (data->assert_now) { + data->called += 1; + } +} + +SENTRY_TEST(logger_level) +{ + // Test structure: for each level, test that only messages >= that level are + // logged + const struct { + sentry_level_t level; + int expected_count; // How many of the 5 test messages should be logged + } test_cases[] = { + { SENTRY_LEVEL_TRACE, + 5 }, // All messages: TRACE, DEBUG, INFO, WARN, ERROR + { SENTRY_LEVEL_DEBUG, + 4 }, // DEBUG, INFO, WARN, ERROR (TRACE filtered out) + { SENTRY_LEVEL_INFO, 3 }, // INFO, WARN, ERROR + { SENTRY_LEVEL_WARNING, 2 }, // WARN, ERROR + { SENTRY_LEVEL_ERROR, 1 }, // ERROR only + }; + + for (size_t i = 0; i < 5; i++) { // for each of the 5 logger levels + logger_test_t data = { 0, false }; + + { + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_debug(options, true); + sentry_options_set_logger_level(options, test_cases[i].level); + sentry_options_set_logger(options, test_log_level, &data); + + sentry_init(options); + + data.assert_now = true; + // Test all 5 levels in order from most to least verbose + SENTRY_TRACE("Logging Trace"); // level -2 + SENTRY_DEBUG("Logging Debug"); // level -1 + SENTRY_INFO("Logging Info"); // level 0 + SENTRY_WARN("Logging Warning"); // level 1 + SENTRY_ERROR("Logging Error"); // level 2 + + data.assert_now = false; + + TEST_CHECK_INT_EQUAL(data.called, test_cases[i].expected_count); + + sentry_close(); + } + + { + // *really* clear the logger instance + SENTRY_TEST_OPTIONS_NEW(options); + sentry_init(options); + sentry_close(); + } + } +} diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c new file mode 100644 index 000000000..9e2d89dbc --- /dev/null +++ b/tests/unit/test_logs.c @@ -0,0 +1,166 @@ +#include "sentry_logs.h" +#include "sentry_testsupport.h" + +#include "sentry_envelope.h" + +#ifdef SENTRY_PLATFORM_WINDOWS +# include +# define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) +#else +# include +# define sleep_ms(MILLISECONDS) usleep(MILLISECONDS * 1000) +#endif + +static void +validate_logs_envelope(sentry_envelope_t *envelope, void *data) +{ + uint64_t *called = data; + *called += 1; + + // Verify we have at least one envelope item + TEST_CHECK(sentry__envelope_get_item_count(envelope) > 0); + + // Get the first item and check it's a logs item + const sentry_envelope_item_t *item = sentry__envelope_get_item(envelope, 0); + sentry_value_t type_header = sentry__envelope_item_get_header(item, "type"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(type_header), "log"); + + sentry_envelope_free(envelope); +} + +SENTRY_TEST(basic_logging_functionality) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_init(options); + // TODO if we don't sleep, log timer_task might not start in time to flush + sleep_ms(20); + + // These should not crash and should respect the enable_logs option + sentry_log_trace("Trace message"); + sentry_log_debug("Debug message"); + sentry_log_info("Info message"); + sentry_log_warn("Warning message"); + sentry_log_error("Error message"); + // sleep to finish flush of the first 5, otherwise failed enqueue + sleep_ms(20); + sentry_log_fatal("Fatal message"); + sentry_close(); + + // TODO for now we set unit test buffer size to 5; does this make sense? + // Or should we just pump out 100+ logs to fill a batch in a for-loop? + TEST_CHECK_INT_EQUAL(called_transport, 2); +} + +SENTRY_TEST(logs_disabled_by_default) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + // Don't explicitly enable logs - they should be disabled by default + sentry_init(options); + + sentry_log_info("This should not be sent"); + + sentry_close(); + + // Transport should not be called since logs are disabled + TEST_CHECK_INT_EQUAL(called_transport, 0); +} + +SENTRY_TEST(formatted_log_messages) +{ + uint64_t called_transport = 0; + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_enable_logs(options, true); + + sentry_transport_t *transport + = sentry_transport_new(validate_logs_envelope); + sentry_transport_set_state(transport, &called_transport); + sentry_options_set_transport(options, transport); + + sentry_init(options); + + // Test format specifiers + sentry_log_info("String: %s, Integer: %d, Float: %.2f", "test", 42, 3.14); + sentry_log_warn("Character: %c, Hex: 0x%x", 'A', 255); + sentry_log_error("Pointer: %p", (void *)0x1234); + sentry_log_error("Big number: %zu", UINT64_MAX); + sentry_log_error("Small number: %d", INT64_MIN); + + sentry_close(); + + // Transport should be called once + TEST_CHECK_INT_EQUAL(called_transport, 1); +} + +static void +test_param_conversion_helper(const char *format, ...) +{ + sentry_value_t attributes = sentry_value_new_object(); + va_list args; + va_start(args, format); + int param_count = populate_message_parameters(attributes, format, args); + va_end(args); + + // Verify we got the expected number of parameters + TEST_CHECK_INT_EQUAL(param_count, 3); + + // Verify the parameters were extracted correctly + sentry_value_t param0 + = sentry_value_get_by_key(attributes, "sentry.message.parameter.0"); + sentry_value_t param1 + = sentry_value_get_by_key(attributes, "sentry.message.parameter.1"); + sentry_value_t param2 + = sentry_value_get_by_key(attributes, "sentry.message.parameter.2"); + + TEST_CHECK(!sentry_value_is_null(param0)); + TEST_CHECK(!sentry_value_is_null(param1)); + TEST_CHECK(!sentry_value_is_null(param2)); + + // Check the values + sentry_value_t value0 = sentry_value_get_by_key(param0, "value"); + sentry_value_t value1 = sentry_value_get_by_key(param1, "value"); + sentry_value_t value2 = sentry_value_get_by_key(param2, "value"); + + TEST_CHECK_INT_EQUAL(sentry_value_as_int64(value0), 1); + TEST_CHECK_INT_EQUAL(sentry_value_as_int64(value1), 2); + TEST_CHECK_INT_EQUAL(sentry_value_as_int64(value2), 3); + + sentry_value_decref(attributes); +} + +SENTRY_TEST(logs_param_conversion) +{ + // TODO this test shows the current limitation for parsing integers on + // 32-bit systems + int a = 1, b = 2, c = 3; +#if defined(__i386__) || defined(_M_IX86) || defined(__arm__) + // Currently, on 32-bit platforms, we need to cast to a 64-bit integer type + // since the parameter conversion expects long long for %d format specifiers + test_param_conversion_helper( + "%" PRId64 " %" PRId64 " %" PRId64, (int64_t)a, (int64_t)b, (int64_t)c); +#else + // since we read these values as 64-bit, this is still undefined behaviour + // but it works because the variadic arguments are passed in 8-byte slots + test_param_conversion_helper("%d %d %d", a, b, c); +#endif +} diff --git a/tests/unit/test_process.c b/tests/unit/test_process.c index 238ad1f2f..2689384b5 100644 --- a/tests/unit/test_process.c +++ b/tests/unit/test_process.c @@ -7,7 +7,7 @@ # define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) #else # include -# define sleep_ms(SECONDS) usleep(SECONDS * 1000) +# define sleep_ms(MILLISECONDS) usleep(MILLISECONDS * 1000) #endif // merely tests that it doesn't crash with invalid arguments diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index cbb63c71f..4d2407cf9 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -17,6 +17,7 @@ XX(basic_http_request_preparation_for_minidump) XX(basic_http_request_preparation_for_transaction) XX(basic_http_request_preparation_for_user_feedback) XX(basic_http_request_preparation_for_user_report) +XX(basic_logging_functionality) XX(basic_spans) XX(basic_tracing_context) XX(basic_transaction) @@ -74,6 +75,7 @@ XX(embedded_info_sentry_version) XX(empty_transport) XX(event_with_id) XX(exception_without_type_or_value_still_valid) +XX(formatted_log_messages) XX(fuzz_json) XX(init_failure) XX(internal_uuid_api) @@ -82,6 +84,9 @@ XX(invalid_proxy) XX(iso_time) XX(lazy_attachments) XX(logger_enable_disable_functionality) +XX(logger_level) +XX(logs_disabled_by_default) +XX(logs_param_conversion) XX(message_with_null_text_is_valid) XX(module_addr) XX(module_finder) From 075b3bfee1dbb85fa10d50df631286196943a3e0 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 23 Sep 2025 14:23:45 +0000 Subject: [PATCH 24/27] release: 0.11.1 --- CHANGELOG.md | 2 +- include/sentry.h | 2 +- ndk/gradle.properties | 2 +- tests/assertions.py | 4 ++-- tests/test_integration_http.py | 2 +- tests/win_utils.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 439a29b5f..891bd6cb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.11.1 **Features**: diff --git a/include/sentry.h b/include/sentry.h index 74378ca2f..1fec4805a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -78,7 +78,7 @@ extern "C" { # define SENTRY_SDK_NAME "sentry.native" # endif #endif -#define SENTRY_SDK_VERSION "0.11.0" +#define SENTRY_SDK_VERSION "0.11.1" #define SENTRY_SDK_USER_AGENT SENTRY_SDK_NAME "/" SENTRY_SDK_VERSION /* marks a function as part of the sentry API */ diff --git a/ndk/gradle.properties b/ndk/gradle.properties index 2bf245e7a..647c2e07a 100644 --- a/ndk/gradle.properties +++ b/ndk/gradle.properties @@ -7,7 +7,7 @@ org.gradle.parallel=true android.useAndroidX=true # Release information, used for maven publishing -versionName=0.11.0 +versionName=0.11.1 # disable renderscript, it's enabled by default android.defaults.buildfeatures.renderscript=false diff --git a/tests/assertions.py b/tests/assertions.py index ce768cb21..f6db545d6 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -105,9 +105,9 @@ def assert_event_meta( } expected_sdk = { "name": "sentry.native", - "version": "0.11.0", + "version": "0.11.1", "packages": [ - {"name": "github:getsentry/sentry-native", "version": "0.11.0"}, + {"name": "github:getsentry/sentry-native", "version": "0.11.1"}, ], } if is_android: diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index b6d9b8209..88dac3e4c 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -45,7 +45,7 @@ # fmt: off auth_header = ( - "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.11.0" + "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.11.1" ) # fmt: on diff --git a/tests/win_utils.py b/tests/win_utils.py index 0492ac430..84041f9f9 100644 --- a/tests/win_utils.py +++ b/tests/win_utils.py @@ -1,7 +1,7 @@ import pathlib import win32api -sentry_version = "0.11.0" +sentry_version = "0.11.1" def check_binary_version(binary_path: pathlib.Path): From f2eaa5ea85181022e354e76e8accaafb9654e619 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:13:00 +0200 Subject: [PATCH 25/27] test: Fix test failures when session tracking is enabled (#1393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(logs): Fix test failures when session tracking is enabled This commit fixes 3 test failures in test_logs.c that occur when auto-session tracking is enabled (the default): - basic_logging_functionality - formatted_log_messages - logs_disabled_by_default Root causes and fixes: 1. validate_logs_envelope counted all envelopes but only validated logs - Session envelopes from sentry_close() were triggering assertions - Fixed by filtering: only count/validate log envelopes, skip others 2. formatted_log_messages didn't wait for batching thread to start - Added sleep_ms(20) after sentry_init() to match other tests 3. batching_stop flag wasn't reset between sentry_init() calls - Once set to 1 during shutdown, subsequent startups would fail - Fixed by resetting to 0 in sentry__logs_startup() These issues were discovered in console SDK testing where session tracking is enabled by default and tests run sequentially in a single process. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix(logs): Eliminate thread startup race with enum state machine This commit eliminates the thread startup race condition by: 1. Replacing batching_stop with enum-based thread_state - SENTRY_LOGS_THREAD_STOPPED (0): Not running - SENTRY_LOGS_THREAD_RUNNING (1): Thread active and processing logs - Improves code clarity and makes thread lifecycle explicit 2. Thread signals RUNNING state after initialization - Batching thread sets state to RUNNING after mutex setup - Provides deterministic indication that thread is ready 3. Adding test-only helper: sentry__logs_wait_for_thread_startup() - Polls thread_state until RUNNING (max 1 second) - Zero production overhead (only compiled with SENTRY_UNITTEST) - Tests explicitly wait for thread readiness instead of arbitrary sleeps 4. Updating shutdown to use atomic state transition - Atomically transitions from RUNNING to STOPPED - Detects double shutdown or never-started cases - Returns early if thread wasn't running Benefits: - Eliminates race where logs could be enqueued before thread starts - Tests are deterministic (no arbitrary timing assumptions) - Code is clearer with explicit state names - No production overhead (test helper is ifdef'd out) The one remaining sleep in basic_logging_functionality test is intentional - it tests batch timing behavior (wait for buffer flush). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * fix(logs): Fix thread lifecycle race condition causing memory leaks This commit fixes a race condition in the logs batching thread lifecycle that caused valgrind to report 336 byte memory leaks in CI tests. ## Problem When `sentry__logs_shutdown()` ran before the batching thread transitioned from initial state to RUNNING, the two-state model couldn't distinguish between "never started" and "starting but not yet running", causing shutdown to skip joining the thread. ## Solution 1. Added three-state lifecycle enum: - STOPPED (0): Thread never started or shut down - STARTING (1): Thread spawned but not yet initialized - RUNNING (2): Thread active and processing logs 2. Added `sentry__atomic_compare_swap()` primitive for atomic state verification (cross-platform: Windows InterlockedCompareExchange, POSIX __atomic_compare_exchange_n) 3. Startup sets state to STARTING before spawning thread 4. Thread uses CAS to verify STARTING → RUNNING transition - If CAS fails (shutdown already set to STOPPED), exits cleanly - Guarantees thread only runs if it successfully transitioned 5. Shutdown always joins thread if old state != STOPPED ## Benefits - Eliminates race condition where shutdown could miss a spawned thread - Thread explicitly verifies state transition with CAS - No memory leaks in any shutdown scenario - All 212 unit tests pass - All log integration tests pass Fixes test failures: - test_before_send_log - test_before_send_log_discard 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * docs(logs): Address code review comments with clarifying documentation Addresses bot review feedback by adding documentation without changing behavior: 1. **CAS memory ordering**: Added comment explaining sequential consistency usage for thread state transitions and why it's appropriate for synchronization 2. **Condition variable cleanup**: Added note explaining that static storage condition variables don't require explicit cleanup on POSIX and Windows 3. **CAS function documentation**: Enhanced docstring to document memory ordering guarantees and note that ABA problem isn't a concern for simple integer state machines 4. **Shutdown race handling**: Added comment explaining how the atomic CAS in the thread prevents the race when shutdown occurs during STARTING state All changes are documentation/comments only - no behavior changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Update src/sentry_logs.c Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --------- Co-authored-by: Claude Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- src/sentry_logs.c | 102 +++++++++++++++++++++++++++++++++++++---- src/sentry_logs.h | 6 +++ src/sentry_sync.h | 28 +++++++++++ tests/unit/test_logs.c | 16 ++++--- 4 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/sentry_logs.c b/src/sentry_logs.c index f4c09dfba..4cd17128c 100644 --- a/src/sentry_logs.c +++ b/src/sentry_logs.c @@ -13,11 +13,30 @@ #ifdef SENTRY_UNITTEST # define QUEUE_LENGTH 5 +# ifdef SENTRY_PLATFORM_WINDOWS +# include +# define sleep_ms(MILLISECONDS) Sleep(MILLISECONDS) +# else +# include +# define sleep_ms(MILLISECONDS) usleep(MILLISECONDS * 1000) +# endif #else # define QUEUE_LENGTH 100 #endif #define FLUSH_TIMER 5 +/** + * Thread lifecycle states for the logs batching thread. + */ +typedef enum { + /** Thread is not running (initial state or after shutdown) */ + SENTRY_LOGS_THREAD_STOPPED = 0, + /** Thread is starting up but not yet ready */ + SENTRY_LOGS_THREAD_STARTING = 1, + /** Thread is running and processing logs */ + SENTRY_LOGS_THREAD_RUNNING = 2, +} sentry_logs_thread_state_t; + typedef struct { sentry_value_t logs[QUEUE_LENGTH]; long index; // (atomic) index for producer threads to get a unique slot @@ -29,7 +48,7 @@ static struct { log_buffer_t buffers[2]; // double buffer long active_idx; // (atomic) index to the active buffer long flushing; // (atomic) reentrancy guard to the flusher - long batching_stop; // (atomic) run variable of the batching thread + long thread_state; // (atomic) sentry_logs_thread_state_t sentry_cond_t request_flush; // condition variable to schedule a flush sentry_threadid_t batching_thread; // the batching thread } g_logs_state = { @@ -47,6 +66,7 @@ static struct { }, .active_idx = 0, .flushing = 0, + .thread_state = SENTRY_LOGS_THREAD_STOPPED, }; // checks whether the currently active buffer should be flushed. @@ -209,14 +229,30 @@ batching_thread_func(void *data) sentry__mutex_init(&task_lock); sentry__mutex_lock(&task_lock); - // check if thread got a shut-down signal - while (sentry__atomic_fetch(&g_logs_state.batching_stop) == 0) { + // Transition from STARTING to RUNNING using compare-and-swap + // CAS ensures atomic state verification: only succeeds if state is STARTING + // If CAS fails, shutdown already set state to STOPPED, so exit immediately + // Uses sequential consistency to ensure all thread initialization is + // visible + if (!sentry__atomic_compare_swap(&g_logs_state.thread_state, + (long)SENTRY_LOGS_THREAD_STARTING, + (long)SENTRY_LOGS_THREAD_RUNNING)) { + SENTRY_DEBUG("logs thread detected shutdown during startup, exiting"); + sentry__mutex_unlock(&task_lock); + sentry__mutex_free(&task_lock); + return 0; + } + + // Main loop: run while state is RUNNING + while (sentry__atomic_fetch(&g_logs_state.thread_state) + == SENTRY_LOGS_THREAD_RUNNING) { // Sleep for 5 seconds or until request_flush hits const int triggered_by = sentry__cond_wait_timeout( &g_logs_state.request_flush, &task_lock, 5000); - // make sure loop invariant still holds - if (sentry__atomic_fetch(&g_logs_state.batching_stop) != 0) { + // Check if we should still be running + if (sentry__atomic_fetch(&g_logs_state.thread_state) + != SENTRY_LOGS_THREAD_RUNNING) { break; } @@ -251,6 +287,7 @@ batching_thread_func(void *data) sentry__mutex_unlock(&task_lock); sentry__mutex_free(&task_lock); + SENTRY_DEBUG("batching thread exiting"); return 0; } @@ -733,6 +770,12 @@ sentry_log_fatal(const char *message, ...) void sentry__logs_startup(void) { + // Mark thread as starting before actually spawning so thread can transition + // to RUNNING. This prevents shutdown from thinking the thread was never + // started if it races with the thread's initialization. + sentry__atomic_store( + &g_logs_state.thread_state, (long)SENTRY_LOGS_THREAD_STARTING); + sentry__cond_init(&g_logs_state.request_flush); sentry__thread_init(&g_logs_state.batching_thread); @@ -741,6 +784,11 @@ sentry__logs_startup(void) if (spawn_result == 1) { SENTRY_ERROR("Failed to start batching thread"); + // Failed to spawn, reset to STOPPED + // Note: condition variable doesn't need explicit cleanup for static + // storage (pthread_cond_t on POSIX and CONDITION_VARIABLE on Windows) + sentry__atomic_store( + &g_logs_state.thread_state, (long)SENTRY_LOGS_THREAD_STOPPED); } } @@ -750,12 +798,23 @@ sentry__logs_shutdown(uint64_t timeout) (void)timeout; SENTRY_DEBUG("shutting down logs system"); - // Signal the batching thread to stop running - if (sentry__atomic_store(&g_logs_state.batching_stop, 1) != 0) { - SENTRY_DEBUG("preventing double shutdown of logs system"); + // Atomically transition to STOPPED and get the previous state + // This handles the race where thread might be in STARTING state: + // - If thread's CAS hasn't run yet: CAS will fail, thread exits cleanly + // - If thread already transitioned to RUNNING: normal shutdown path + const long old_state = sentry__atomic_store( + &g_logs_state.thread_state, (long)SENTRY_LOGS_THREAD_STOPPED); + + // If thread was never started, nothing to do + if (old_state == SENTRY_LOGS_THREAD_STOPPED) { + SENTRY_DEBUG("logs thread was not started, skipping shutdown"); return; } + + // Thread was started (either STARTING or RUNNING), signal it to stop sentry__cond_wake(&g_logs_state.request_flush); + + // Always join the thread to avoid leaks sentry__thread_join(g_logs_state.batching_thread); // Perform final flush to ensure any remaining logs are sent @@ -765,3 +824,30 @@ sentry__logs_shutdown(uint64_t timeout) SENTRY_DEBUG("logs system shutdown complete"); } + +#ifdef SENTRY_UNITTEST +/** + * Wait for the logs batching thread to be ready. + * This is a test-only helper to avoid race conditions in tests. + */ +void +sentry__logs_wait_for_thread_startup(void) +{ + const int max_wait_ms = 1000; + const int check_interval_ms = 10; + const int max_attempts = max_wait_ms / check_interval_ms; + + for (int i = 0; i < max_attempts; i++) { + const long state = sentry__atomic_fetch(&g_logs_state.thread_state); + if (state == SENTRY_LOGS_THREAD_RUNNING) { + SENTRY_DEBUGF( + "logs thread ready after %d ms", i * check_interval_ms); + return; + } + sleep_ms(check_interval_ms); + } + + SENTRY_WARNF( + "logs thread failed to start within %d ms timeout", max_wait_ms); +} +#endif diff --git a/src/sentry_logs.h b/src/sentry_logs.h index b23e65263..ce0f39074 100644 --- a/src/sentry_logs.h +++ b/src/sentry_logs.h @@ -19,6 +19,12 @@ void sentry__logs_shutdown(uint64_t timeout); #ifdef SENTRY_UNITTEST int populate_message_parameters( sentry_value_t attributes, const char *message, va_list args); + +/** + * Wait for the logs batching thread to be ready. + * This is a test-only helper to avoid race conditions in tests. + */ +void sentry__logs_wait_for_thread_startup(void); #endif #endif diff --git a/src/sentry_sync.h b/src/sentry_sync.h index dfbe59e14..c19974434 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -384,6 +384,34 @@ sentry__atomic_fetch(volatile long *val) return sentry__atomic_fetch_and_add(val, 0); } +/** + * Compare and swap: atomically compare *val with expected, and if equal, + * set *val to desired. Returns true if the swap occurred. + * + * Uses sequential consistency (ATOMIC_SEQ_CST / InterlockedCompareExchange) + * to ensure all memory operations are visible across threads. This is + * appropriate for thread synchronization and state machine transitions. + * + * Note: The ABA problem (where a value changes A->B->A between reads) is not + * a concern for simple integer-based state machines with monotonic transitions. + */ +static inline bool +sentry__atomic_compare_swap(volatile long *val, long expected, long desired) +{ +#ifdef SENTRY_PLATFORM_WINDOWS +# if SIZEOF_LONG == 8 + return InterlockedCompareExchange64((LONG64 *)val, desired, expected) + == expected; +# else + return InterlockedCompareExchange((LONG *)val, desired, expected) + == expected; +# endif +#else + return __atomic_compare_exchange_n( + val, &expected, desired, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); +#endif +} + struct sentry_bgworker_s; typedef struct sentry_bgworker_s sentry_bgworker_t; diff --git a/tests/unit/test_logs.c b/tests/unit/test_logs.c index 9e2d89dbc..ad773ef64 100644 --- a/tests/unit/test_logs.c +++ b/tests/unit/test_logs.c @@ -15,15 +15,19 @@ static void validate_logs_envelope(sentry_envelope_t *envelope, void *data) { uint64_t *called = data; - *called += 1; // Verify we have at least one envelope item TEST_CHECK(sentry__envelope_get_item_count(envelope) > 0); - // Get the first item and check it's a logs item + // Get the first item and check its type const sentry_envelope_item_t *item = sentry__envelope_get_item(envelope, 0); sentry_value_t type_header = sentry__envelope_item_get_header(item, "type"); - TEST_CHECK_STRING_EQUAL(sentry_value_as_string(type_header), "log"); + const char *type = sentry_value_as_string(type_header); + + // Only validate and count log envelopes, skip others (e.g., session) + if (strcmp(type, "log") == 0) { + *called += 1; + } sentry_envelope_free(envelope); } @@ -42,8 +46,7 @@ SENTRY_TEST(basic_logging_functionality) sentry_options_set_transport(options, transport); sentry_init(options); - // TODO if we don't sleep, log timer_task might not start in time to flush - sleep_ms(20); + sentry__logs_wait_for_thread_startup(); // These should not crash and should respect the enable_logs option sentry_log_trace("Trace message"); @@ -51,7 +54,7 @@ SENTRY_TEST(basic_logging_functionality) sentry_log_info("Info message"); sentry_log_warn("Warning message"); sentry_log_error("Error message"); - // sleep to finish flush of the first 5, otherwise failed enqueue + // Sleep to allow first batch to flush (testing batch timing behavior) sleep_ms(20); sentry_log_fatal("Fatal message"); sentry_close(); @@ -98,6 +101,7 @@ SENTRY_TEST(formatted_log_messages) sentry_options_set_transport(options, transport); sentry_init(options); + sentry__logs_wait_for_thread_startup(); // Test format specifiers sentry_log_info("String: %s, Integer: %d, Float: %.2f", "test", 42, 3.14); From 1af38cceda2e71faaf1f9c863b7bfd4ed547edcc Mon Sep 17 00:00:00 2001 From: Mischan Toosarani-Hausberger Date: Wed, 1 Oct 2025 19:05:53 +0200 Subject: [PATCH 26/27] fix(win): make symbolication and modulefinder independent of the system ANSI code page. (#1389) * fix(win): make symbolication independent of the system ANSI code page. * allow `NULL` module paths and symbol names. * add an integration test that runs the example from a cyrillic directory and validates the package paths * resolve relative paths + clean up subdir * remove the assertion that a frame must have a function * only assert on the frame_package being a file if it exists... ...however, no longer assert that a frame_package exists. * isolate package assertions to new test * don't conflate checking any function/package with checking package file validity * also adapt the windows modulefinder to be independent system ACP. The szExePath generated for actual UTF-8 paths was already filled with mojibake :-) so LoadLibrary couldn't find any local modules. This is actually connected to the symbolication: * in the server, if a module was found, the backend would assign packages to frames based on the instruction address and the module address range * if the module couldn't be found, as was the case previously, it had to use the frame package provided So, now we fixed both and they should overlap. * update the CHANGELOG * explicitly specify `PSAPI_VERSION` because we only want to link kernel32 * check string_from_wstr return values * use a heap-allocated 32K buffer for module paths * use a heap-allocated 32K buffer for symbol paths * move allocation into wrapping if * format after webui edit --- CHANGELOG.md | 6 ++ .../sentry_modulefinder_windows.c | 73 ++++++++++++++++--- src/symbolizer/sentry_symbolizer_windows.c | 50 +++++++++---- tests/assertions.py | 16 +++- tests/test_integration_stdout.py | 56 ++++++++++++++ 5 files changed, 175 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 891bd6cb9..d25cf2c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Windows: Make symbolication and the modulefinder independent of the system ANSI code page. ([#1389](https://github.com/getsentry/sentry-native/pull/1389)) + ## 0.11.1 **Features**: diff --git a/src/modulefinder/sentry_modulefinder_windows.c b/src/modulefinder/sentry_modulefinder_windows.c index 7c747c562..49d1eab84 100644 --- a/src/modulefinder/sentry_modulefinder_windows.c +++ b/src/modulefinder/sentry_modulefinder_windows.c @@ -6,10 +6,10 @@ #ifndef SENTRY_PLATFORM_XBOX # include -#else -# include #endif -#include +#define PSAPI_VERSION 2 +#include +#include static bool g_initialized = false; static sentry_mutex_t g_mutex = SENTRY__MUTEX_INIT; @@ -17,6 +17,10 @@ static sentry_value_t g_modules = { 0 }; #define CV_SIGNATURE 0x53445352 +// follow the maximum path length documented here: +// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +#define MAX_PATH_BUFFER_SIZE 32768 + struct CodeViewRecord70 { uint32_t signature; GUID pdb_signature; @@ -92,6 +96,35 @@ extract_pdb_info(uintptr_t module_addr, sentry_value_t module) } } +static void +log_library_load_error(const wchar_t *module_filename_w) +{ + const DWORD ec = GetLastError(); + LPWSTR msg_w = NULL; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, ec, 0, (LPWSTR)&msg_w, 0, NULL); + char *msg = sentry__string_from_wstr(msg_w); + char *module_filename = sentry__string_from_wstr(module_filename_w); + if (!msg || !module_filename) { + if (module_filename) { + sentry_free(module_filename); + } + if (msg) { + sentry_free(msg); + } + if (msg_w) { + LocalFree(msg_w); + } + return; + } + SENTRY_ERRORF("LoadLibraryExW failed (%lu): %s (\"%s\")\n", ec, + msg ? msg : "(no message)", module_filename); + sentry_free(module_filename); + sentry_free(msg); + LocalFree(msg_w); +} + static void load_modules(void) { @@ -101,14 +134,32 @@ load_modules(void) MODULEENTRY32W module = { 0 }; module.dwSize = sizeof(MODULEENTRY32W); g_modules = sentry_value_new_list(); + wchar_t *module_filename_w = NULL; - if (Module32FirstW(snapshot, &module)) { + if (Module32FirstW(snapshot, &module) + && (module_filename_w + = sentry_malloc(sizeof(wchar_t) * MAX_PATH_BUFFER_SIZE))) { do { - HMODULE handle = LoadLibraryExW( - module.szExePath, NULL, LOAD_LIBRARY_AS_DATAFILE); + HMODULE module_handle = NULL; + if (GetModuleFileNameExW(GetCurrentProcess(), module.hModule, + module_filename_w, MAX_PATH_BUFFER_SIZE)) { + module_handle = LoadLibraryExW( + module_filename_w, NULL, LOAD_LIBRARY_AS_DATAFILE); + } else { + char *module_name = sentry__string_from_wstr(module.szModule); + if (module_name) { + SENTRY_ERRORF( + "Failed to get module filename for %s", module_name); + sentry_free(module_name); + } + continue; + } + if (!module_handle) { + log_library_load_error(module_filename_w); + continue; + } MEMORY_BASIC_INFORMATION vmem_info = { 0 }; - if (handle - && sizeof(vmem_info) + if (sizeof(vmem_info) == VirtualQuery( module.modBaseAddr, &vmem_info, sizeof(vmem_info)) && vmem_info.State == MEM_COMMIT) { @@ -120,12 +171,14 @@ load_modules(void) sentry_value_set_by_key(rv, "image_size", sentry_value_new_int32((int32_t)module.modBaseSize)); sentry_value_set_by_key(rv, "code_file", - sentry__value_new_string_from_wstr(module.szExePath)); + sentry__value_new_string_from_wstr(module_filename_w)); extract_pdb_info((uintptr_t)module.modBaseAddr, rv); sentry_value_append(g_modules, rv); } - FreeLibrary(handle); + FreeLibrary(module_handle); } while (Module32NextW(snapshot, &module)); + + sentry_free(module_filename_w); } CloseHandle(snapshot); diff --git a/src/symbolizer/sentry_symbolizer_windows.c b/src/symbolizer/sentry_symbolizer_windows.c index deeedd46c..fb447e137 100644 --- a/src/symbolizer/sentry_symbolizer_windows.c +++ b/src/symbolizer/sentry_symbolizer_windows.c @@ -1,4 +1,5 @@ #include "sentry_boot.h" +#include "sentry_string.h" #include "sentry_symbolizer.h" #include "sentry_windows_dbghelp.h" @@ -6,7 +7,9 @@ #include #include -#define MAX_SYM 1024 +// follow the maximum path length documented here: +// https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation +#define MAX_PATH_BUFFER_SIZE 32768 bool sentry__symbolize( @@ -17,29 +20,46 @@ sentry__symbolize( (void)func; (void)addr; #else + if (!addr || !func) { + return false; + } HANDLE proc = sentry__init_dbghelp(); + size_t symbol_info_size + = sizeof(SYMBOL_INFOW) + MAX_SYM_NAME * sizeof(WCHAR); + SYMBOL_INFOW *symbol_info = _alloca(symbol_info_size); + memset(symbol_info, 0, symbol_info_size); + symbol_info->MaxNameLen = MAX_SYM_NAME; + symbol_info->SizeOfStruct = sizeof(SYMBOL_INFOW); - SYMBOL_INFO *sym = (SYMBOL_INFO *)_alloca(sizeof(SYMBOL_INFO) + MAX_SYM); - memset(sym, 0, sizeof(SYMBOL_INFO) + MAX_SYM); - sym->MaxNameLen = MAX_SYM; - sym->SizeOfStruct = sizeof(SYMBOL_INFO); + if (!SymFromAddrW(proc, (uintptr_t)addr, NULL, symbol_info)) { + return false; + } - if (!SymFromAddr(proc, (DWORD64)addr, 0, sym)) { + wchar_t *mod_path_w = sentry_malloc(sizeof(wchar_t) * MAX_PATH_BUFFER_SIZE); + if (!mod_path_w) { + return false; + } + const DWORD n = GetModuleFileNameW((HMODULE)(uintptr_t)symbol_info->ModBase, + mod_path_w, MAX_PATH_BUFFER_SIZE); + if (n == 0 || n >= MAX_PATH_BUFFER_SIZE) { + sentry_free(mod_path_w); return false; } - char mod_name[MAX_PATH]; - GetModuleFileNameA( - (HMODULE)(size_t)sym->ModBase, mod_name, sizeof(mod_name)); + char *mod_path = sentry__string_from_wstr(mod_path_w); + char *symbol_name = sentry__string_from_wstr(symbol_info->Name); - sentry_frame_info_t frame_info; - memset(&frame_info, 0, sizeof(sentry_frame_info_t)); - frame_info.load_addr = (void *)(size_t)sym->ModBase; + sentry_frame_info_t frame_info = { 0 }; + frame_info.load_addr = (void *)(uintptr_t)symbol_info->ModBase; frame_info.instruction_addr = addr; - frame_info.symbol_addr = (void *)(size_t)sym->Address; - frame_info.symbol = sym->Name; - frame_info.object_name = mod_name; + frame_info.symbol_addr = (void *)(uintptr_t)symbol_info->Address; + frame_info.symbol = symbol_name; + frame_info.object_name = mod_path; func(&frame_info, data); + + sentry_free(mod_path); + sentry_free(symbol_name); + sentry_free(mod_path_w); #endif // SENTRY_PLATFORM_XBOX return true; diff --git a/tests/assertions.py b/tests/assertions.py index f6db545d6..711430d83 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -6,6 +6,7 @@ import sys from dataclasses import dataclass from datetime import datetime, UTC +from pathlib import Path import msgpack @@ -167,7 +168,9 @@ def assert_event_meta( ) -def assert_stacktrace(envelope, inside_exception=False, check_size=True): +def assert_stacktrace( + envelope, inside_exception=False, check_size=True, check_package=False +): event = envelope.get_event() parent = event["exception"] if inside_exception else event["threads"] @@ -182,6 +185,17 @@ def assert_stacktrace(envelope, inside_exception=False, check_size=True): for frame in frames ) + if check_package: + for frame in frames: + frame_package = frame.get("package") + if frame_package is not None: + frame_package_path = Path(frame_package) + # only assert on absolute paths, since letting pathlib resolve relative paths is cheating + if frame_package_path.is_absolute(): + assert ( + frame_package_path.is_file() + ), f"package is not a valid file path: '{frame_package}'" + def assert_breadcrumb_inner(breadcrumbs, message="debug crumb"): expected = { diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 7236d46b0..6c67cba79 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -1,7 +1,9 @@ import os +import shutil import subprocess import sys import time +from pathlib import Path import pytest @@ -18,6 +20,7 @@ assert_no_before_send, assert_crash_timestamp, assert_breakpad_crash, + assert_exception, ) from .conditions import has_breakpad, has_files @@ -46,6 +49,59 @@ def test_capture_stdout(cmake): assert_event(envelope) +def copy_except(src: Path, dst: Path, exceptions: list[str] = None) -> None: + """ + Recursively copy everything from src to dst, except for entries whose + names are in `exceptions`. + """ + exceptions = set(exceptions or []) + + dst.mkdir(parents=True, exist_ok=True) + + for entry in src.iterdir(): + if entry.name in exceptions: + continue + + dest = dst / entry.name + if entry.is_dir(): + shutil.copytree(entry, dest, symlinks=True) + else: + shutil.copy2(entry, dest) + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_capture_exception_from_utf8_path_stdout(cmake): + """ + This test verifies that we can handle symbolication from an utf-8 path. + """ + tmp_path = cmake( + ["sentry_example"], + { + "SENTRY_BACKEND": "none", + "SENTRY_TRANSPORT": "none", + }, + ) + # create a cyrillic subdirectory in tmp_path and copy tmp_path into it + cwd = tmp_path / "кириллица-тест" + cwd.mkdir() + copy_except(tmp_path, cwd, exceptions=["кириллица-тест"]) + + output = check_output( + cwd, + "sentry_example", + ["stdout", "capture-exception", "add-stacktrace"], + ) + envelope = Envelope.deserialize(output) + + assert_meta(envelope) + assert_breadcrumb(envelope) + assert_stacktrace(envelope, inside_exception=True, check_package=True) + assert_exception(envelope) + + # delete the cyrillic directory, but only after we asserted on stack frame packages being files + shutil.rmtree(cwd) + + def test_dynamic_sdk_name_override(cmake): tmp_path = cmake( ["sentry_example"], From 027459265ab94de340a5f59b767248652640d1e6 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 1 Oct 2025 17:09:37 +0000 Subject: [PATCH 27/27] release: 0.11.2 --- CHANGELOG.md | 2 +- include/sentry.h | 2 +- ndk/gradle.properties | 2 +- tests/assertions.py | 4 ++-- tests/test_integration_http.py | 2 +- tests/win_utils.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25cf2c63..f01468cad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.11.2 **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 1fec4805a..5c76224b8 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -78,7 +78,7 @@ extern "C" { # define SENTRY_SDK_NAME "sentry.native" # endif #endif -#define SENTRY_SDK_VERSION "0.11.1" +#define SENTRY_SDK_VERSION "0.11.2" #define SENTRY_SDK_USER_AGENT SENTRY_SDK_NAME "/" SENTRY_SDK_VERSION /* marks a function as part of the sentry API */ diff --git a/ndk/gradle.properties b/ndk/gradle.properties index 647c2e07a..713c4d15c 100644 --- a/ndk/gradle.properties +++ b/ndk/gradle.properties @@ -7,7 +7,7 @@ org.gradle.parallel=true android.useAndroidX=true # Release information, used for maven publishing -versionName=0.11.1 +versionName=0.11.2 # disable renderscript, it's enabled by default android.defaults.buildfeatures.renderscript=false diff --git a/tests/assertions.py b/tests/assertions.py index 711430d83..ca9f7c902 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -106,9 +106,9 @@ def assert_event_meta( } expected_sdk = { "name": "sentry.native", - "version": "0.11.1", + "version": "0.11.2", "packages": [ - {"name": "github:getsentry/sentry-native", "version": "0.11.1"}, + {"name": "github:getsentry/sentry-native", "version": "0.11.2"}, ], } if is_android: diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 88dac3e4c..656688aa9 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -45,7 +45,7 @@ # fmt: off auth_header = ( - "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.11.1" + "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.11.2" ) # fmt: on diff --git a/tests/win_utils.py b/tests/win_utils.py index 84041f9f9..f4c1eee8d 100644 --- a/tests/win_utils.py +++ b/tests/win_utils.py @@ -1,7 +1,7 @@ import pathlib import win32api -sentry_version = "0.11.1" +sentry_version = "0.11.2" def check_binary_version(binary_path: pathlib.Path):