diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c748df8..833b4269b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,14 +49,14 @@ jobs: # RUN_ANALYZER: gcc - name: Linux (clang + asan + llvm-cov) os: ubuntu-22.04 - CC: clang-14 - CXX: clang++-14 + CC: clang-15 + CXX: clang++-15 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: asan,llvm-cov - name: Linux (clang + kcov) os: ubuntu-22.04 - CC: clang-14 - CXX: clang++-14 + CC: clang-15 + CXX: clang++-15 ERROR_ON_WARNINGS: 1 RUN_ANALYZER: kcov - name: Linux (gcc + code-checker + valgrind) @@ -137,6 +137,11 @@ jobs: sudo apt update sudo apt install cmake gcc-7-multilib g++-7-multilib zlib1g-dev:i386 libssl-dev:i386 libcurl4-openssl-dev:i386 + # https://github.com/actions/runner-images/issues/9491 + - name: Decrease vm.mmap_rnd_bit to prevent ASLR ASAN issues + if: ${{ runner.os == 'Linux' && contains(env['RUN_ANALYZER'], 'asan') }} + run: sudo sysctl vm.mmap_rnd_bits=28 + - name: Installing CodeChecker if: ${{ contains(env['RUN_ANALYZER'], 'code-checker') }} run: sudo snap install codechecker --classic @@ -153,6 +158,11 @@ jobs: MINGW_ASM_MASM_COMPILER: ${{ matrix.MINGW_ASM_MASM_COMPILER }} run: . "scripts\install-llvm-mingw.ps1" + - name: Set up zlib for Windows + if: ${{ runner.os == 'Windows' }} + shell: powershell + run: . "scripts\install-zlib.ps1" + - name: Installing Android SDK Dependencies if: ${{ env['ANDROID_API'] }} run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index dcaaf1ebf..12bb6c022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 0.7.2 + +**Features**: + +- Add optional Gzip transport compression via build option `SENTRY_TRANSPORT_COMPRESSION`. Requires system `zlib`. ([#954](https://github.com/getsentry/sentry-native/pull/954)) +- Enable automatic MIME detection of attachments sent with crash-reports from the `crashpad_handler`. ([#973](https://github.com/getsentry/sentry-native/pull/973), [crashpad#98](https://github.com/getsentry/crashpad/pull/98)) + +**Fixes**: + +- Fix the Linux build when targeting RISC-V. ([#972](https://github.com/getsentry/sentry-native/pull/972)) + +**Thank you**: + +- [@Strive-Sun](https://github.com/Strive-Sun) +- [@jwinarske](https://github.com/jwinarske) + +## 0.7.1 + +**Features**: + +- Add user feedback capability to the Native SDK. ([#966](https://github.com/getsentry/sentry-native/pull/966)) + +**Internal**: + +- Remove the `CRASHPAD_WER_ENABLED` build flag. The WER module is now built for all supported Windows targets, and registration is conditional on runtime Windows version checks. ([#950](https://github.com/getsentry/sentry-native/pull/950), [crashpad#96](https://github.com/getsentry/crashpad/pull/96)) + +**Docs**: + +- Add usage of the breadcrumb `data` property to the example. [#951](https://github.com/getsentry/sentry-native/pull/951) + ## 0.7.0 **Breaking changes**: @@ -25,6 +55,7 @@ Features, fixes and improvements in this release have been contributed by: - [@compnerd](https://github.com/compnerd) - [@stima](https://github.com/stima) +- [@hyp](https://github.com/hyp) ## 0.6.7 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e460cded..3996e83db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ endif() option(SENTRY_PIC "Build sentry (and dependent) libraries as position independent libraries" ON) +option(SENTRY_TRANSPORT_COMPRESSION "Enable transport gzip compression" OFF) + option(SENTRY_BUILD_TESTS "Build sentry-native tests" "${SENTRY_MAIN_PROJECT}") option(SENTRY_BUILD_EXAMPLES "Build sentry-native example(s)" "${SENTRY_MAIN_PROJECT}") @@ -278,17 +280,20 @@ if(SENTRY_TRANSPORT_CURL) find_package(CURL REQUIRED COMPONENTS AsynchDNS) endif() - if(TARGET CURL::libcurl) # Only available in cmake 3.12+ - target_link_libraries(sentry PRIVATE CURL::libcurl) - else() - # Needed for cmake < 3.12 support (cmake 3.12 introduced the target CURL::libcurl) - target_include_directories(sentry PRIVATE ${CURL_INCLUDE_DIR}) - # The exported sentry target must not contain any path of the build machine, therefore use generator expressions - string(REPLACE ";" "$" GENEX_CURL_LIBRARIES "${CURL_LIBRARIES}") - string(REPLACE ";" "$" GENEX_CURL_COMPILE_DEFINITIONS "${CURL_COMPILE_DEFINITIONS}") - target_link_libraries(sentry PRIVATE $) - target_compile_definitions(sentry PRIVATE $) + target_link_libraries(sentry PRIVATE CURL::libcurl) +endif() + +if(SENTRY_TRANSPORT_COMPRESSION) + if(NOT ZLIB_FOUND) + find_package(ZLIB REQUIRED) endif() + + if(SENTRY_BACKEND_CRASHPAD) + set(CRASHPAD_ZLIB_SYSTEM ON CACHE BOOL "Force CRASHPAD_ZLIB_SYSTEM when enabling transport compression" FORCE) + endif() + + target_link_libraries(sentry PRIVATE ZLIB::ZLIB) + target_compile_definitions(sentry PRIVATE SENTRY_TRANSPORT_COMPRESSION) endif() set_property(TARGET sentry PROPERTY C_VISIBILITY_PRESET hidden) @@ -413,9 +418,6 @@ if(SENTRY_WITH_LIBUNWINDSTACK) endif() if(SENTRY_BACKEND_CRASHPAD) - # FIXME: required for cmake 3.12 and lower: - # - NEW behavior lets normal variable override option - cmake_policy(SET CMP0077 NEW) if(SENTRY_BUILD_SHARED_LIBS) set(CRASHPAD_ENABLE_INSTALL OFF CACHE BOOL "Enable crashpad installation" FORCE) else() @@ -423,7 +425,7 @@ if(SENTRY_BACKEND_CRASHPAD) endif() add_subdirectory(external/crashpad crashpad_build) - if(CRASHPAD_WER_ENABLED) + if(WIN32) add_dependencies(sentry crashpad::wer) endif() @@ -438,9 +440,7 @@ if(SENTRY_BACKEND_CRASHPAD) 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>") - if(CRASHPAD_WER_ENABLED) - set_property(TARGET crashpad_wer PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") - endif() + set_property(TARGET crashpad_wer PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET crashpad_zlib PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set_property(TARGET mini_chromium PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() @@ -457,9 +457,7 @@ if(SENTRY_BACKEND_CRASHPAD) set_target_properties(crashpad_util PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(crashpad_zlib PROPERTIES FOLDER ${SENTRY_FOLDER}) set_target_properties(mini_chromium PROPERTIES FOLDER ${SENTRY_FOLDER}) - if(CRASHPAD_WER_ENABLED) - set_target_properties(crashpad_wer PROPERTIES FOLDER ${SENTRY_FOLDER}) - endif() + set_target_properties(crashpad_wer PROPERTIES FOLDER ${SENTRY_FOLDER}) endif() target_link_libraries(sentry PRIVATE @@ -472,16 +470,10 @@ if(SENTRY_BACKEND_CRASHPAD) if(WIN32 AND MSVC) sentry_install(FILES $ DESTINATION "${CMAKE_INSTALL_BINDIR}" OPTIONAL) - if (CRASHPAD_WER_ENABLED) - sentry_install(FILES $ - DESTINATION "${CMAKE_INSTALL_BINDIR}" OPTIONAL) - endif() + sentry_install(FILES $ + DESTINATION "${CMAKE_INSTALL_BINDIR}" OPTIONAL) endif() add_dependencies(sentry crashpad::handler) - - if(CRASHPAD_WER_ENABLED) - add_compile_definitions(CRASHPAD_WER_ENABLED) - endif() elseif(SENTRY_BACKEND_BREAKPAD) option(SENTRY_BREAKPAD_SYSTEM "Use system breakpad" OFF) if(SENTRY_BREAKPAD_SYSTEM) @@ -575,10 +567,9 @@ if(SENTRY_BUILD_EXAMPLES) if(MSVC) target_compile_options(sentry_example PRIVATE $) - if(CRASHPAD_WER_ENABLED) - # to test handling SEH by-passing exceptions we need to enable the control flow guard - target_compile_options(sentry_example PRIVATE $) - endif() + + # to test handling SEH by-passing exceptions we need to enable the control flow guard + target_compile_options(sentry_example PRIVATE $) endif() # set static runtime if enabled @@ -604,4 +595,4 @@ endif() if(SENTRY_BUILD_SHARED_LIBS) target_link_libraries(sentry PRIVATE "$<$,$>:-Wl,--build-id=sha1,--version-script=${PROJECT_SOURCE_DIR}/src/exports.map>") -endif() \ No newline at end of file +endif() diff --git a/README.md b/README.md index 3c809b50c..2577e82c6 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ applications, optimized for C and C++. Sentry allows to add tags, breadcrumbs and arbitrary custom context to enrich error reports. Supports Sentry _20.6.0_ and later. +### Note + +Using the `sentry-native` SDK in a standalone use case is currently an experimental feature. The SDK’s primary function is to fuel our other SDKs, like [`sentry-java`](https://github.com/getsentry/sentry-java) or [`sentry-unreal`](https://github.com/getsentry/sentry-unreal). Support from our side is best effort and we do what we can to respond to issues in a timely fashion, but please understand if we won’t be able to address your issues or feature suggestions. + ## Resources - [SDK Documentation](https://docs.sentry.io/platforms/native/) @@ -239,7 +243,7 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. Builds the Qt integration, which turns Qt log messages into breadcrumbs. - `SENTRY_BREAKPAD_SYSTEM` (Default: OFF): - This instructs the build system to use system-installed breakpad libraries instead of using the in-tree version. + This instructs the build system to use system-installed breakpad libraries instead of using the in-tree version. | Feature | Windows | macOS | Linux | Android | iOS | | ---------- | ------- | ----- | ----- | ------- | --- | diff --git a/examples/example.c b/examples/example.c index 7e1b71b0c..36a84bb44 100644 --- a/examples/example.c +++ b/examples/example.c @@ -9,17 +9,21 @@ #include #include #include + #ifdef NDEBUG # undef NDEBUG #endif + #include #ifdef SENTRY_PLATFORM_WINDOWS # include # define sleep_s(SECONDS) Sleep((SECONDS)*1000) #else + # include # include + # define sleep_s(SECONDS) sleep(SECONDS) #endif @@ -93,7 +97,9 @@ has_arg(int argc, char **argv, const char *arg) return false; } -#ifdef CRASHPAD_WER_ENABLED +#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \ + && !defined(__MINGW64__) + int call_rffe_many_times() { @@ -138,7 +144,7 @@ trigger_fastfail_crash() __fastfail(77); } -#endif // CRASHPAD_WER_ENABLED +#endif #ifdef SENTRY_PLATFORM_AIX // AIX has a null page mapped to the bottom of memory, which means null derefs @@ -258,6 +264,21 @@ main(int argc, char **argv) debug_crumb, "category", sentry_value_new_string("example!")); sentry_value_set_by_key( debug_crumb, "level", sentry_value_new_string("debug")); + + // extend the `http` crumb with (optional) data properties as documented + // here: + // https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types + sentry_value_t http_data = sentry_value_new_object(); + sentry_value_set_by_key(http_data, "url", + sentry_value_new_string("https://example.com/api/1.0/users")); + sentry_value_set_by_key( + http_data, "method", sentry_value_new_string("GET")); + sentry_value_set_by_key( + http_data, "status_code", sentry_value_new_int32(200)); + sentry_value_set_by_key( + http_data, "reason", sentry_value_new_string("OK")); + sentry_value_set_by_key(debug_crumb, "data", http_data); + sentry_add_breadcrumb(debug_crumb); sentry_value_t nl_crumb @@ -301,7 +322,8 @@ main(int argc, char **argv) if (has_arg(argc, argv, "crash")) { trigger_crash(); } -#ifdef CRASHPAD_WER_ENABLED +#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \ + && !defined(__MINGW64__) if (has_arg(argc, argv, "fastfail")) { trigger_fastfail_crash(); } @@ -343,6 +365,16 @@ main(int argc, char **argv) sentry_capture_event(event); } + if (has_arg(argc, argv, "capture-user-feedback")) { + sentry_value_t event = sentry_value_new_message_event( + SENTRY_LEVEL_INFO, "my-logger", "Hello user feedback!"); + sentry_uuid_t event_id = sentry_capture_event(event); + + sentry_value_t user_feedback = sentry_value_new_user_feedback( + &event_id, "some-name", "some-email", "some-comment"); + + sentry_capture_user_feedback(user_feedback); + } if (has_arg(argc, argv, "capture-transaction")) { sentry_transaction_context_t *tx_ctx diff --git a/external/crashpad b/external/crashpad index 89991e991..3f8bcc49b 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 89991e9910bc4c0893e45c8cfad0bdd31cc25a5c +Subproject commit 3f8bcc49bbfff0662bcfa59e831b65a5d050c9f0 diff --git a/include/sentry.h b/include/sentry.h index bba0dd0e7..43203151a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -30,7 +30,7 @@ extern "C" { # define SENTRY_SDK_NAME "sentry.native" # endif #endif -#define SENTRY_SDK_VERSION "0.7.0" +#define SENTRY_SDK_VERSION "0.7.2" #define SENTRY_SDK_USER_AGENT SENTRY_SDK_NAME "/" SENTRY_SDK_VERSION /* common platform detection */ @@ -1068,6 +1068,11 @@ typedef void (*sentry_logger_function_t)( * Sets the sentry-native logger function. * * Used for logging debug events when the `debug` option is set to true. + * + * Note: Multiple threads may invoke your `func`. If you plan to mutate any data + * inside the `userdata` argument after initialization, you must ensure proper + * synchronization inside the logger function. + * */ SENTRY_API void sentry_options_set_logger( sentry_options_t *opts, sentry_logger_function_t func, void *userdata); @@ -1888,6 +1893,27 @@ SENTRY_EXPERIMENTAL_API void sentry_transaction_set_name( SENTRY_EXPERIMENTAL_API void sentry_transaction_set_name_n( sentry_transaction_t *transaction, const char *name, size_t name_len); +/** + * Creates a new User Feedback with a specific name, email and comments. + * + * See https://develop.sentry.dev/sdk/envelopes/#user-feedback + * + * User Feedback has to be associated with a specific event that has been + * sent to Sentry earlier. + */ +SENTRY_API sentry_value_t sentry_value_new_user_feedback( + const sentry_uuid_t *uuid, const char *name, const char *email, + const char *comments); +SENTRY_API sentry_value_t sentry_value_new_user_feedback_n( + const sentry_uuid_t *uuid, const char *name, size_t name_len, + const char *email, size_t email_len, const char *comments, + size_t comments_len); + +/** + * Captures a manually created User Feedback and sends it to Sentry. + */ +SENTRY_API void sentry_capture_user_feedback(sentry_value_t user_feedback); + /** * The status of a Span or Transaction. * diff --git a/scripts/install-llvm-mingw.ps1 b/scripts/install-llvm-mingw.ps1 index 835c884f6..2af029272 100755 --- a/scripts/install-llvm-mingw.ps1 +++ b/scripts/install-llvm-mingw.ps1 @@ -14,7 +14,8 @@ $CurlArguments = '-s', '-Lf', '-o', "${LLVM_MINGW_DL_PATH}", "${LLVM_MINGW_DL_UR $dl_zip_hash = Get-FileHash -LiteralPath "${LLVM_MINGW_DL_PATH}" -Algorithm SHA512 if ($dl_zip_hash.Hash -eq $LLVM_MINGW_DL_SHA512) { Write-Host "Successfully downloaded LLVM-mingw .zip" -} Else { +} +Else { Write-Error "The downloaded LLVM-mingw zip hash '$($dl_zip_hash.Hash)' does not match the expected hash: '$LLVM_MINGW_DL_SHA512'" } @@ -25,10 +26,10 @@ New-Item -ItemType Directory -Force -Path "${LLVM_MINGW_INSTALL_PATH}" Expand-Archive -LiteralPath "${LLVM_MINGW_DL_PATH}" -DestinationPath "${LLVM_MINGW_INSTALL_PATH}" # Export the LLVM-mingw install path $LLVM_MINGW_INSTALL_PATH = "${LLVM_MINGW_INSTALL_PATH}\${LLVM_MINGW_PKG}" -echo "LLVM_MINGW_INSTALL_PATH=${LLVM_MINGW_INSTALL_PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append +"LLVM_MINGW_INSTALL_PATH=${LLVM_MINGW_INSTALL_PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # Prepend bin path to the system PATH -echo "Path to LLVM-mingw bin folder: ${LLVM_MINGW_INSTALL_PATH}\bin" -echo "${LLVM_MINGW_INSTALL_PATH}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append +Write-Host "Path to LLVM-mingw bin folder: ${LLVM_MINGW_INSTALL_PATH}\bin" +"${LLVM_MINGW_INSTALL_PATH}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append # Download ninja-build $NINJA_DL_URL = "https://github.com/ninja-build/ninja/releases/download/v1.11.1/ninja-win.zip" @@ -39,7 +40,8 @@ $CurlArguments = '-s', '-Lf', '-o', "${NINJA_DL_PATH}", "${NINJA_DL_URL}" $ninja_zip_hash = Get-FileHash -LiteralPath "${NINJA_DL_PATH}" -Algorithm SHA512 if ($ninja_zip_hash.Hash -eq $NINJA_DL_SHA512) { Write-Host "Successfully downloaded Ninja-Build .zip" -} Else { +} +Else { Write-Error "The downloaded Ninja-build zip hash '$($ninja_zip_hash.Hash)' does not match the expected hash: '$NINJA_DL_SHA512'" } @@ -47,9 +49,10 @@ Write-Host "Extracting Ninja-Build..." $NINJA_INSTALL_PATH = "$env:GITHUB_WORKSPACE\buildtools\ninja" New-Item -ItemType Directory -Force -Path "${NINJA_INSTALL_PATH}" Expand-Archive -LiteralPath "${NINJA_DL_PATH}" -DestinationPath "${NINJA_INSTALL_PATH}" + # Export the NINJA executable path -echo "NINJA_INSTALL_PATH=${NINJA_INSTALL_PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -echo "PATH=${NINJA_INSTALL_PATH};$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append +"NINJA_INSTALL_PATH=${NINJA_INSTALL_PATH}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append +"PATH=${NINJA_INSTALL_PATH}; $env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append -# Add CMAKE_DEFINES -echo "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} -GNinja" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append +# export CMAKE_DEFINES to the runner environment +"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 diff --git a/scripts/install-zlib.ps1 b/scripts/install-zlib.ps1 new file mode 100644 index 000000000..c9b543638 --- /dev/null +++ b/scripts/install-zlib.ps1 @@ -0,0 +1,44 @@ +$DL_BASEDIR = "$env:GITHUB_WORKSPACE\dl" +if (!(Test-Path -Path "$DL_BASEDIR")) { New-Item -ItemType Directory -Force -Path "$DL_BASEDIR" } + +$ZLIB_RELEASE = "1.3.1"; +$ZLIB_RELEASE_ASSET = "zlib131.zip" +$ZLIB_DL_URL = "https://github.com/madler/zlib/releases/download/v${ZLIB_RELEASE}/${ZLIB_RELEASE_ASSET}" +$ZLIB_DL_SHA512 = "1f171880153b0120e1364baaf7d0a17f65086eff279f8f8c8538e5950097d1feee37cc173181676ba1e2aeb4565ba68749c814cd3e25bfb06271bea02feb7d94" +$ZLIB_DL_PATH = "${DL_BASEDIR}\${ZLIB_RELEASE_ASSET}" +$CurlArguments = '-s', '-Lf', '-o', "${ZLIB_DL_PATH}", "${ZLIB_DL_URL}" +& curl.exe @CurlArguments +$zlib_zip_hash = Get-FileHash -LiteralPath "${ZLIB_DL_PATH}" -Algorithm SHA512 +if ($zlib_zip_hash.Hash -eq $ZLIB_DL_SHA512) { + Write-Host "Successfully downloaded ${ZLIB_RELEASE_ASSET}" +} +Else { + Write-Error "The downloaded ${ZLIB_RELEASE_ASSET} hash '$($zlib_zip_hash.Hash)' does not match the expected hash: '$ZLIB_DL_SHA512'" +} + +Write-Host "Extracting zlib source..." +$ZLIB_SOURCE_PATH = "$env:GITHUB_WORKSPACE\buildtools\zlib-${ZLIB_RELEASE}" +Expand-Archive -LiteralPath "${ZLIB_DL_PATH}" -DestinationPath "$env:GITHUB_WORKSPACE\buildtools" + +Write-Host "Building zlib source..." +$ZLIB_BUILD_PATH = "$env:GITHUB_WORKSPACE\buildtools\zlib_build" +if ($env:TEST_MINGW -eq 1) { + cmake.exe -B "${ZLIB_BUILD_PATH}" -S "${ZLIB_SOURCE_PATH}" -GNinja +} +Elseif ($env:TEST_X86 -eq 1) { + cmake.exe -B "${ZLIB_BUILD_PATH}" -S "${ZLIB_SOURCE_PATH}" -AWin32 +} +Else { + cmake.exe -B "${ZLIB_BUILD_PATH}" -S "${ZLIB_SOURCE_PATH}" +} +cmake.exe --build "${ZLIB_BUILD_PATH}" --target zlibstatic +Copy-Item "${ZLIB_SOURCE_PATH}\zlib.h" "${ZLIB_BUILD_PATH}" + +# Append zlib CMAKE_DEFINES to the runner env. +if ($env:TEST_MINGW -eq 1) { + $NEW_CMAKE_DEFINES="CMAKE_DEFINES=${env:CMAKE_DEFINES} -DZLIB_LIBRARY=${ZLIB_BUILD_PATH}\libzlibstatic.a -DZLIB_INCLUDE_DIR=${ZLIB_BUILD_PATH} -GNinja" +} +Else { + $NEW_CMAKE_DEFINES="CMAKE_DEFINES=-DZLIB_LIBRARY=${ZLIB_BUILD_PATH}\Debug\zlibstaticd.lib -DZLIB_INCLUDE_DIR=${ZLIB_BUILD_PATH}" +} +$NEW_CMAKE_DEFINES | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append \ No newline at end of file diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 96d961657..711dc5700 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -7,10 +7,15 @@ extern "C" { #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" +#ifdef SENTRY_PLATFORM_WINDOWS +# include "sentry_os.h" +#endif #include "sentry_path.h" #include "sentry_sync.h" #include "sentry_transport.h" -#include "sentry_unix_pageallocator.h" +#ifdef SENTRY_PLATFORM_LINUX +# include "sentry_unix_pageallocator.h" +#endif #include "sentry_utils.h" #include "transports/sentry_disk_transport.h" } @@ -110,6 +115,51 @@ crashpad_backend_user_consent_changed(sentry_backend_t *backend) data->db->GetSettings()->SetUploadsEnabled(!sentry__should_skip_upload()); } +#ifdef SENTRY_PLATFORM_WINDOWS +static void +crashpad_register_wer_module( + const sentry_path_t *absolute_handler_path, const crashpad_state_t *data) +{ + windows_version_t win_ver; + if (!sentry__get_windows_version(&win_ver) || win_ver.build < 19041) { + SENTRY_WARN("Crashpad WER module not registered, because Windows " + "doesn't meet version requirements (build >= 19041)."); + return; + } + sentry_path_t *handler_dir = sentry__path_dir(absolute_handler_path); + sentry_path_t *wer_path = nullptr; + if (handler_dir) { + wer_path = sentry__path_join_str(handler_dir, "crashpad_wer.dll"); + sentry__path_free(handler_dir); + } + + if (wer_path && sentry__path_is_file(wer_path)) { + SENTRY_TRACEF("registering crashpad WER handler " + "\"%" SENTRY_PATH_PRI "\"", + wer_path->path); + + // The WER handler needs to be registered in the registry first. + constexpr DWORD dwOne = 1; + const LSTATUS reg_res = RegSetKeyValueW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\Windows Error Reporting\\" + L"RuntimeExceptionHelperModules", + wer_path->path, REG_DWORD, &dwOne, sizeof(DWORD)); + if (reg_res != ERROR_SUCCESS) { + SENTRY_WARN("registering crashpad WER handler in registry failed"); + } else { + const std::wstring wer_path_string(wer_path->path); + if (!data->client->RegisterWerModule(wer_path_string)) { + SENTRY_WARN("registering crashpad WER handler module failed"); + } + } + + sentry__path_free(wer_path); + } else { + SENTRY_WARN("crashpad WER handler module not found"); + } +} +#endif + static void crashpad_backend_flush_scope( sentry_backend_t *backend, const sentry_options_t *options) @@ -363,39 +413,9 @@ crashpad_backend_startup( return 1; } -#ifdef CRASHPAD_WER_ENABLED - sentry_path_t *handler_dir = sentry__path_dir(absolute_handler_path); - sentry_path_t *wer_path = nullptr; - if (handler_dir) { - wer_path = sentry__path_join_str(handler_dir, "crashpad_wer.dll"); - sentry__path_free(handler_dir); - } - - if (wer_path && sentry__path_is_file(wer_path)) { - SENTRY_TRACEF("registering crashpad WER handler " - "\"%" SENTRY_PATH_PRI "\"", - wer_path->path); - - // The WER handler needs to be registered in the registry first. - DWORD dwOne = 1; - LSTATUS reg_res = RegSetKeyValueW(HKEY_CURRENT_USER, - L"Software\\Microsoft\\Windows\\Windows Error Reporting\\" - L"RuntimeExceptionHelperModules", - wer_path->path, REG_DWORD, &dwOne, sizeof(DWORD)); - if (reg_res != ERROR_SUCCESS) { - SENTRY_WARN("registering crashpad WER handler in registry failed"); - } else { - std::wstring wer_path_string(wer_path->path); - if (!data->client->RegisterWerModule(wer_path_string)) { - SENTRY_WARN("registering crashpad WER handler module failed"); - } - } - - sentry__path_free(wer_path); - } else { - SENTRY_WARN("crashpad WER handler module not found"); - } -#endif // CRASHPAD_WER_ENABLED +#ifdef SENTRY_PLATFORM_WINDOWS + crashpad_register_wer_module(absolute_handler_path, data); +#endif sentry__path_free(absolute_handler_path); diff --git a/src/modulefinder/sentry_modulefinder_linux.c b/src/modulefinder/sentry_modulefinder_linux.c index 1d459c12f..1ae8e2b73 100644 --- a/src/modulefinder/sentry_modulefinder_linux.c +++ b/src/modulefinder/sentry_modulefinder_linux.c @@ -568,12 +568,14 @@ try_append_module(sentry_value_t modules, const sentry_module_t *module) } // copied from: -// https://github.com/google/breakpad/blob/216cea7bca53fa441a3ee0d0f5fd339a3a894224/src/client/linux/minidump_writer/linux_dumper.h#L61-L70 +// https://github.com/google/breakpad/blob/eb28e7ed9c1c1e1a717fa34ce0178bf471a6311f/src/client/linux/minidump_writer/linux_dumper.h#L61-L69 #if defined(__i386) || defined(__ARM_EABI__) \ - || (defined(__mips__) && _MIPS_SIM == _ABIO32) + || (defined(__mips__) && _MIPS_SIM == _ABIO32) \ + || (defined(__riscv) && __riscv_xlen == 32) typedef Elf32_auxv_t elf_aux_entry; -#elif defined(__x86_64) || defined(__aarch64__) || defined(__powerpc64__) \ - || (defined(__mips__) && _MIPS_SIM != _ABIO32) +#elif defined(__x86_64) || defined(__aarch64__) \ + || (defined(__mips__) && _MIPS_SIM != _ABIO32) \ + || (defined(__riscv) && __riscv_xlen == 64) typedef Elf64_auxv_t elf_aux_entry; #endif diff --git a/src/sentry_core.c b/src/sentry_core.c index 24cf52539..c9ef8cd45 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -550,6 +550,26 @@ sentry__prepare_transaction(const sentry_options_t *options, return NULL; } +sentry_envelope_t * +sentry__prepare_user_feedback(sentry_value_t user_feedback) +{ + sentry_envelope_t *envelope = NULL; + + envelope = sentry__envelope_new(); + if (!envelope + || !sentry__envelope_add_user_feedback(envelope, user_feedback)) { + goto fail; + } + + return envelope; + +fail: + SENTRY_WARN("dropping user feedback"); + sentry_envelope_free(envelope); + sentry_value_decref(user_feedback); + return NULL; +} + void sentry_handle_exception(const sentry_ucontext_t *uctx) { @@ -1120,6 +1140,20 @@ sentry_span_finish(sentry_span_t *opaque_span) sentry__span_decref(opaque_span); } +void +sentry_capture_user_feedback(sentry_value_t user_feedback) +{ + sentry_envelope_t *envelope = NULL; + + SENTRY_WITH_OPTIONS (options) { + envelope = sentry__prepare_user_feedback(user_feedback); + if (envelope) { + sentry__capture_envelope(options->transport, envelope); + } + } + sentry_value_decref(user_feedback); +} + int sentry_get_crashed_last_run(void) { diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 4b68a2787..b71242c1d 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -295,6 +295,36 @@ sentry__envelope_add_transaction( return item; } +sentry_envelope_item_t * +sentry__envelope_add_user_feedback( + sentry_envelope_t *envelope, sentry_value_t user_feedback) +{ + sentry_envelope_item_t *item = envelope_add_item(envelope); + if (!item) { + return NULL; + } + + sentry_jsonwriter_t *jw = sentry__jsonwriter_new(NULL); + if (!jw) { + return NULL; + } + + sentry_value_t event_id = sentry__ensure_event_id(user_feedback, NULL); + + sentry__jsonwriter_write_value(jw, user_feedback); + item->payload = sentry__jsonwriter_into_string(jw, &item->payload_len); + + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("user_report")); + sentry_value_t length = sentry_value_new_int32((int32_t)item->payload_len); + sentry__envelope_item_set_header(item, "length", length); + + sentry_value_incref(event_id); + sentry__envelope_set_header(envelope, "event_id", event_id); + + return item; +} + sentry_envelope_item_t * sentry__envelope_add_session( sentry_envelope_t *envelope, const sentry_session_t *session) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index b5a8f1ab0..8ea986827 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -42,6 +42,12 @@ sentry_envelope_item_t *sentry__envelope_add_event( sentry_envelope_item_t *sentry__envelope_add_transaction( sentry_envelope_t *envelope, sentry_value_t transaction); +/** + * Add a user feedback to this envelope. + */ +sentry_envelope_item_t *sentry__envelope_add_user_feedback( + sentry_envelope_t *envelope, sentry_value_t user_feedback); + /** * Add a session to this envelope. */ diff --git a/src/sentry_os.c b/src/sentry_os.c index fde706684..4675575c2 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -8,10 +8,9 @@ # define CURRENT_VERSION "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion" void * -sentry__try_file_version(LPCWSTR filename) +sentry__try_file_version(const LPCWSTR filename) { - - DWORD size = GetFileVersionInfoSizeW(filename, NULL); + const DWORD size = GetFileVersionInfoSizeW(filename, NULL); if (!size) { return NULL; } @@ -24,86 +23,115 @@ sentry__try_file_version(LPCWSTR filename) return ffibuf; } -sentry_value_t -sentry__get_os_context(void) +int +sentry__get_kernel_version(windows_version_t *win_ver) { - sentry_value_t os = sentry_value_new_object(); - if (sentry_value_is_null(os)) { - return os; - } - - sentry_value_set_by_key(os, "name", sentry_value_new_string("Windows")); - void *ffibuf = sentry__try_file_version(L"ntoskrnl.exe"); if (!ffibuf) { ffibuf = sentry__try_file_version(L"kernel32.dll"); } if (!ffibuf) { - goto fail; + return 0; } VS_FIXEDFILEINFO *ffi; UINT ffi_size; - if (!VerQueryValueW(ffibuf, L"\\", &ffi, &ffi_size)) { - goto fail; + if (!VerQueryValueW(ffibuf, L"\\", (LPVOID *)&ffi, &ffi_size)) { + sentry_free(ffibuf); + return 0; } ffi->dwFileFlags &= ffi->dwFileFlagsMask; - uint32_t major_version = ffi->dwFileVersionMS >> 16; - uint32_t minor_version = ffi->dwFileVersionMS & 0xffff; - uint32_t build_version = ffi->dwFileVersionLS >> 16; - uint32_t ubr = ffi->dwFileVersionLS & 0xffff; - - char buf[32]; - snprintf(buf, sizeof(buf), "%u.%u.%u.%lu", major_version, minor_version, - build_version, ubr); - sentry_value_set_by_key(os, "kernel_version", sentry_value_new_string(buf)); + win_ver->major = ffi->dwFileVersionMS >> 16; + win_ver->minor = ffi->dwFileVersionMS & 0xffff; + win_ver->build = ffi->dwFileVersionLS >> 16; + win_ver->ubr = ffi->dwFileVersionLS & 0xffff; sentry_free(ffibuf); + return 1; +} + +int +sentry__get_windows_version(windows_version_t *win_ver) +{ // The `CurrentMajorVersionNumber`, `CurrentMinorVersionNumber` and `UBR` // are DWORD, while `CurrentBuild` is a SZ (text). - uint32_t reg_version = 0; DWORD buf_size = sizeof(uint32_t); if (RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "CurrentMajorVersionNumber", RRF_RT_REG_DWORD, NULL, ®_version, &buf_size) - == ERROR_SUCCESS) { - major_version = reg_version; + != ERROR_SUCCESS) { + return 0; } + win_ver->major = reg_version; + buf_size = sizeof(uint32_t); if (RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "CurrentMinorVersionNumber", RRF_RT_REG_DWORD, NULL, ®_version, &buf_size) - == ERROR_SUCCESS) { - minor_version = reg_version; + != ERROR_SUCCESS) { + return 0; } + win_ver->minor = reg_version; + + char buf[32]; buf_size = sizeof(buf); if (RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "CurrentBuild", RRF_RT_REG_SZ, NULL, buf, &buf_size) - == ERROR_SUCCESS) { - build_version = (uint32_t)sentry__strtod_c(buf, NULL); + != ERROR_SUCCESS) { + return 0; } + win_ver->build = (uint32_t)sentry__strtod_c(buf, NULL); + buf_size = sizeof(uint32_t); if (RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "UBR", RRF_RT_REG_DWORD, NULL, ®_version, &buf_size) - == ERROR_SUCCESS) { - ubr = reg_version; + != ERROR_SUCCESS) { + return 0; } + win_ver->ubr = reg_version; - snprintf(buf, sizeof(buf), "%u.%u.%u", major_version, minor_version, - build_version); - sentry_value_set_by_key(os, "version", sentry_value_new_string(buf)); + return 1; +} - snprintf(buf, sizeof(buf), "%lu", ubr); - sentry_value_set_by_key(os, "build", sentry_value_new_string(buf)); +sentry_value_t +sentry__get_os_context(void) +{ + const sentry_value_t os = sentry_value_new_object(); + if (sentry_value_is_null(os)) { + return os; + } + sentry_value_set_by_key(os, "name", sentry_value_new_string("Windows")); - sentry_value_freeze(os); - return os; + bool at_least_one_key_successful = false; + char buf[32]; + windows_version_t win_ver; + if (sentry__get_kernel_version(&win_ver)) { + at_least_one_key_successful = true; -fail: - sentry_free(ffibuf); + snprintf(buf, sizeof(buf), "%u.%u.%u.%lu", win_ver.major, win_ver.minor, + win_ver.build, win_ver.ubr); + sentry_value_set_by_key( + os, "kernel_version", sentry_value_new_string(buf)); + } + + if (sentry__get_windows_version(&win_ver)) { + at_least_one_key_successful = true; + + snprintf(buf, sizeof(buf), "%u.%u.%u", win_ver.major, win_ver.minor, + win_ver.build); + sentry_value_set_by_key(os, "version", sentry_value_new_string(buf)); + + snprintf(buf, sizeof(buf), "%lu", win_ver.ubr); + sentry_value_set_by_key(os, "build", sentry_value_new_string(buf)); + } + + if (at_least_one_key_successful) { + sentry_value_freeze(os); + return os; + } sentry_value_decref(os); return sentry_value_new_null(); diff --git a/src/sentry_os.h b/src/sentry_os.h index 0a0488fd1..cefff7978 100644 --- a/src/sentry_os.h +++ b/src/sentry_os.h @@ -3,6 +3,18 @@ #include "sentry_boot.h" +#ifdef SENTRY_PLATFORM_WINDOWS +typedef struct { + uint32_t major; + uint32_t minor; + uint32_t build; + uint32_t ubr; +} windows_version_t; + +int sentry__get_kernel_version(windows_version_t *win_ver); +int sentry__get_windows_version(windows_version_t *win_ver); +#endif + sentry_value_t sentry__get_os_context(void); #endif diff --git a/src/sentry_transport.c b/src/sentry_transport.c index ea62c5620..097546441 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -5,9 +5,24 @@ #include "sentry_ratelimiter.h" #include "sentry_string.h" +// 强制开启 SENTRY_TRANSPORT_COMPRESSION +#ifndef SENTRY_TRANSPORT_COMPRESSION +# define SENTRY_TRANSPORT_COMPRESSION +#endif // !SENTRY_TRANSPORT_COMPRESSION + +#ifdef SENTRY_TRANSPORT_COMPRESSION +# include "external/crashpad/third_party/zlib/zlib/zlib.h" +#endif + #define ENVELOPE_MIME "application/x-sentry-envelope" +#ifdef SENTRY_TRANSPORT_COMPRESSION +// The headers we use are: `x-sentry-auth`, `content-type`, `content-encoding`, +// `content-length` +# define MAX_HTTP_HEADERS 4 +#else // The headers we use are: `x-sentry-auth`, `content-type`, `content-length` -#define MAX_HTTP_HEADERS 3 +# define MAX_HTTP_HEADERS 3 +#endif typedef struct sentry_transport_s { void (*send_envelope_func)(sentry_envelope_t *envelope, void *state); @@ -148,6 +163,56 @@ sentry_transport_free(sentry_transport_t *transport) sentry_free(transport); } +#ifdef SENTRY_TRANSPORT_COMPRESSION +static bool +gzipped_with_compression(const char *body, const size_t body_len, + char **compressed_body, size_t *compressed_body_len) +{ + if (!body || body_len == 0) { + return false; + } + + z_stream stream; + memset(&stream, 0, sizeof(stream)); + stream.next_in = (unsigned char *)body; + stream.avail_in = (unsigned int)body_len; + + int err = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + MAX_WBITS + 16, 9, Z_DEFAULT_STRATEGY); + if (err != Z_OK) { + SENTRY_TRACEF("deflateInit2 failed: %d", err); + return false; + } + + size_t len = compressBound((unsigned long)body_len); + char *buffer = sentry_malloc(len); + if (!buffer) { + deflateEnd(&stream); + return false; + } + + while (err == Z_OK) { + stream.next_out = (unsigned char *)(buffer + stream.total_out); + stream.avail_out = (unsigned int)(len - stream.total_out); + err = deflate(&stream, Z_FINISH); + } + + if (err != Z_STREAM_END) { + SENTRY_TRACEF("deflate failed: %d", err); + sentry_free(buffer); + buffer = NULL; + deflateEnd(&stream); + return false; + } + + *compressed_body_len = stream.total_out; + *compressed_body = buffer; + + deflateEnd(&stream); + return true; +} +#endif + sentry_prepared_http_request_t * sentry__prepare_http_request(sentry_envelope_t *envelope, const sentry_dsn_t *dsn, const sentry_rate_limiter_t *rl, @@ -165,6 +230,23 @@ sentry__prepare_http_request(sentry_envelope_t *envelope, return NULL; } +#ifdef SENTRY_TRANSPORT_COMPRESSION + bool compressed = false; + char *compressed_body = NULL; + size_t compressed_body_len = 0; + compressed = gzipped_with_compression( + body, body_len, &compressed_body, &compressed_body_len); + if (compressed) { + if (body_owned) { + sentry_free(body); + body_owned = false; + } + body = compressed_body; + body_len = compressed_body_len; + body_owned = true; + } +#endif + sentry_prepared_http_request_t *req = SENTRY_MAKE(sentry_prepared_http_request_t); if (!req) { @@ -196,6 +278,14 @@ sentry__prepare_http_request(sentry_envelope_t *envelope, h->key = "content-type"; h->value = sentry__string_clone(ENVELOPE_MIME); +#ifdef SENTRY_TRANSPORT_COMPRESSION + if (compressed) { + h = &req->headers[req->headers_len++]; + h->key = "content-encoding"; + h->value = sentry__string_clone("gzip"); + } +#endif + h = &req->headers[req->headers_len++]; h->key = "content-length"; h->value = sentry__int64_to_string((int64_t)body_len); diff --git a/src/sentry_value.c b/src/sentry_value.c index e46eb79f5..3c3e344c9 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1265,6 +1265,42 @@ sentry_value_new_stacktrace(void **ips, size_t len) return stacktrace; } +sentry_value_t +sentry_value_new_user_feedback(const sentry_uuid_t *uuid, const char *name, + const char *email, const char *comments) +{ + size_t name_len = name ? strlen(name) : 0; + size_t email_len = email ? strlen(email) : 0; + size_t comments_len = email ? strlen(comments) : 0; + return sentry_value_new_user_feedback_n( + uuid, name, name_len, email, email_len, comments, comments_len); +} + +sentry_value_t +sentry_value_new_user_feedback_n(const sentry_uuid_t *uuid, const char *name, + size_t name_len, const char *email, size_t email_len, const char *comments, + size_t comments_len) +{ + sentry_value_t rv = sentry_value_new_object(); + + sentry_value_set_by_key(rv, "event_id", sentry__value_new_uuid(uuid)); + + if (name) { + sentry_value_set_by_key( + rv, "name", sentry_value_new_string_n(name, name_len)); + } + if (email) { + sentry_value_set_by_key( + rv, "email", sentry_value_new_string_n(email, email_len)); + } + if (comments) { + sentry_value_set_by_key( + rv, "comments", sentry_value_new_string_n(comments, comments_len)); + } + + return rv; +} + static sentry_value_t sentry__get_or_insert_values_list(sentry_value_t parent, const char *key) { diff --git a/tests/__init__.py b/tests/__init__.py index b34e1c2ea..61380f90b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ +import gzip import subprocess import os import io @@ -162,10 +163,17 @@ def deserialize_from( @classmethod def deserialize( - cls, bytes # type: bytes + cls, data # type: bytes ): # type: (...) -> Envelope - return cls.deserialize_from(io.BytesIO(bytes)) + + # check if the data is gzip encoded and extract it before deserialization. + # 0x1f8b: gzip-magic, 0x08: `DEFLATE` compression method. + if data[:3] == b"\x1f\x8b\x08": + with gzip.open(io.BytesIO(data), "rb") as output: + return cls.deserialize_from(output) + + return cls.deserialize_from(io.BytesIO(data)) def print_verbose(self, indent=0): """Pretty prints the envelope.""" @@ -254,7 +262,7 @@ def deserialize_from( headers = json.loads(line) length = headers["length"] payload = f.read(length) - if headers.get("type") in ["event", "session", "transaction"]: + if headers.get("type") in ["event", "session", "transaction", "user_report"]: rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: rv = cls(headers=headers, payload=payload) diff --git a/tests/assertions.py b/tests/assertions.py index f12ec9972..93f1b14ab 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -4,7 +4,7 @@ import re import sys from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, UTC import msgpack @@ -38,6 +38,18 @@ def assert_session(envelope, extra_assertion=None): assert_matches(session, extra_assertion) +def assert_user_feedback(envelope): + user_feedback = None + for item in envelope: + if item.headers.get("type") == "user_report" and item.payload.json is not None: + user_feedback = item.payload.json + + assert user_feedback is not None + assert user_feedback["name"] == "some-name" + assert user_feedback["email"] == "some-email" + assert user_feedback["comments"] == "some-comment" + + def assert_meta( envelope, release="test-example-release", @@ -54,13 +66,16 @@ def assert_meta( "user": {"id": 42, "username": "some_name"}, "transaction": transaction, "tags": {"expected-tag": "some value"}, - "extra": {"extra stuff": "some value", "…unicode key…": "őá…–🤮🚀¿ 한글 테스트"}, + "extra": { + "extra stuff": "some value", + "…unicode key…": "őá…–🤮🚀¿ 한글 테스트", + }, } expected_sdk = { "name": "sentry.native", - "version": "0.7.0", + "version": "0.7.2", "packages": [ - {"name": "github:getsentry/sentry-native", "version": "0.7.0"}, + {"name": "github:getsentry/sentry-native", "version": "0.7.2"}, ], } if is_android: @@ -142,6 +157,12 @@ def assert_breadcrumb(envelope): "message": "debug crumb", "category": "example!", "level": "debug", + "data": { + "url": "https://example.com/api/1.0/users", + "method": "GET", + "status_code": 200, + "reason": "OK", + }, } assert any(matches(b, expected) for b in event["breadcrumbs"]) @@ -164,16 +185,16 @@ def assert_minidump(envelope): assert minidump.payload.bytes.startswith(b"MDMP") -def assert_timestamp(ts, now=datetime.utcnow()): +def assert_timestamp(ts, now=datetime.now(UTC)): assert ts[:11] == now.isoformat()[:11] -def assert_event(envelope): +def assert_event(envelope, message="Hello World!"): event = envelope.get_event() expected = { "level": "info", "logger": "my-logger", - "message": {"formatted": "Hello World!"}, + "message": {"formatted": message}, } assert_matches(event, expected) assert_timestamp(event["timestamp"]) @@ -242,6 +263,9 @@ def _load_crashpad_attachments(msg): breadcrumb1 = [] breadcrumb2 = [] for part in msg.walk(): + if part.get_filename() is not None: + assert part.get("Content-Type") is None + match part.get_filename(): case "__sentry-event": event = msgpack.unpackb(part.get_payload(decode=True)) @@ -286,3 +310,11 @@ def assert_crashpad_upload(req): and b"\n\nMDMP" in part.as_bytes() for part in msg.walk() ) + + +def assert_gzip_file_header(output): + assert output[:3] == b"\x1f\x8b\x08" + + +def assert_gzip_content_encoding(req): + assert req.content_encoding == "gzip" diff --git a/tests/cmake.py b/tests/cmake.py index 35c48a8b5..906b8afad 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -1,9 +1,10 @@ -import os import json -import sys +import os +import shutil import subprocess +import sys + import pytest -import shutil class CMake: diff --git a/tests/requirements.txt b/tests/requirements.txt index c90370ba0..cbc8051ec 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,5 @@ -black==23.3.0 -pytest==7.4.0 -pytest-httpserver==1.0.8 -msgpack==1.0.5 +black==24.3.0 +pytest==8.1.1 +pytest-httpserver==1.0.10 +msgpack==1.0.8 +pytest-xdist==3.5.0 \ No newline at end of file diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index fda314da6..28647bcc4 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -1,11 +1,17 @@ -import pytest import os import shutil import sys import time + +import pytest + from . import make_dsn, run, Envelope +from .assertions import ( + assert_crashpad_upload, + assert_session, + assert_gzip_file_header, +) from .conditions import has_crashpad -from .assertions import assert_crashpad_upload, assert_session pytestmark = pytest.mark.skipif(not has_crashpad, reason="tests need crashpad backend") @@ -118,13 +124,15 @@ def test_crashpad_wer_crash(cmake, httpserver, run_args): @pytest.mark.parametrize( - "run_args", + "run_args,build_args", [ # if we crash, we want a dump - ([]), + ([], {"SENTRY_TRANSPORT_COMPRESSION": "Off"}), + ([], {"SENTRY_TRANSPORT_COMPRESSION": "On"}), # if we crash and before-send doesn't discard, we want a dump pytest.param( ["before-send"], + {}, marks=pytest.mark.skipif( sys.platform == "darwin", reason="crashpad doesn't provide SetFirstChanceExceptionHandler on macOS", @@ -133,6 +141,7 @@ def test_crashpad_wer_crash(cmake, httpserver, run_args): # if on_crash() is non-discarding, a discarding before_send() is overruled, so we get a dump pytest.param( ["discarding-before-send", "on-crash"], + {}, marks=pytest.mark.skipif( sys.platform == "darwin", reason="crashpad doesn't provide SetFirstChanceExceptionHandler on macOS", @@ -140,8 +149,9 @@ def test_crashpad_wer_crash(cmake, httpserver, run_args): ), ], ) -def test_crashpad_dumping_crash(cmake, httpserver, run_args): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) +def test_crashpad_dumping_crash(cmake, httpserver, run_args, build_args): + build_args.update({"SENTRY_BACKEND": "crashpad"}) + tmp_path = cmake(["sentry_example"], build_args) # make sure we are isolated from previous runs shutil.rmtree(tmp_path / ".sentry-native", ignore_errors=True) @@ -169,19 +179,24 @@ def test_crashpad_dumping_crash(cmake, httpserver, run_args): run(tmp_path, "sentry_example", ["log", "no-setup"], check=True, env=env) assert len(httpserver.log) == 2 - outputs = (httpserver.log[0][0], httpserver.log[1][0]) session, multipart = ( - (outputs[0].get_data(), outputs[1]) - if b'"type":"session"' in outputs[0].get_data() - else (outputs[1].get_data(), outputs[0]) + (httpserver.log[0][0], httpserver.log[1][0]) + if is_session_envelope(httpserver.log[0][0].get_data()) + else (httpserver.log[1][0], httpserver.log[0][0]) ) - envelope = Envelope.deserialize(session) + if build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On": + assert_gzip_file_header(session.get_data()) + envelope = Envelope.deserialize(session.get_data()) assert_session(envelope, {"status": "crashed", "errors": 1}) assert_crashpad_upload(multipart) +def is_session_envelope(data): + return b'"type":"session"' in data + + @pytest.mark.skipif( sys.platform == "darwin", reason="crashpad doesn't provide SetFirstChanceExceptionHandler on macOS", diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index ee915f00e..d0ee056cb 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -16,20 +16,31 @@ assert_exception, assert_inproc_crash, assert_session, + assert_user_feedback, assert_minidump, assert_breakpad_crash, + assert_gzip_content_encoding, + assert_gzip_file_header, ) from .conditions import has_http, has_breakpad, has_files pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") auth_header = ( - "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.7.0" + "Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/0.7.2" ) -def test_capture_http(cmake, httpserver): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) +@pytest.mark.parametrize( + "build_args", + [ + ({"SENTRY_TRANSPORT_COMPRESSION": "Off"}), + ({"SENTRY_TRANSPORT_COMPRESSION": "On"}), + ], +) +def test_capture_http(cmake, httpserver, build_args): + build_args.update({"SENTRY_BACKEND": "none"}) + tmp_path = cmake(["sentry_example"], build_args) httpserver.expect_oneshot_request( "/api/123456/envelope/", @@ -46,8 +57,14 @@ def test_capture_http(cmake, httpserver): ) assert len(httpserver.log) == 1 - output = httpserver.log[0][0].get_data() - envelope = Envelope.deserialize(output) + req = httpserver.log[0][0] + body = req.get_data() + + if build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On": + assert_gzip_content_encoding(req) + assert_gzip_file_header(body) + + envelope = Envelope.deserialize(body) assert_meta(envelope, "🤮🚀") assert_breadcrumb(envelope) @@ -117,6 +134,35 @@ def test_capture_and_session_http(cmake, httpserver): assert_session(envelope, {"status": "exited", "errors": 0}) +def test_user_feedback_http(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "capture-user-feedback"], + check=True, + env=env, + ) + + assert len(httpserver.log) == 2 + output = httpserver.log[0][0].get_data() + envelope = Envelope.deserialize(output) + + assert_event(envelope, "Hello user feedback!") + + output = httpserver.log[1][0].get_data() + envelope = Envelope.deserialize(output) + + assert_user_feedback(envelope) + + def test_exception_and_session_http(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) @@ -202,8 +248,16 @@ def test_abnormal_session(cmake, httpserver): assert_session(envelope1, {"status": "abnormal", "errors": 0, "duration": 10}) -def test_inproc_crash_http(cmake, httpserver): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) +@pytest.mark.parametrize( + "build_args", + [ + ({"SENTRY_TRANSPORT_COMPRESSION": "Off"}), + ({"SENTRY_TRANSPORT_COMPRESSION": "On"}), + ], +) +def test_inproc_crash_http(cmake, httpserver, build_args): + build_args.update({"SENTRY_BACKEND": "inproc"}) + tmp_path = cmake(["sentry_example"], build_args) httpserver.expect_request( "/api/123456/envelope/", @@ -228,7 +282,14 @@ def test_inproc_crash_http(cmake, httpserver): ) assert len(httpserver.log) == 1 - envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + req = httpserver.log[0][0] + body = req.get_data() + + if build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On": + assert_gzip_content_encoding(req) + assert_gzip_file_header(body) + + envelope = Envelope.deserialize(body) assert_session(envelope, {"init": True, "status": "crashed", "errors": 1}) @@ -287,8 +348,16 @@ def test_inproc_dump_inflight(cmake, httpserver): @pytest.mark.skipif(not has_breakpad, reason="test needs breakpad backend") -def test_breakpad_crash_http(cmake, httpserver): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "breakpad"}) +@pytest.mark.parametrize( + "build_args", + [ + ({"SENTRY_TRANSPORT_COMPRESSION": "Off"}), + ({"SENTRY_TRANSPORT_COMPRESSION": "On"}), + ], +) +def test_breakpad_crash_http(cmake, httpserver, build_args): + build_args.update({"SENTRY_BACKEND": "breakpad"}) + tmp_path = cmake(["sentry_example"], build_args) httpserver.expect_request( "/api/123456/envelope/", @@ -313,7 +382,14 @@ def test_breakpad_crash_http(cmake, httpserver): ) assert len(httpserver.log) == 1 - envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + req = httpserver.log[0][0] + body = req.get_data() + + if build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On": + assert_gzip_content_encoding(req) + assert_gzip_file_header(body) + + envelope = Envelope.deserialize(body) assert_session(envelope, {"init": True, "status": "crashed", "errors": 1}) @@ -423,8 +499,16 @@ def delayed(req): RFC3339_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" -def test_transaction_only(cmake, httpserver): - tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) +@pytest.mark.parametrize( + "build_args", + [ + ({"SENTRY_TRANSPORT_COMPRESSION": "Off"}), + ({"SENTRY_TRANSPORT_COMPRESSION": "On"}), + ], +) +def test_transaction_only(cmake, httpserver, build_args): + build_args.update({"SENTRY_BACKEND": "none"}) + tmp_path = cmake(["sentry_example"], build_args) httpserver.expect_oneshot_request( "/api/123456/envelope/", @@ -441,8 +525,14 @@ def test_transaction_only(cmake, httpserver): ) assert len(httpserver.log) == 1 - output = httpserver.log[0][0].get_data() - envelope = Envelope.deserialize(output) + req = httpserver.log[0][0] + body = req.get_data() + + if build_args.get("SENTRY_TRANSPORT_COMPRESSION") == "On": + assert_gzip_content_encoding(req) + assert_gzip_file_header(body) + + envelope = Envelope.deserialize(body) # Show what the envelope looks like if the test fails. envelope.print_verbose() diff --git a/tests/test_unit.py b/tests/test_unit.py index f75a719d7..3633efec9 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -14,6 +14,9 @@ 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"]: + pytest.skip("excluded from transport test-suite") + cwd = cmake(["sentry_test_unit"], {"SENTRY_BACKEND": "none"}) env = dict(os.environ) run(cwd, "sentry_test_unit", ["--no-summary", unittest], check=True, env=env) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 7be59db4a..803fb4730 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -33,10 +33,12 @@ SENTRY_TEST(basic_http_request_preparation_for_event) TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/envelope/"); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK_STRING_EQUAL(req->body, "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" "{\"type\":\"event\",\"length\":51}\n" "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}"); +#endif sentry__prepared_http_request_free(req); sentry_envelope_free(envelope); @@ -62,14 +64,46 @@ SENTRY_TEST(basic_http_request_preparation_for_transaction) TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/envelope/"); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK_STRING_EQUAL(req->body, "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"sent_at\":" "\"2021-12-16T05:53:59.343Z\"}\n" "{\"type\":\"transaction\",\"length\":72}\n" "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"type\":" "\"transaction\"}"); +#endif + sentry__prepared_http_request_free(req); + sentry_envelope_free(envelope); + + sentry__dsn_decref(dsn); +} +SENTRY_TEST(basic_http_request_preparation_for_user_feedback) +{ + sentry_dsn_t *dsn = sentry__dsn_new("https://foo@sentry.invalid/42"); + + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t user_feedback = sentry_value_new_user_feedback( + &event_id, "some-name", "some-email", "some-comment"); + sentry__envelope_add_user_feedback(envelope, user_feedback); + + sentry_prepared_http_request_t *req + = sentry__prepare_http_request(envelope, dsn, NULL, NULL); + TEST_CHECK_STRING_EQUAL(req->method, "POST"); + TEST_CHECK_STRING_EQUAL( + req->url, "https://sentry.invalid:443/api/42/envelope/"); +#ifndef SENTRY_TRANSPORT_COMPRESSION + TEST_CHECK_STRING_EQUAL(req->body, + "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" + "{\"type\":\"user_report\",\"length\":117}\n" + "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\",\"name\":" + "\"some-name\",\"email\":\"some-email\",\"comments\":" + "\"some-comment\"}"); +#endif sentry__prepared_http_request_free(req); + sentry_value_decref(user_feedback); sentry_envelope_free(envelope); sentry__dsn_decref(dsn); @@ -95,12 +129,14 @@ SENTRY_TEST(basic_http_request_preparation_for_event_with_attachment) TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/envelope/"); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK_STRING_EQUAL(req->body, "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" "{\"type\":\"event\",\"length\":51}\n" "{\"event_id\":\"c993afb6-b4ac-48a6-b61b-2558e601d65d\"}\n" "{\"type\":\"attachment\",\"length\":12}\n" "Hello World!"); +#endif sentry__prepared_http_request_free(req); sentry_envelope_free(envelope); @@ -124,12 +160,14 @@ SENTRY_TEST(basic_http_request_preparation_for_minidump) TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/envelope/"); +#ifndef SENTRY_TRANSPORT_COMPRESSION TEST_CHECK_STRING_EQUAL(req->body, "{}\n" "{\"type\":\"minidump\",\"length\":4}\n" "MDMP\n" "{\"type\":\"attachment\",\"length\":12}\n" "Hello World!"); +#endif sentry__prepared_http_request_free(req); sentry_envelope_free(envelope); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 92e195d58..d1012602b 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -772,3 +772,24 @@ SENTRY_TEST(thread_without_name_still_valid) test_name); sentry_value_decref(thread); } + +SENTRY_TEST(user_feedback_is_valid) +{ + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry_value_t user_feedback = sentry_value_new_user_feedback( + &event_id, "some-name", "some-email", "some-comment"); + + TEST_CHECK(!sentry_value_is_null(user_feedback)); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(user_feedback, "name")), + "some-name"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(user_feedback, "email")), + "some-email"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + user_feedback, "comments")), + "some-comment"); + + sentry_value_decref(user_feedback); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 1770edfc1..d7f9fae29 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -9,6 +9,7 @@ XX(basic_http_request_preparation_for_event) XX(basic_http_request_preparation_for_event_with_attachment) XX(basic_http_request_preparation_for_minidump) XX(basic_http_request_preparation_for_transaction) +XX(basic_http_request_preparation_for_user_feedback) XX(basic_spans) XX(basic_tracing_context) XX(basic_transaction) @@ -107,6 +108,7 @@ XX(update_from_header_null_ctx) XX(url_parsing_complete) XX(url_parsing_invalid) XX(url_parsing_partial) +XX(user_feedback_is_valid) XX(uuid_api) XX(uuid_v4) XX(value_bool)